From c37b80b4b324a4b6aad9fc7a371760079b935d7c Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 22 Dec 2017 15:47:25 +0200 Subject: [PATCH 0001/1846] Change name of the project --- .gitignore | 2 + README.md | 10 +- cli.py | 2 +- config.yml | 4 +- {stakemachine => dexbot}/__init__.py | 0 {stakemachine => dexbot}/basestrategy.py | 0 {stakemachine => dexbot}/bot.py | 0 {stakemachine => dexbot}/cli.py | 2 +- {stakemachine => dexbot}/errors.py | 0 {stakemachine => dexbot}/exceptions.py | 0 {stakemachine => dexbot}/statemachine.py | 0 {stakemachine => dexbot}/storage.py | 0 .../strategies/__init__.py | 0 {stakemachine => dexbot}/strategies/echo.py | 4 +- .../strategies/storagedemo.py | 2 +- dexbot/strategies/walls.py | 122 ++++++++++++++++++ {stakemachine => dexbot}/ui.py | 0 setup.py | 23 ++-- stakemachine/stakemachine.sqlite | Bin 8192 -> 0 bytes 19 files changed, 147 insertions(+), 24 deletions(-) rename {stakemachine => dexbot}/__init__.py (100%) rename {stakemachine => dexbot}/basestrategy.py (100%) rename {stakemachine => dexbot}/bot.py (100%) rename {stakemachine => dexbot}/cli.py (94%) mode change 100755 => 100644 rename {stakemachine => dexbot}/errors.py (100%) rename {stakemachine => dexbot}/exceptions.py (100%) rename {stakemachine => dexbot}/statemachine.py (100%) rename {stakemachine => dexbot}/storage.py (100%) rename {stakemachine => dexbot}/strategies/__init__.py (100%) rename {stakemachine => dexbot}/strategies/echo.py (97%) rename {stakemachine => dexbot}/strategies/storagedemo.py (84%) create mode 100644 dexbot/strategies/walls.py rename {stakemachine => dexbot}/ui.py (100%) delete mode 100644 stakemachine/stakemachine.sqlite diff --git a/.gitignore b/.gitignore index e25ec9d8b..77a45b664 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,5 @@ deprecated *.sqlite *.yaml *.yml +venv/ +.idea/ diff --git a/README.md b/README.md index 680afad46..e3fae6630 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ -# StakeMachine +# DEXBot -Trading Bot Infrastructure for the BitShares Decentralized Exchange +Trading Bot for the BitShares Decentralized Exchange (DEX). **Warning**: This is highly experimental code! Use at your OWN risk! ## Installation - git clone https://github.com/xeroc/stakemachine - cd stakemachine + git clone https://github.com/codaone/dexbot + cd dexbot python3 setup.py install # or python3 setup.py install --user @@ -25,7 +25,7 @@ Add your account's private key to the pybitshares wallet using `uptick` ## Execution - stakemachine run + dexbot run # IMPORTANT NOTE diff --git a/cli.py b/cli.py index d2d6272ad..9ee2f6f34 100755 --- a/cli.py +++ b/cli.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -from stakemachine import cli +from dexbot import cli cli.main() diff --git a/config.yml b/config.yml index e3ac7a8be..a4fb47797 100644 --- a/config.yml +++ b/config.yml @@ -5,9 +5,9 @@ bots: module: "stakemachine.strategies.echo" bot: "Echo" market: GOLD:TEST - account: xeroc + account: codaonetest-1 Demo: module: "stakemachine.strategies.storagedemo" bot: "StorageDemo" market: GOLD:TEST - account: xeroc + account: codaonetest-1 diff --git a/stakemachine/__init__.py b/dexbot/__init__.py similarity index 100% rename from stakemachine/__init__.py rename to dexbot/__init__.py diff --git a/stakemachine/basestrategy.py b/dexbot/basestrategy.py similarity index 100% rename from stakemachine/basestrategy.py rename to dexbot/basestrategy.py diff --git a/stakemachine/bot.py b/dexbot/bot.py similarity index 100% rename from stakemachine/bot.py rename to dexbot/bot.py diff --git a/stakemachine/cli.py b/dexbot/cli.py old mode 100755 new mode 100644 similarity index 94% rename from stakemachine/cli.py rename to dexbot/cli.py index 46dfc4611..c280f16e5 --- a/stakemachine/cli.py +++ b/dexbot/cli.py @@ -12,7 +12,7 @@ warning, alert, ) -from stakemachine.bot import BotInfrastructure +from dexbot.bot import BotInfrastructure log = logging.getLogger(__name__) logging.basicConfig( diff --git a/stakemachine/errors.py b/dexbot/errors.py similarity index 100% rename from stakemachine/errors.py rename to dexbot/errors.py diff --git a/stakemachine/exceptions.py b/dexbot/exceptions.py similarity index 100% rename from stakemachine/exceptions.py rename to dexbot/exceptions.py diff --git a/stakemachine/statemachine.py b/dexbot/statemachine.py similarity index 100% rename from stakemachine/statemachine.py rename to dexbot/statemachine.py diff --git a/stakemachine/storage.py b/dexbot/storage.py similarity index 100% rename from stakemachine/storage.py rename to dexbot/storage.py diff --git a/stakemachine/strategies/__init__.py b/dexbot/strategies/__init__.py similarity index 100% rename from stakemachine/strategies/__init__.py rename to dexbot/strategies/__init__.py diff --git a/stakemachine/strategies/echo.py b/dexbot/strategies/echo.py similarity index 97% rename from stakemachine/strategies/echo.py rename to dexbot/strategies/echo.py index 9355dd6e2..13d8b2561 100644 --- a/stakemachine/strategies/echo.py +++ b/dexbot/strategies/echo.py @@ -1,4 +1,4 @@ -from stakemachine.basestrategy import BaseStrategy +from dexbot.basestrategy import BaseStrategy import logging log = logging.getLogger(__name__) @@ -76,7 +76,7 @@ def print_newBlock(self, i): need to know the most recent block number, you need to use ``bitshares.blockchain.Blockchain`` """ - print("new block: %s" % i) + print("new1 block: %s" % i) # raise ValueError("Testing disabling") def print_accountUpdate(self, i): diff --git a/stakemachine/strategies/storagedemo.py b/dexbot/strategies/storagedemo.py similarity index 84% rename from stakemachine/strategies/storagedemo.py rename to dexbot/strategies/storagedemo.py index 6a117da4b..3579a4c87 100644 --- a/stakemachine/strategies/storagedemo.py +++ b/dexbot/strategies/storagedemo.py @@ -1,4 +1,4 @@ -from stakemachine.basestrategy import BaseStrategy +from dexbot.basestrategy import BaseStrategy class StorageDemo(BaseStrategy): diff --git a/dexbot/strategies/walls.py b/dexbot/strategies/walls.py new file mode 100644 index 000000000..3159d1ed4 --- /dev/null +++ b/dexbot/strategies/walls.py @@ -0,0 +1,122 @@ +from math import fabs +from pprint import pprint +from collections import Counter +from bitshares.amount import Amount +from dexbot.basestrategy import BaseStrategy +from dexbot.errors import InsufficientFundsError +import logging +log = logging.getLogger(__name__) + + +class Walls(BaseStrategy): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Define Callbacks + self.onMarketUpdate += self.test + self.ontick += self.tick + self.onAccount += self.test + + self.error_ontick = self.error + self.error_onMarketUpdate = self.error + self.error_onAccount = self.error + + # Counter for blocks + self.counter = Counter() + + # Tests for actions + self.test_blocks = self.bot.get("test", {}).get("blocks", 0) + + def error(self, *args, **kwargs): + self.disabled = True + self.cancelall() + pprint(self.execute()) + + def updateorders(self): + """ Update the orders + """ + log.info("Replacing orders") + + # Canceling orders + self.cancelall() + + # Target + target = self.bot.get("target", {}) + price = self.getprice() + + # prices + buy_price = price * (1 - target["offsets"]["buy"] / 100) + sell_price = price * (1 + target["offsets"]["sell"] / 100) + + # Store price in storage for later use + self["feed_price"] = float(price) + + # Buy Side + if float(self.balance(self.market["base"])) < buy_price * target["amount"]["buy"]: + InsufficientFundsError(Amount(target["amount"]["buy"] * float(buy_price), self.market["base"])) + self["insufficient_buy"] = True + else: + self["insufficient_buy"] = False + self.market.buy( + buy_price, + Amount(target["amount"]["buy"], self.market["quote"]), + account=self.account + ) + + # Sell Side + if float(self.balance(self.market["quote"])) < target["amount"]["sell"]: + InsufficientFundsError(Amount(target["amount"]["sell"], self.market["quote"])) + self["insufficient_sell"] = True + else: + self["insufficient_sell"] = False + self.market.sell( + sell_price, + Amount(target["amount"]["sell"], self.market["quote"]), + account=self.account + ) + + pprint(self.execute()) + + def getprice(self): + """ Here we obtain the price for the quote and make sure it has + a feed price + """ + target = self.bot.get("target", {}) + if target.get("reference") == "feed": + assert self.market == self.market.core_quote_market(), "Wrong market for 'feed' reference!" + ticker = self.market.ticker() + price = ticker.get("quoteSettlement_price") + assert abs(price["price"]) != float("inf"), "Check price feed of asset! (%s)" % str(price) + return price + + def tick(self, d): + """ ticks come in on every block + """ + if self.test_blocks: + if not (self.counter["blocks"] or 0) % self.test_blocks: + self.test() + self.counter["blocks"] += 1 + + def test(self, *args, **kwargs): + """ Tests if the orders need updating + """ + orders = self.orders + + # Test if still 2 orders in the market (the walls) + if len(orders) < 2 and len(orders) > 0: + if ( + not self["insufficient_buy"] and + not self["insufficient_sell"] + ): + log.info("No 2 orders available. Updating orders!") + self.updateorders() + elif len(orders) == 0: + self.updateorders() + + # 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 + ): + log.info("Price feed moved by more than the threshold. Updating orders!") + self.updateorders() diff --git a/stakemachine/ui.py b/dexbot/ui.py similarity index 100% rename from stakemachine/ui.py rename to dexbot/ui.py diff --git a/setup.py b/setup.py index 26f72e980..0b3129d0b 100755 --- a/setup.py +++ b/setup.py @@ -5,20 +5,19 @@ VERSION = '0.0.6' setup( - name='stakemachine', + name='dexbot', version=VERSION, - description='Trading bot infrastructure for the DEX (BitShares)', + description='Trading bot for the DEX (BitShares)', long_description=open('README.md').read(), - download_url='https://github.com/xeroc/stakemachine/tarball/' + VERSION, - author='Fabian Schuh', - author_email='Fabian@chainsquad.com', - maintainer='Fabian Schuh', - maintainer_email='Fabian@chainsquad.com', - url='http://www.github.com/xeroc/stakemachine', - keywords=['stake', 'bot', 'trading', 'api', 'blog', 'blockchain'], + author='Codaone Oy', + author_email='support@codaone.com', + maintainer='Codaone Oy', + maintainer_email='support@codaone.com', + url='http://www.github.com/codaone/dexbot', + keywords=['bot', 'trading', 'api', 'blockchain'], packages=[ - "stakemachine", - "stakemachine.strategies", + "dexbot", + "dexbot.strategies", ], classifiers=[ 'License :: OSI Approved :: MIT License', @@ -29,7 +28,7 @@ ], entry_points={ 'console_scripts': [ - 'stakemachine = stakemachine.cli:main', + 'dexbot = dexbot.cli:main', ], }, install_requires=[ diff --git a/stakemachine/stakemachine.sqlite b/stakemachine/stakemachine.sqlite deleted file mode 100644 index 258c615577775be96cefea5dba0433461ff62870..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8192 zcmeI#O-jQ+90l;1v?2+}1WJ+a{&bas2+|7}%b>*=b&NunX*(&w+CUtIEPKA5p=a<4 zPAGxwx)pqn`Fzakx0{#eC^wq2qWnk=jhW9lXV*lGv96ttolUqs)LoPMek@&f=>})^ z6&D+32Lb^IKmY;|fB*y_009U<00I#B2LfBcPtOMfzO76;U+YD&$@0}&YH}BfjUzjMO+{hraE XCM$~ZJS}~v-CWs5aNG2nH@5f%Lli*k From d4d019527a85a560bfb56c3caae152f40ed89c7b Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 1 Jan 2018 20:24:06 +0200 Subject: [PATCH 0002/1846] Add base for the manual order strategy --- dexbot/strategies/manualorders.py | 10 +++ stakemachine/strategies/walls.py | 122 ------------------------------ 2 files changed, 10 insertions(+), 122 deletions(-) create mode 100644 dexbot/strategies/manualorders.py delete mode 100644 stakemachine/strategies/walls.py diff --git a/dexbot/strategies/manualorders.py b/dexbot/strategies/manualorders.py new file mode 100644 index 000000000..261b55827 --- /dev/null +++ b/dexbot/strategies/manualorders.py @@ -0,0 +1,10 @@ +from dexbot.basestrategy import BaseStrategy +import logging +log = logging.getLogger(__name__) + + +class ManualOrders(BaseStrategy): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # TODO: do the strategy diff --git a/stakemachine/strategies/walls.py b/stakemachine/strategies/walls.py deleted file mode 100644 index dc32840ab..000000000 --- a/stakemachine/strategies/walls.py +++ /dev/null @@ -1,122 +0,0 @@ -from math import fabs -from pprint import pprint -from collections import Counter -from bitshares.amount import Amount -from stakemachine.basestrategy import BaseStrategy -from stakemachine.errors import InsufficientFundsError -import logging -log = logging.getLogger(__name__) - - -class Walls(BaseStrategy): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Define Callbacks - self.onMarketUpdate += self.test - self.ontick += self.tick - self.onAccount += self.test - - self.error_ontick = self.error - self.error_onMarketUpdate = self.error - self.error_onAccount = self.error - - # Counter for blocks - self.counter = Counter() - - # Tests for actions - self.test_blocks = self.bot.get("test", {}).get("blocks", 0) - - def error(self, *args, **kwargs): - self.disabled = True - self.cancelall() - pprint(self.execute()) - - def updateorders(self): - """ Update the orders - """ - log.info("Replacing orders") - - # Canceling orders - self.cancelall() - - # Target - target = self.bot.get("target", {}) - price = self.getprice() - - # prices - buy_price = price * (1 - target["offsets"]["buy"] / 100) - sell_price = price * (1 + target["offsets"]["sell"] / 100) - - # Store price in storage for later use - self["feed_price"] = float(price) - - # Buy Side - if float(self.balance(self.market["base"])) < buy_price * target["amount"]["buy"]: - InsufficientFundsError(Amount(target["amount"]["buy"] * float(buy_price), self.market["base"])) - self["insufficient_buy"] = True - else: - self["insufficient_buy"] = False - self.market.buy( - buy_price, - Amount(target["amount"]["buy"], self.market["quote"]), - account=self.account - ) - - # Sell Side - if float(self.balance(self.market["quote"])) < target["amount"]["sell"]: - InsufficientFundsError(Amount(target["amount"]["sell"], self.market["quote"])) - self["insufficient_sell"] = True - else: - self["insufficient_sell"] = False - self.market.sell( - sell_price, - Amount(target["amount"]["sell"], self.market["quote"]), - account=self.account - ) - - pprint(self.execute()) - - def getprice(self): - """ Here we obtain the price for the quote and make sure it has - a feed price - """ - target = self.bot.get("target", {}) - if target.get("reference") == "feed": - assert self.market == self.market.core_quote_market(), "Wrong market for 'feed' reference!" - ticker = self.market.ticker() - price = ticker.get("quoteSettlement_price") - assert abs(price["price"]) != float("inf"), "Check price feed of asset! (%s)" % str(price) - return price - - def tick(self, d): - """ ticks come in on every block - """ - if self.test_blocks: - if not (self.counter["blocks"] or 0) % self.test_blocks: - self.test() - self.counter["blocks"] += 1 - - def test(self, *args, **kwargs): - """ Tests if the orders need updating - """ - orders = self.orders - - # Test if still 2 orders in the market (the walls) - if len(orders) < 2 and len(orders) > 0: - if ( - not self["insufficient_buy"] and - not self["insufficient_sell"] - ): - log.info("No 2 orders available. Updating orders!") - self.updateorders() - elif len(orders) == 0: - self.updateorders() - - # 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 - ): - log.info("Price feed moved by more than the threshold. Updating orders!") - self.updateorders() From 55345e3da8d8f9ad2835e2ee6e77cb5c5db661c3 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 10 Jan 2018 13:33:11 +0200 Subject: [PATCH 0003/1846] Add base for GUI --- app.py | 20 ++ dexbot/controllers/__init__.py | 0 dexbot/controllers/main_controller.py | 14 + dexbot/img/pause.png | Bin 0 -> 1886 bytes dexbot/img/play.png | Bin 0 -> 2216 bytes dexbot/model/__init__.py | 0 dexbot/model/model.py | 7 + dexbot/views/__init__.py | 0 dexbot/views/bot_list.py | 61 +++++ dexbot/views/create_bot.py | 22 ++ dexbot/views/gen/__init__.py | 0 dexbot/views/gen/bot_item_widget.py | 190 +++++++++++++ dexbot/views/gen/bot_list_window.py | 95 +++++++ dexbot/views/gen/create_bot_window.py | 72 +++++ dexbot/views/orig/bot_item_widget.ui | 353 +++++++++++++++++++++++++ dexbot/views/orig/bot_list_window.ui | 156 +++++++++++ dexbot/views/orig/create_bot_window.ui | 99 +++++++ setup.py | 5 +- 18 files changed, 1092 insertions(+), 2 deletions(-) create mode 100644 app.py create mode 100644 dexbot/controllers/__init__.py create mode 100644 dexbot/controllers/main_controller.py create mode 100644 dexbot/img/pause.png create mode 100644 dexbot/img/play.png create mode 100644 dexbot/model/__init__.py create mode 100644 dexbot/model/model.py create mode 100644 dexbot/views/__init__.py create mode 100644 dexbot/views/bot_list.py create mode 100644 dexbot/views/create_bot.py create mode 100644 dexbot/views/gen/__init__.py create mode 100644 dexbot/views/gen/bot_item_widget.py create mode 100644 dexbot/views/gen/bot_list_window.py create mode 100644 dexbot/views/gen/create_bot_window.py create mode 100644 dexbot/views/orig/bot_item_widget.ui create mode 100644 dexbot/views/orig/bot_list_window.ui create mode 100644 dexbot/views/orig/create_bot_window.ui diff --git a/app.py b/app.py new file mode 100644 index 000000000..aea053925 --- /dev/null +++ b/app.py @@ -0,0 +1,20 @@ +import sys + +from PyQt5 import Qt + +from dexbot.views.bot_list import MainView +from dexbot.controllers.main_controller import MainController +from dexbot.model.model import Model + + +class App(Qt.QApplication): + def __init__(self, sys_argv): + super(App, self).__init__(sys_argv) + self.model = Model() + self.main_ctrl = MainController(self.model) + self.main_view = MainView(self.model, self.main_ctrl) + self.main_view.show() + +if __name__ == '__main__': + app = App(sys.argv) + sys.exit(app.exec_()) \ No newline at end of file diff --git a/dexbot/controllers/__init__.py b/dexbot/controllers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py new file mode 100644 index 000000000..4decdb977 --- /dev/null +++ b/dexbot/controllers/main_controller.py @@ -0,0 +1,14 @@ +from PyQt5 import QtWidgets +import yaml + +class MainController(object): + + def __init__(self, model): + pass + + def get_bots_data(self): + """ + Returns dict of all the bots data + """ + with open('config.yml', 'r') as f: + return yaml.load(f)['bots'] diff --git a/dexbot/img/pause.png b/dexbot/img/pause.png new file mode 100644 index 0000000000000000000000000000000000000000..f2614606ea1f05e48f865fb8d61b478d62b4f25e GIT binary patch literal 1886 zcmV-k2ch_hP)%TTc^B9LAsB?TN+hfdZuw6eSI{qJRXAO;ijLydnAxOuX8;D2hV2x3>|C#jv%tg>*WNY&MHb zCR6&WC<-hV3wWMKLqh|2o(IqK5Cj1(mkX_}t*}@unwgtA%x1ZjIFIL%*@Q-+qZ9!BuR~| z%V=$F#ofDi;q&=mu~<|w^2!e2; zdpOa>o12?-a&i*ka2V-yx~x;v8l%w&r_+gh_wM1^wQDdMjmNr|V_hUk5?x+i#>B(~ z5{bldvQQ0%VbI^-kI~UlSgqD0ca}#sKomv#@#9C#&COwVclW55YsRHZmoPLm1dqpa zq`u)u2CT2I)A8|ftgNi)THR}f=Xs2bjNsO-TQ!?dQ=)u6Pghn}5C{b7UgfL7FbrZ4Hx?lYiCC z13)ksL~Cnn)pYEiz7$1~zIyfQ)K~k1pp;^Eb{3yMf2N9}9NJ+WS_5`=c4#0F!0+F` zwNa=8gb<96kHcg#fe=Cu>JJXA0g@!qnVA_ZEiGxUQez}Z!iNtZu(7dm;P(d_5Rb<( zKR>UjFg@AW*ud=UEC?YLO`*a7MN#Pc_wSL*<@8af6JNi6MI;ibIA38vC=^0C9M(mY zo=B2}H*elxe}`CRKr)%6v$M1MD$|j*wKXg*E|xWgk^zJe8Vm;i>R!JZilShCejce* zs&uYofGo>cSXj_xBI7ji2zI0=}jmU zD*bN&3hn>Jix-*|==b|^;lhQobMM~0Lo%5x9a}6G`2GH}@w+U`n4X@-_V%`_dRA9g z5sSsB+wEoy03eDYR#sM2Eo3j9ot?OT{d(ECsi~C-3G z4FG^IU%mkNzX|;O`4ftws9wlvCKwDNm&;KWz?ta$_LI$Kkw_%K5<=+S=i*s`5Q1zr z3s#opzizTa4G1B~WHMlLxtuPG8K(_JQ3^F6m&^TgZ+^8}L$g+Iq z9)U$9Ndjv&n_)B>>#aq17=|gt9hgj}GidMA!ghCekxHe?K7^36G?Y?gv)QsV>2$gxof?=-CS1LG z6~LbOfM_&IpFe-DdSL+2)YOFi9qsDsDr8wM9kVQprlzK{ClaL;>+9>P57oQ7yYckt zQwG?xI^5RQ2B*`B&CN|!3j=_yt*xqQ2qA|qV5&{=*9Z2@3L6Xtcs!m`obJDh-EK!m zM@Q+nWB|*u=8XFt6SEmC#Jv|jGQ$-Gp65}|IQ51zHlS#aO z{aRCTT4S|ZF*Y_1ATWo8%>FS@5&%X< zM&R@L)K{l5f*@dQYz!?eEq@%@RW+cou@R%AqYwl^J#{*8`}S>kz22(nY99BdQYjh; z1hBlke5%(GoK7c(hlepRFrZ$ze35iIP3Pw3Ff}!$$!qFKX>V`GqeqX>-ripG&XHri z`7A6f(3dY?o~i|v&dyFedGZ82&uhD7rbsv(rqk2Yh(scFHNtMUqpz_Czv2!a5c%?7L03cKA7hr@B?-u$@y Y2P-k~C$h(-f&c&j07*qoM6N<$f((LoX#fBK literal 0 HcmV?d00001 diff --git a/dexbot/img/play.png b/dexbot/img/play.png new file mode 100644 index 0000000000000000000000000000000000000000..42756bb7e3c9d3a63e0e825e95125d8e20155ade GIT binary patch literal 2216 zcmV;Z2v_%sP)eN%Ck^$rTuB z^<*vXtR%!Z<8?|S*0%3OgK`-@tp^M&8%g)WPHh-tEyAwihTq*IcRm>e^PH3MY1xYqshBjcNuL157_V2F_)nU)-ou#(0FFb!Q`q491W`Sf z^hro+K$xlct#2Qi)HY+|Gl{L8hft?9COwzbfRjoCJ`U6Y7_bhxCuq~UvB9@L>A71B z2x$?%w)K7J26ScylLMhM*w=IBmOFZj0dIF7$Cr`I=0s&OzqV)iAwOct21F#u%pn@cDH&%)l+=;KPxtv8U{1EObtWG)P9& z_Fh1ksSp9+VywedaVrt=xN|(F%A>I_cm~h6?LpM!A+{KcpiS!mWsLEbnBP?K@9>DZ zsCLgpd*w!~E_f9Ag3~nZ7}wFR^?)ioh6qzk6HbOxu;ca8W%#&!4W4n&gq_NUYsR^b z4lM*4W>F~2ybl_;!%9R<&l!U)r9a0%%U(dGG|EstjqB*r70_;7K~Nt$gB}h+!sCwd zs2}?ZUMYSOUW*6RfN!`e*U_v6K?9nC5Q}DAAXDtriuJ{du&;bAR(j?F)ShJp22~G( zQbItC{YsuTln^{*ABn}T$;PA)0#PCs*obI|Kqly%;av18w)qd^uidqP0rixHS|N&r zLL>u@j+;tLybFKs`U3BD)#AS~W4h-A6e6`jbWl5FYBNM69gg>2!2B!Qpv1cjnPc!B zBB97562u~l10K;~m{2NcT*sHuW^D4+;Df*yrf5pUf(ok#qC~Ca+2);tHvD@*ZNo-i z4UUA*;);n)avKrhwdVKr1;*KinI?QAJGyGIuJs@AYZ=ZXN&_3UV1&ifXTUUhv}wXo z7RT}4i+HW`AU+MAHcj_tywXVIQG1^O6YZm57xEC%1BQxTQrq#%w%s@x`U(L(WT@6a zROeNJQrh?avkO+T%r(g{F^x0)q2nONU;H%=1WpWk<^hW3uBz+5uJdx?JXj4Hobc-* zyxCcU$Pnms0l*U16y%5!0d6p$!diqWa>d|9Wfnxl)A`dAy9opA#Dc|nQ$dGdvpAbP zB9Fmjds$*XVF0C+EOk!A{r2*~h|3g22w0jw9V0C6#Ib&z0I$W3=RC7b56moNYXP2i zRrQ-t+0OtLGjQ^~yOOkl2a0-kV< zzh&SfX+L;I&V5*tHx&jiCom1Kr2wxMFHU+cX_YSvHuA07!5gAiaZG9V@Z{b7iF>>CqG}a*p-w z|3Yk#zapi=?A-gYaoAHBEfo(~i>Gv3U7}2Mrt^pFqfp)a7FvejT7flp54Euc}9S&Oh~*m5j#O@NKwxy;{8S~c5fTM-HCQC=mN>LM)1)9z{2cf + + widget + + + true + + + + 0 + 0 + 500 + 161 + + + + + 0 + 0 + + + + widget + + + + + 0 + 0 + 500 + 161 + + + + + 0 + 0 + + + + + 491 + 161 + + + + + 16777215 + 16777215 + + + + Qt::LeftToRight + + + false + + + .QFrame { border: 1px solid #005B78; border-radius: 4px; } +* { background-color: white; } + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + QLayout::SetDefaultConstraint + + + 10 + + + 10 + + + + + + 0 + 0 + + + + PointingHandCursor + + + + + + border: 0; + + + + + + + dexbot/img/play.pngdexbot/img/play.png + + + + 30 + 30 + + + + + + + + + 0 + 0 + + + + PointingHandCursor + + + border: 0; + + + + + + + + dexbot/img/pause.pngdexbot/img/pause.png + + + + 30 + 30 + + + + + + + + + 0 + 0 + + + + + + + + 15 + + + 15 + + + + + + 0 + 0 + + + + QSlider::groove:horizontal { +height: 2px; +background: #005B78; +} +QSlider::handle:horizontal { +background: #005B78; +width: 18px; +margin: -5px 0; +} +QSlider { +border-left: 2px solid #005B78; +border-right: 2px solid #005B78; +} + + + 100 + + + 50 + + + false + + + Qt::Horizontal + + + QSlider::TicksAbove + + + + + + + + + + PointingHandCursor + + + Remove + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + 0 + 0 + + + + + 0 + + + 11 + + + + + + 12 + 75 + true + + + + color: #005B78; + + + BOTNAME + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + + 75 + true + + + + color: #005B78; + + + BTS/USD + + + Qt::AlignBottom|Qt::AlignHCenter + + + + + + + + 12 + 75 + true + + + + color: #00D05A; + + + +1.234% + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + + + + + + 16777215 + 16777215 + + + + + 9 + 75 + true + + + + false + + + color: #005B78; + + + SIMPLE STRATEGY + + + Qt::AlignBottom|Qt::AlignRight|Qt::AlignTrailing + + + + + + + PointingHandCursor + + + Edit + + + + + + + + + + + + diff --git a/dexbot/views/orig/bot_list_window.ui b/dexbot/views/orig/bot_list_window.ui new file mode 100644 index 000000000..142133653 --- /dev/null +++ b/dexbot/views/orig/bot_list_window.ui @@ -0,0 +1,156 @@ + + + MainWindow + + + + 0 + 0 + 814 + 513 + + + + ArrowCursor + + + DEXBot + + + background-color: #EDEDED + + + + false + + + + + + + + + + 1 + 1 + + + + false + + + QFrame::NoFrame + + + Qt::ScrollBarAsNeeded + + + QAbstractScrollArea::AdjustToContents + + + true + + + Qt::AlignHCenter|Qt::AlignTop + + + + + 389 + 0 + 18 + 18 + + + + + 0 + 0 + + + + -7 + + + + + + + + + true + + + + 0 + 0 + + + + + 0 + 100 + + + + + + + + + DEXBot + + + + + + + + + + + PointingHandCursor + + + -1 + + + Add bot + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Qt::Horizontal + + + + + + scrollArea + widget + line + + + + + diff --git a/dexbot/views/orig/create_bot_window.ui b/dexbot/views/orig/create_bot_window.ui new file mode 100644 index 000000000..abfb11fe3 --- /dev/null +++ b/dexbot/views/orig/create_bot_window.ui @@ -0,0 +1,99 @@ + + + Dialog + + + + 0 + 0 + 400 + 300 + + + + Create Bot + + + true + + + + + + + 0 + 0 + + + + + + + Qt::Horizontal + + + + 179 + 20 + + + + + + + + + 0 + 0 + + + + Cancel + + + + + + + + 0 + 0 + + + + Save + + + + + + + + + + + + Strategy + + + + + + + + + + Account + + + + + + + + + + + + + diff --git a/setup.py b/setup.py index 0b3129d0b..ecf51e9d5 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ maintainer='Codaone Oy', maintainer_email='support@codaone.com', url='http://www.github.com/codaone/dexbot', - keywords=['bot', 'trading', 'api', 'blockchain'], + keywords=['DEX', 'bot', 'trading', 'api', 'blockchain'], packages=[ "dexbot", "dexbot.strategies", @@ -41,7 +41,8 @@ "tqdm", "pyyaml", "sqlalchemy", - "appdirs" + "appdirs", + "pyqt5" ], include_package_data=True, ) From 3d1108e0bc5c9761a04050777a06ae024ddf576d Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 11 Jan 2018 13:20:29 +1100 Subject: [PATCH 0004/1846] base code for config system: bots have a classmethod whivh reports their config values to different config systems --- dexbot/basestrategy.py | 36 +++++++++++++++++++++++++++++++++++- dexbot/bot.py | 4 ++++ dexbot/strategies/walls.py | 14 +++++++++++++- 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index d791a978d..cccb4bafd 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -1,4 +1,4 @@ -import logging +import logging, collections from events import Events from bitshares.market import Market from bitshares.account import Account @@ -9,6 +9,23 @@ log = logging.getLogger(__name__) +ConfigElement = collections.namedtuple('ConfigElement','key type default description extra') +# bots need to specify their own configuration values +# I want this to be UI-agnostic so a future web or GUI interface can use it too +# so each bot can have a class method 'configure' which returns a list of ConfigElement +# named tuples. tuple fields as follows. +# key: the key in the bot config dictionary that gets saved back to config.yml +# type: one of "int", "float", "bool", "string", "choice" +# default: the default value. must be right type. +# description: comments to user, full sentences encouraged +# extra: +# for int & float: a (min, max) tuple +# for string: a regular expression, entries must match it, can be None which equivalent to .* +# for bool, ignored +# for choice: a list of choices, choices are in turn (tag, label) tuples. labels get presented to user, and tag is used +# as the value saved back to the config dict + + class BaseStrategy(Storage, StateMachine, Events): """ Base Strategy and methods available in all Sub Classes that inherit this BaseStrategy. @@ -51,6 +68,23 @@ class BaseStrategy(Storage, StateMachine, Events): 'onUpdateCallOrder', ] + @classmethod + def configure(kls): + """ + Return a list of ConfigElement objects defining the configuration values for + this class + User interfaces should then generate widgets based on this values, gather + data and save back to the config dictionary for the bot. + + NOTE: when overriding you almost certainly will want to call the ancestor + and then add your config values to the list. + """ + # these configs are common to all bots + return [ + ConfigElement("account","string","","BitShares account name for the bot to operate with",""), + ConfigElement("market","string","USD:BTS","BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"","[A-Z]+:[A-Z]+") + ] + def __init__( self, config, diff --git a/dexbot/bot.py b/dexbot/bot.py index 2e0ff79ba..57441abb3 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -6,6 +6,10 @@ from bitshares.instance import shared_bitshares_instance log = logging.getLogger(__name__) +# FIXME: currently static list of bot strategies: ? how to enumerate bots available and deploy new bot strategies. +STRATEGIES={'Echo':('dexbot.strategies.echo','Echo'), + 'Liquidity Walls':('dexbot.strategies.walls','Walls')} + class BotInfrastructure(): diff --git a/dexbot/strategies/walls.py b/dexbot/strategies/walls.py index 3159d1ed4..ac8e2b8d2 100644 --- a/dexbot/strategies/walls.py +++ b/dexbot/strategies/walls.py @@ -2,13 +2,25 @@ from pprint import pprint from collections import Counter from bitshares.amount import Amount -from dexbot.basestrategy import BaseStrategy +from dexbot.basestrategy import BaseStrategy, ConfigElement from dexbot.errors import InsufficientFundsError import logging log = logging.getLogger(__name__) class Walls(BaseStrategy): + + @classmethod + def configure(cls): + return BaseStrategy.configure()+[ + ConfigElement("spread","int",5,"the spread between sell and buy as percentage",(0,100)), + ConfigElement("threshold","int",5,"percentage the feed has to move before we change orders",(0,100)), + ConfigElement("buy","float",0.0,"the default amount to buy",(0.0,None)), + ConfigElement("sell","float",0.0,"the default amount to sell",(0.0,None)), + ConfigElement("blocks","int",20,"number of blocks to wait before re-calculating",(0,10000)), + ConfigElement("dry_run","bool",False,"Dry Run Mode\nIf Yes the bot won't buy or sell anything, just log what it would do.\nIf No, the bot will buy and sell for real.",None) + ] + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) From f8046942aba7c3af1f71e906c1ad84217bac5939 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 11 Jan 2018 13:28:24 +1100 Subject: [PATCH 0005/1846] the CLI configurator with systemd support this will walk the user through the bot's config questions will cope with no systemd available: won't ask the user about this --- dexbot/cli.py | 52 ++++++++++++- dexbot/cli_conf.py | 177 +++++++++++++++++++++++++++++++++++++++++++++ dexbot/ui.py | 11 ++- 3 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 dexbot/cli_conf.py diff --git a/dexbot/cli.py b/dexbot/cli.py index c280f16e5..0d4069656 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -2,6 +2,8 @@ import yaml import logging import click +import os.path, os + from .ui import ( verbose, chain, @@ -12,9 +14,15 @@ warning, alert, ) + + from dexbot.bot import BotInfrastructure +from dexbot.cli_conf import configure_dexbot, QuitException + + log = logging.getLogger(__name__) +# inital logging before proper setup. logging.basicConfig( level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s' @@ -32,6 +40,11 @@ type=int, default=3, help='Verbosity (0-15)') +@click.option( + '--systemd/--no-systemd', + '-d', + default=False, + help='Run as a daemon from systemd') @click.pass_context def main(ctx, **kwargs): ctx.obj = {} @@ -49,8 +62,45 @@ def run(ctx): """ Continuously run the bot """ bot = BotInfrastructure(ctx.config) + if ctx.obj['systemd']: + try: + import sdnotify # a soft dependency on sdnotify -- don't crash on non-systemd systems + n = sdnotify.SystemdNotifier() + n.notify("READY=1") + except: + warning("sdnotify not available") bot.run() - +@main.command() +@click.pass_context +@verbose +def configure(ctx): + """ Interactively configure dexbot + """ + if os.path.exists(ctx.obj['configfile']): + with open(ctx.obj["configfile"]) as fd: + config = yaml.load(fd) + else: + config = {} + try: + configure_dexbot(config) + click.clear() + cfg_file = ctx.obj["configfile"] + if not "/" in cfg_file: # use hoke directory by default. + cfg_file = os.path.expanduser("~/"+cfg_file) + with open(cfg_file,"w") as fd: + yaml.dump(config,fd,default_flow_style=False) + click.echo("new configuration saved") + if config['systemd_status'] == 'installed': + # we are already installed + click.echo("restarting dexbot daemon") + os.system("systemctl --user restart dexbot") + if config['systemd_status'] == 'install': + os.system("systemctl --user enable dexbot") + click.echo("starting dexbot daemon") + os.system("systemctl --user start dexbot") + except QuitException: + click.echo("configuration exited: nothing changed") + if __name__ == '__main__': main() diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py new file mode 100644 index 000000000..525e1df9e --- /dev/null +++ b/dexbot/cli_conf.py @@ -0,0 +1,177 @@ +""" +A module to provide an interactive text-based tool for dexbot configuration +The result is takemachine can be run without having to hand-edit config files. +If systemd is detected it will offer to install a user service unit (under ~/.local/share/systemd +This requires a per-user systemd process to be runnng + +Requires the 'dialog' tool: so UNIX-like sytems only + +Note there is some common cross-UI configuration stuff: look in basestrategy.py +It's expected GUI/web interfaces will be re-implementing code in this file, but they should +understand the common code so bot strategy writers can define their configuration once +for each strategy class. + +""" + + +import dialog, importlib, os, os.path, sys, collections, re + +from dexbot.bot import STRATEGIES + + +NODES=[("wss://openledger.hk/ws", "OpenLedger"), + ("wss://dexnode.net/ws", "DEXNode"), + ("wss://node.bitshares.eu/ws", "BitShares.EU")] + + +SYSTEMD_SERVICE_NAME=os.path.expanduser("~/.local/share/systemd/user/dexbot.service") + +SYSTEMD_SERVICE_FILE=""" +[Unit] +Description=Dexbot + +[Service] +Type=notify +WorkingDirectory={homedir} +ExecStart={exe} --systemd run +Environment=PYTHONUNBUFFERED=true +Environment=UNLOCK={passwd} + +[Install] +WantedBy=default.target +""" + +class QuitException(Exception): pass + +def select_choice(current,choices): + """for the radiolist, get us a list with the current value selected""" + return [(tag,text,current == tag) for tag,text in choices] + + +def process_config_element(elem,d,config): + """ + process an item of configuration metadata display a widget as approrpriate + d: the Dialog object + config: the config dctionary for this bot + """ + if elem.type == "string": + code, txt = d.inputbox(elem.description,init=config.get(elem.key,elem.default)) + if code != d.OK: raise QuitException() + if elem.extra: + while not re.match(elem.extra,txt): + d.msgbox("The value is not valid") + code, txt = d.inputbox(elem.description,init=config.get(elem.key,elem.default)) + if code != d.OK: raise QuitException() + config[elem.key] = txt + if elem.type == "int": + code, val = d.rangebox(elem.description,init=config.get(elem.key,elem.default),min=elem.extra[0],max=elem.extra[1]) + if code != d.OK: raise QuitException() + config[elem.key] = val + if elem.type == "bool": + code = d.yesno(elem.description) + config[elem.key] = (code == d.OK) + if elem.type == "float": + code, txt = d.inputbox(elem.description,init=config.get(elem.key,str(elem.default))) + if code != d.OK: raise QuitException() + while True: + try: + val = float(txt) + if val < elem.extra[0]: + d.msgbox("The value is too low") + elif elem.extra[1] and val > elem.extra[1]: + d.msgbox("the value is too high") + else: + break + except ValueError: + d.msgbox("Not a valid value") + code, txt = d.inputbox(elem.description,init=config.get(elem.key,str(elem.default))) + if code != d.OK: raise QuitException() + config[elem.key] = val + if elem.type == "choice": + code, tag = d.radiolist(elem.description,choices=select_choice(config.get(elem.key,elem.default),elem.extra)) + if code != d.OK: raise QuitException() + config[elem.key] = tag + +def setup_systemd(d,config): + if config.get("systemd_status","install") == "reject": + return # don't nag user if previously said no + if not os.path.exists("/etc/systemd"): + return # no working systemd + if os.path.exists(SYSTEMD_SERVICE_NAME): + # dexbot already installed + # so just tell cli.py to quietly restart the daemon + config["systemd_status"] = "installed" + return + if d.yesno("Do you want to install dexbot as a background (daemon) process?") == d.OK: + for i in ["~/.local","~/.local/share","~/.local/share/systemd","~/.local/share/systemd/user"]: + j = os.path.expanduser(i) + if not os.path.exists(j): + os.mkdir(j) + code, passwd = d.passwordbox("The wallet password entered with uptick\nNOTE: this will be saved on disc so the bot can run unattended. This means anyone with access to this computer's file can spend all your money",insecure=True) + if code != d.OK: raise QuitException() + fd = os.open(SYSTEMD_SERVICE_NAME, os.O_WRONLY|os.O_CREAT, 0o600) # because we hold password be restrictive + with open(fd, "w") as fp: + fp.write(SYSTEMD_SERVICE_FILE.format(exe=sys.argv[0],passwd=passwd,homedir=os.path.expanduser("~"))) + config['systemd_status'] = 'install' # signal cli.py to set the unit up after writing config file + else: + config['systemd_status'] = 'reject' + + +def configure_bot(d,bot): + if 'module' in bot: + inv_map = {v:k for k,v in STRATEGIES.items()} + strategy = inv_map[(bot['module'],bot['bot'])] + else: + strategy = 'Echo' + code, tag = d.radiolist("Choose a bot strategy", + choices=select_choice(strategy,[(i,i) for i in STRATEGIES])) + if code != d.OK: raise QuitException() + bot['module'], bot['bot'] = STRATEGIES[tag] + # import the bot class but we don't __init__ it here + klass = getattr( + importlib.import_module(bot["module"]), + bot["bot"] + ) + # use class metadata for per-bot configuration + configs = klass.configure() + if configs: + for c in configs: + process_config_element(c,d,bot) + else: + d.msgbox("This bot type does not have configuration information. You will have to check the bot code and add configuration values to config.yml if required") + return bot + + + +def configure_dexbot(config): + d = dialog.Dialog(dialog="dialog",autowidgetsize=True) + d.set_background_title("dexbot configuration") + tag = "" + while not tag: + code, tag = d.radiolist("Choose a Witness node to use", + choices=select_choice(config.get("node"),NODES)) + if code != d.OK: raise QuitException() + if not tag: d.msgbox("You need to choose a node") + config['node'] = tag + bots = config.get('bots',{}) + if len(bots) == 0: + code, txt = d.inputbox("Your name for the bot") + if code != d.OK: raise QuitException() + config['bots'] = {txt:configure_bot(d,{})} + else: + code, botname = d.menu("Select bot to edit", + choices=[(i,i) for i in bots]+[('NEW','New bot')]) + if code != d.OK: raise QuitException() + if botname == 'NEW': + code, txt = d.inputbox("Your name for the bot") + if code != d.OK: raise QuitException() + config['bots'][txt] = configure_bot(d,{}) + else: + config['bots'][botname] = configure_bot(d,config['bots'][botname]) + setup_systemd(d,config) + return config + +if __name__=='__main__': + print(repr(configure({}))) + + diff --git a/dexbot/ui.py b/dexbot/ui.py index ed845b951..913d425f9 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -1,4 +1,4 @@ -import os +import os, sys import click import logging import yaml @@ -62,13 +62,22 @@ def unlock(f): @click.pass_context def new_func(ctx, *args, **kwargs): if not ctx.obj.get("unsigned", False): + systemd = ctx.obj.get('systemd',False) if ctx.bitshares.wallet.created(): if "UNLOCK" in os.environ: pwd = os.environ["UNLOCK"] else: + if systemd: + # no user available to interact with + log.critical("Passphrase not available, exiting") + sys.exit(78) # 'configuation error' in systexits.h pwd = click.prompt("Current Wallet Passphrase", hide_input=True) ctx.bitshares.wallet.unlock(pwd) else: + if systemd: + # no user available to interact with + log.critical("Wallet not installed, cannot run") + sys.exit(78) click.echo("No wallet installed yet. Creating ...") pwd = click.prompt("Wallet Encryption Passphrase", hide_input=True, confirmation_prompt=True) ctx.bitshares.wallet.create(pwd) From e5604f2a25e5ec4569d472e95342205e8843777b Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 11 Jan 2018 13:31:53 +1100 Subject: [PATCH 0006/1846] documentation chnges that flow from the CLI configurator --- docs/configuration.rst | 83 ++++++++++++++++++++++++++++-------------- docs/index.rst | 6 +-- docs/manual.rst | 54 +++++++++++++++++++++++++++ docs/setup.rst | 50 +++++++++++++++---------- 4 files changed, 142 insertions(+), 51 deletions(-) create mode 100644 docs/manual.rst diff --git a/docs/configuration.rst b/docs/configuration.rst index 22b9bc9a3..388d5e413 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -1,40 +1,67 @@ -************* -Configuration -************* +Configuration Questions +======================= -The configuration of ``stakemachine`` happens through a YAML formated -file and takes the following form: +The configuration consists of a series of questions about the bots you wish to configure. -.. code-block:: yaml +1. the Node. - # The BitShares endpoint to talk to - node: "wss://node.testnet.bitshares.eu" + You need to set the public node that gives access to the BitShares blockchain. Pick the one closest to you. - # List of bots - bots: + (Please submit a `Github bug report `_ if you think items need to added or removed from this list). - # Name of the bot. This is mostly for logging and internal - # use to distinguish different bots - NAME_OF_BOT: +2. The Bot Name. + + Choose a unique name for your bot, DEXBot doesn't care what you call it. It is used to identify the bot in the logs so should be fairly short. - # Python module to look for the strategy (can be custom) - module: "stakemachine.strategies.echo" +3. The Bot Strategy + + DEXBot provides a number of different bot strategies. They can be quite different in + how they behave (i.e. spend *your* money) so it is important you understand the strategy + before deploying a bot. - # The bot class in that module to use - bot: Echo + a. :doc:`echo` For testing this just logs events on a market, does no trading. + b. :doc:`wall` a basic liquidity bot maintaining a "wall" of buy and sell orders around the market price. - # The market to subscribe to - market: GOLD:TEST + Technically the questions that follow are determined by the strategy chosen, and each strategy will have its own questions around + amounts to trade, spreads etc. See the strategy documentations linked above. But two questions are nearly universal among strategies + so are documented here. - # The account to use for this bot - account: xeroc +3. The Account. - # Custom bot configuration - foo: bar + This is the same account name as the one where you entered the keys into ``uptick`` earlier on: the bot must + always have the private key so it can execute trades. -Usig the configuration in custom strategies -------------------------------------------- +4. The Market. + + This is the main market the bot trade on. They are specified by the quote asset, a colon (:), and the base asset, for example + the market for BitShares priced in US dollars is called BTS:USD. BitShares always provides a "reverse" market so + there will be a USD:BTS with the same trades, the only difference is the prices will be the inverse (1/x) of BTS:USD. -The bot's configuration is available to in each strategy as dictionary -in ``self.bot``. The whole configuration is avaialable in -``self.config``. The name of your bot can be found in ``self.name``. +5. Systemd. + + If the configuration tool detects systemd (the process control system on most modern systems) it will offer to install dexbot + as a background service, this will run continuously in the background whenever you are logged in. if you enabled lingering + as described, it wil run whenever the computer is turned on. + +6. If you select yes above, the final question will be the password you entered to protect the private key with ``uptick``. + Entering it here is a security risk: the configuration tool will save the password to a file on the computer. This + means anyone with access to the computer can access your private key and spend the money in your account. + + There is no alternative to enable 24/7 bot trading without you being physically present to enter the password every time + the bot wnats to execute a trade (which defeats the purpose of using a bot). It does mean you need to think carefully + where dexbot is installed: my advice is on the computer in a secure location that you control behind a properly- + configured firewall/router. + +Manual Running +-------------- + +If you are not using systemd, the bot can be run manually by:: + + dexbot run + +It will ask for your wallet passphrase (that you have provide when +adding your private key to pybitshares using ``uptick addkey``). + +If you want to prevent the password dialog, you can predefine an +environmental variable ``UNLOCK``, if you understand the security +implications. diff --git a/docs/index.rst b/docs/index.rst index 8235cd29b..8c5f34fc1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,5 +1,5 @@ -Welcome to StakeMachine's documentation! -======================================== +Welcome to DEXBot's documentation! +================================== Basics ------ @@ -24,8 +24,8 @@ Developing own Strategies .. toctree:: :maxdepth: 1 + manual basestrategy - configuration storage statemachine events diff --git a/docs/manual.rst b/docs/manual.rst new file mode 100644 index 000000000..c5492f92f --- /dev/null +++ b/docs/manual.rst @@ -0,0 +1,54 @@ +Manual Configuration +==================== + +The configuration of ``dexbot`` internally happens through a YAML formatted +file. Unless you are developing or want to use a custom strategy, you don't +need to edit this. + +The default +file name is ``config.yml``, and ``dexbot`` only seeks the file in the current directory. + +Otherwise you can specify a different +config file using the ``--configfile X`` parameter when calling ``dexbot run``. + +The config.yml file +------------------- + + +.. code-block:: yaml + + # The BitShares endpoint to talk to + node: "wss://node.testnet.bitshares.eu" + + # List of bots + bots: + + # Name of the bot. This is mostly for logging and internal + # use to distinguish different bots + NAME_OF_BOT: + + # Python module to look for the strategy (can be custom) + # dexbot will search in ~/bots as well as standard dirs + module: "dexbot.strategies.echo" + + # The bot class in that module to use + bot: Echo + + # The market to subscribe to + market: GOLD:TEST + + # The account to use for this bot + account: xeroc + + # Custom bot configuration + foo: bar + +Using the configuration in custom strategies +-------------------------------------------- + +The bot's configuration is available to in each strategy as dictionary +in ``self.bot``. The whole configuration is avaialable in +``self.config``. The name of your bot can be found in ``self.name``. + + + diff --git a/docs/setup.rst b/docs/setup.rst index fbe2562db..311ed99cf 100644 --- a/docs/setup.rst +++ b/docs/setup.rst @@ -2,42 +2,52 @@ Setup ***** +Requirements -- Linux +--------------------- + +To run in the background you need systemd and *lingering* enabled:: + + sudo loginctl enable-linger $USER + +On some systems, such as the Raspberry Pi, you need to reboot for this to take effect. + +You need to have python3 installed, including the ``pip`` tool, and the development tools for C extensions. +Plus for the configuration you need the ``dialog`` command. + +On Ubuntu/Debian type systems:: + + sudo apt-get install dialog python3-pip python3-dev + + +On other distros you need to check the documentation for how to install packages, the names should be very similar. + Installation ------------ :: - pip3 install stakemachine [--user] + pip3 install git+https://github.com/ihaywood3/DEXBot.git [--user] If you install using the ``--user`` flag, the binaries of -``stakemachine`` and ``uptick`` are located in ``~/.local/bin``. +``dexbot`` and ``uptick`` are located in ``~/.local/bin``. Otherwise they should be globally reachable. Adding Keys ----------- + It is important to *install* the private key of your -bot's account into the pybitshares wallet. This can be done using -``uptick`` which is installed as a dependency of ``stakemachine``:: +bot's account into a local wallet. This can be done using +``uptick`` which is installed as a dependency of ``dexbot``:: uptick addkey -Configuration -------------- -You will need to create configuration file in YAML format. The default -file name is ``config.yml``, otherwise you can specify a different -config file using the ``--configufile X`` parameter of ``stakemachine``. - -Read more about the :doc:`configuration`. +Easy Configuration +------------------ -Running -------- -The bot can be run by:: +``dexbot`` can be configured using:: - stakemachine run + dexbot configure -It will ask for your wallet passphrase (that you have provide when -adding your private key to pybitshares using ``uptick addkey``). +This will walk you through the configuration process. +Read more about this in the :doc:`configuration`. -If you want to prevent the password dialog, you can predefine an -environmental variable ``UNLOCK``, if you understand the security -implications. From 7757c9899e06861fb959fdeef18d84cb827ce536 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 11 Jan 2018 13:39:46 +1100 Subject: [PATCH 0007/1846] use sed --in-place to really change all references to stakemachine to dexbot --- dexbot/basestrategy.py | 6 +++--- dexbot/storage.py | 6 +++--- docs/Makefile | 8 ++++---- docs/basestrategy.rst | 4 ++-- docs/conf.py | 14 +++++++------- docs/configuration.rst | 4 ++-- docs/echo.rst | 4 ++-- docs/events.rst | 2 +- docs/index.rst | 2 +- docs/requirements.txt | 2 +- docs/setup.rst | 10 +++++----- docs/statemachine.rst | 2 +- docs/storage.rst | 6 +++--- docs/wall.rst | 4 ++-- 14 files changed, 37 insertions(+), 37 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index d791a978d..46f755362 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -15,8 +15,8 @@ class BaseStrategy(Storage, StateMachine, Events): BaseStrategy inherits: - * :class:`stakemachine.storage.Storage` - * :class:`stakemachine.statemachine.StateMachine` + * :class:`dexbot.storage.Storage` + * :class:`dexbot.statemachine.StateMachine` * ``Events`` Available attributes: @@ -30,7 +30,7 @@ class BaseStrategy(Storage, StateMachine, Events): * ``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 - Also, Base Strategy inherits :class:`stakemachine.storage.Storage` + Also, Base Strategy inherits :class:`dexbot.storage.Storage` which allows to permanently store data in a sqlite database using: diff --git a/dexbot/storage.py b/dexbot/storage.py index c9dd74e92..e247d6084 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -7,10 +7,10 @@ from appdirs import user_data_dir Base = declarative_base() -# For stakemachine.sqlite file -appname = "stakemachine" +# For dexbot.sqlite file +appname = "dexbot" appauthor = "ChainSquad GmbH" -storageDatabase = "stakemachine.sqlite" +storageDatabase = "dexbot.sqlite" def mkdir_p(d): diff --git a/docs/Makefile b/docs/Makefile index 66b2f6ba8..7c3bf7cfb 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -87,9 +87,9 @@ qthelp: @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/StakeMachine.qhcp" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DEXBot.qhcp" @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/StakeMachine.qhc" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DEXBot.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @@ -104,8 +104,8 @@ devhelp: @echo @echo "Build finished." @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/StakeMachine" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/StakeMachine" + @echo "# mkdir -p $$HOME/.local/share/devhelp/DEXBot" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DEXBot" @echo "# devhelp" epub: diff --git a/docs/basestrategy.rst b/docs/basestrategy.rst index f4cee0c96..b3cb61578 100644 --- a/docs/basestrategy.rst +++ b/docs/basestrategy.rst @@ -3,11 +3,11 @@ Base Strategy ************* All strategies should inherit -:class:`stakemachine.basestrategy.BaseStrategy` which simplifies and +:class:`dexbot.basestrategy.BaseStrategy` which simplifies and unifies the development of new strategies. API --- -.. autoclass:: stakemachine.basestrategy.BaseStrategy +.. autoclass:: dexbot.basestrategy.BaseStrategy :members: diff --git a/docs/conf.py b/docs/conf.py index 17b6f00d0..2c45b43b5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # -# StakeMachine documentation build configuration file, created by +# DEXBot documentation build configuration file, created by # sphinx-quickstart on Fri May 27 11:21:25 2016. # # This file is execfile()d with the current directory set to its @@ -48,7 +48,7 @@ master_doc = 'index' # General information about the project. -project = 'StakeMachine' +project = 'DEXBot' copyright = '2017, ChainSquad GmbH' author = 'Fabian Schuh' @@ -202,7 +202,7 @@ #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'StakeMachinedoc' +htmlhelp_basename = 'DEXBotdoc' # -- Options for LaTeX output --------------------------------------------- @@ -224,7 +224,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'StakeMachine.tex', 'StakeMachine Documentation', + (master_doc, 'DEXBot.tex', 'DEXBot Documentation', 'Fabian Schuh', 'manual'), ] @@ -254,7 +254,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'stakemachine', 'StakeMachine Documentation', + (master_doc, 'dexbot', 'DEXBot Documentation', [author], 1) ] @@ -268,8 +268,8 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'StakeMachine', 'StakeMachine Documentation', - author, 'StakeMachine', 'One line description of project.', + (master_doc, 'DEXBot', 'DEXBot Documentation', + author, 'DEXBot', 'One line description of project.', 'Miscellaneous'), ] diff --git a/docs/configuration.rst b/docs/configuration.rst index 22b9bc9a3..153ce1b4c 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -2,7 +2,7 @@ Configuration ************* -The configuration of ``stakemachine`` happens through a YAML formated +The configuration of ``dexbot`` happens through a YAML formated file and takes the following form: .. code-block:: yaml @@ -18,7 +18,7 @@ file and takes the following form: NAME_OF_BOT: # Python module to look for the strategy (can be custom) - module: "stakemachine.strategies.echo" + module: "dexbot.strategies.echo" # The bot class in that module to use bot: Echo diff --git a/docs/echo.rst b/docs/echo.rst index 22838655d..f3f06e93f 100644 --- a/docs/echo.rst +++ b/docs/echo.rst @@ -4,11 +4,11 @@ Simple Echo Strategy API --- -.. autoclass:: stakemachine.strategies.echo.Echo +.. autoclass:: dexbot.strategies.echo.Echo :members: Full Source Code ---------------- -.. literalinclude:: ../stakemachine/strategies/echo.py +.. literalinclude:: ../dexbot/strategies/echo.py :language: python :linenos: diff --git a/docs/events.rst b/docs/events.rst index 8866d3b32..95eee6010 100644 --- a/docs/events.rst +++ b/docs/events.rst @@ -3,7 +3,7 @@ Events ****** The websocket endpoint of BitShares has notifications that are -subscribed to and dispatched by ``stakemachine``. This uses python's +subscribed to and dispatched by ``dexbot``. This uses python's native ``Events``. The following events are available in your strategies and depend on the configuration of your bot/strategy: diff --git a/docs/index.rst b/docs/index.rst index 8235cd29b..3ae00b0b7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,4 +1,4 @@ -Welcome to StakeMachine's documentation! +Welcome to DEXBot's documentation! ======================================== Basics diff --git a/docs/requirements.txt b/docs/requirements.txt index 87154af21..8b1378917 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1 @@ -stakemachine + diff --git a/docs/setup.rst b/docs/setup.rst index fbe2562db..0399533d9 100644 --- a/docs/setup.rst +++ b/docs/setup.rst @@ -7,17 +7,17 @@ Installation :: - pip3 install stakemachine [--user] + pip3 install dexbot [--user] If you install using the ``--user`` flag, the binaries of -``stakemachine`` and ``uptick`` are located in ``~/.local/bin``. +``dexbot`` and ``uptick`` are located in ``~/.local/bin``. Otherwise they should be globally reachable. Adding Keys ----------- It is important to *install* the private key of your bot's account into the pybitshares wallet. This can be done using -``uptick`` which is installed as a dependency of ``stakemachine``:: +``uptick`` which is installed as a dependency of ``dexbot``:: uptick addkey @@ -25,7 +25,7 @@ Configuration ------------- You will need to create configuration file in YAML format. The default file name is ``config.yml``, otherwise you can specify a different -config file using the ``--configufile X`` parameter of ``stakemachine``. +config file using the ``--configufile X`` parameter of ``dexbot``. Read more about the :doc:`configuration`. @@ -33,7 +33,7 @@ Running ------- The bot can be run by:: - stakemachine run + dexbot run It will ask for your wallet passphrase (that you have provide when adding your private key to pybitshares using ``uptick addkey``). diff --git a/docs/statemachine.rst b/docs/statemachine.rst index 4d64ee34a..4c1ebc230 100644 --- a/docs/statemachine.rst +++ b/docs/statemachine.rst @@ -12,5 +12,5 @@ inherited by :doc:`basestrategy`. API --- -.. autoclass:: stakemachine.statemachine.StateMachine +.. autoclass:: dexbot.statemachine.StateMachine :members: diff --git a/docs/storage.rst b/docs/storage.rst index d3c0dbeba..d9c02c6d1 100644 --- a/docs/storage.rst +++ b/docs/storage.rst @@ -3,7 +3,7 @@ Storage ******* This class allows to permanently store bot-specific data in a sqlite -database (``stakemachine.sqlite``) using: +database (``dexbot.sqlite``) using: ``self["key"] = "value"`` @@ -33,7 +33,7 @@ The user's data is stored in its OS protected user directory: * `~/.local/share/` -Where ```` is ``stakemachine`` and ```` is +Where ```` is ``dexbot`` and ```` is ``ChainSquad GmbH``. @@ -41,7 +41,7 @@ Simple example -------------- -.. literalinclude:: ../stakemachine/strategies/storagedemo.py +.. literalinclude:: ../dexbot/strategies/storagedemo.py :language: python :linenos: diff --git a/docs/wall.rst b/docs/wall.rst index 9d9858191..669e1c28c 100644 --- a/docs/wall.rst +++ b/docs/wall.rst @@ -19,7 +19,7 @@ Example Configuration Walls: # The Walls strategy module and class - module: stakemachine.strategies.walls + module: dexbot.strategies.walls bot: Walls # The market to serve @@ -58,6 +58,6 @@ Example Configuration Source Code ----------- -.. literalinclude:: ../stakemachine/strategies/walls.py +.. literalinclude:: ../dexbot/strategies/walls.py :language: python :linenos: From 4c911c33b5cd81d60a8877f46c0923ec15787a30 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 11 Jan 2018 14:00:43 +1100 Subject: [PATCH 0008/1846] embellishments to the logging system each bot gets its own LoggerAdapter bot name, market, account and disabled status are added to each log entry idea is GUI code can trap this log stream and display in interesting ways --- dexbot/basestrategy.py | 13 ++++++- dexbot/bot.py | 70 ++++++++++++++++++-------------------- dexbot/strategies/echo.py | 17 +++++---- dexbot/strategies/walls.py | 13 ++++--- dexbot/ui.py | 28 +++++++++++---- 5 files changed, 81 insertions(+), 60 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index d791a978d..d5f9f2876 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -6,7 +6,6 @@ from bitshares.instance import shared_bitshares_instance from .storage import Storage from .statemachine import StateMachine -log = logging.getLogger(__name__) class BaseStrategy(Storage, StateMachine, Events): @@ -29,6 +28,8 @@ class BaseStrategy(Storage, StateMachine, Events): * ``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) Also, Base Strategy inherits :class:`stakemachine.storage.Storage` which allows to permanently store data in a sqlite database @@ -37,6 +38,10 @@ class BaseStrategy(Storage, StateMachine, Events): ``basestrategy["key"] = "value"`` .. 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 + 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. """ __events__ = [ @@ -112,6 +117,12 @@ def __init__( # 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'], + 'is_disabled':(lambda: self.disabled)}) + @property def orders(self): """ Return the bot's open accounts in the current market diff --git a/dexbot/bot.py b/dexbot/bot.py index 2e0ff79ba..146115e8e 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -4,8 +4,14 @@ import logging from bitshares.notify import Notify from bitshares.instance import shared_bitshares_instance + 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(): @@ -14,7 +20,7 @@ class BotInfrastructure(): def __init__( self, config, - bitshares_instance=None, + bitshares_instance=None ): # BitShares instance self.bitshares = bitshares_instance or shared_bitshares_instance() @@ -24,14 +30,29 @@ def __init__( # Load all accounts and markets in use to subscribe to them accounts = set() markets = set() + + # Initialize bots: for botname, bot in config["bots"].items(): if "account" not in bot: - raise ValueError("Bot %s has no account" % botname) + log_bots.critical("Bot has no account",extra={'botname':botname,'account':'unknown','market':'unknown','is_dsabled':(lambda: True)}) + continue if "market" not in bot: - raise ValueError("Bot %s has no market" % botname) - - accounts.add(bot["account"]) - markets.add(bot["market"]) + 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"]), + bot["bot"] + ) + self.bots[botname] = klass( + config=config, + name=botname, + bitshares_instance=self.bitshares + ) + 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)}) # Create notification instance # Technically, this will multiplex markets and accounts and @@ -45,17 +66,6 @@ def __init__( bitshares_instance=self.bitshares ) - # Initialize bots: - for botname, bot in config["bots"].items(): - klass = getattr( - importlib.import_module(bot["module"]), - bot["bot"] - ) - self.bots[botname] = klass( - config=config, - name=botname, - bitshares_instance=self.bitshares - ) # Events def on_block(self, data): @@ -66,49 +76,35 @@ def on_block(self, data): self.bots[botname].ontick(data) except Exception as e: self.bots[botname].error_ontick(e) - log.error( - "Error while processing {botname}.tick(): {exception}\n{stack}".format( - botname=botname, - exception=str(e), - stack=traceback.format_exc() - )) + 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: - log.info("The bot %s has been disabled" % botname) + self.bots[botname].log.warning("disabled") continue if bot["market"] == data.market: try: self.bots[botname].onMarketUpdate(data) except Exception as e: self.bots[botname].error_onMarketUpdate(e) - log.error( - "Error while processing {botname}.onMarketUpdate(): {exception}\n{stack}".format( - botname=botname, - exception=str(e), - stack=traceback.format_exc() - )) + 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: - log.info("The bot %s has been disabled" % botname) + self.bot[botname].log.info("The bot %s has been disabled" % botname) continue if bot["account"] == account["name"]: try: self.bots[botname].onAccount(accountupdate) except Exception as e: self.bots[botname].error_onAccount(e) - log.error( - "Error while processing {botname}.onAccount(): {exception}\n{stack}".format( - botname=botname, - exception=str(e), - stack=traceback.format_exc() - )) + self.bots[botname].log.exception(".onAccountUpdate()") def run(self): self.notify.listen() + diff --git a/dexbot/strategies/echo.py b/dexbot/strategies/echo.py index 13d8b2561..3a37e0155 100644 --- a/dexbot/strategies/echo.py +++ b/dexbot/strategies/echo.py @@ -1,9 +1,8 @@ from dexbot.basestrategy import BaseStrategy -import logging -log = logging.getLogger(__name__) - class Echo(BaseStrategy): + + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -33,7 +32,7 @@ def print_orderMatched(self, i): :param bitshares.price.FilledOrder i: Filled order details """ - print("order matched: %s" % i) + self.log.info("order matched: %s" % i) def print_orderPlaced(self, i): """ Is called when a new order in the market is placed @@ -43,7 +42,7 @@ def print_orderPlaced(self, i): :param bitshares.price.Order i: Order details """ - print("order placed: %s" % i) + self.log.info("order placed: %s" % i) def print_UpdateCallOrder(self, i): """ Is called when a call order for a market pegged asset is updated @@ -53,7 +52,7 @@ def print_UpdateCallOrder(self, i): :param bitshares.price.CallOrder i: Call order details """ - print("call update: %s" % i) + self.log.info("call update: %s" % i) def print_marketUpdate(self, i): """ Is called when Something happens in your market. @@ -64,7 +63,7 @@ def print_marketUpdate(self, i): :param object i: Can be instance of ``FilledOrder``, ``Order``, or ``CallOrder`` """ - print("marketupdate: %s" % i) + self.log.info("marketupdate: %s" % i) def print_newBlock(self, i): """ Is called when a block is received @@ -76,7 +75,7 @@ def print_newBlock(self, i): need to know the most recent block number, you need to use ``bitshares.blockchain.Blockchain`` """ - print("new1 block: %s" % i) + self.log.info("new1 block: %s" % i) # raise ValueError("Testing disabling") def print_accountUpdate(self, i): @@ -84,4 +83,4 @@ def print_accountUpdate(self, i): any update. This includes anything that changes ``2.6.xxxx``, e.g., any operation that affects your account. """ - print("account: %s" % i) + self.log.info("account: %s" % i) diff --git a/dexbot/strategies/walls.py b/dexbot/strategies/walls.py index 3159d1ed4..41b9392c6 100644 --- a/dexbot/strategies/walls.py +++ b/dexbot/strategies/walls.py @@ -4,8 +4,7 @@ from bitshares.amount import Amount from dexbot.basestrategy import BaseStrategy from dexbot.errors import InsufficientFundsError -import logging -log = logging.getLogger(__name__) + class Walls(BaseStrategy): @@ -30,12 +29,12 @@ def __init__(self, *args, **kwargs): def error(self, *args, **kwargs): self.disabled = True self.cancelall() - pprint(self.execute()) + self.log.info(self.execute()) def updateorders(self): """ Update the orders """ - log.info("Replacing orders") + self.log.info("Replacing orders") # Canceling orders self.cancelall() @@ -75,7 +74,7 @@ def updateorders(self): account=self.account ) - pprint(self.execute()) + self.log.info(self.execute()) def getprice(self): """ Here we obtain the price for the quote and make sure it has @@ -108,7 +107,7 @@ def test(self, *args, **kwargs): not self["insufficient_buy"] and not self["insufficient_sell"] ): - log.info("No 2 orders available. Updating orders!") + self.log.info("No 2 orders available. Updating orders!") self.updateorders() elif len(orders) == 0: self.updateorders() @@ -118,5 +117,5 @@ def test(self, *args, **kwargs): self["feed_price"] and fabs(1 - float(self.getprice()) / self["feed_price"]) > self.bot["threshold"] / 100.0 ): - log.info("Price feed moved by more than the threshold. Updating orders!") + 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 ed845b951..b5ee0b6bb 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -14,17 +14,33 @@ def verbose(f): @click.pass_context def new_func(ctx, *args, **kwargs): - global log verbosity = [ "critical", "error", "warn", "info", "debug" ][int(min(ctx.obj.get("verbose", 0), 4))] - log.setLevel(getattr(logging, verbosity.upper())) - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + 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') + 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') + 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') + + # use special format for special bots logger ch = logging.StreamHandler() ch.setLevel(getattr(logging, verbosity.upper())) - ch.setFormatter(formatter) - log.addHandler(ch) - + ch.setFormatter(formatter2) + logging.getLogger("dexbot.per_bot").addHandler(ch) + logging.getLogger("dexbot.per_bot").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) + logging.getLogger("dexbot").addHandler(ch) + # GrapheneAPI logging if ctx.obj["verbose"] > 4: verbosity = [ From edfefeb8fd19a79843ae79b5fccc577ad251b2c0 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 11 Jan 2018 14:08:37 +1100 Subject: [PATCH 0009/1846] allow users to optionally have a ~/bots directory gets added to sys.path if found so easier to have private bot strategies. --- dexbot/bot.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dexbot/bot.py b/dexbot/bot.py index 2e0ff79ba..95a8086c6 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -2,6 +2,7 @@ import importlib import time import logging +import os.path from bitshares.notify import Notify from bitshares.instance import shared_bitshares_instance log = logging.getLogger(__name__) @@ -45,6 +46,11 @@ def __init__( bitshares_instance=self.bitshares ) + # set the module search path + userbotpath = os.path.expanduser("~/bots") + if os.path.exists(userbotpath): + sys.path.append(userbotpath) + # Initialize bots: for botname, bot in config["bots"].items(): klass = getattr( From 552918e428e47fa5f68005eb3b74d9a19ae50191 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 11 Jan 2018 14:23:31 +1100 Subject: [PATCH 0010/1846] add dependencies for configurator to work pythonconfig only works on UNIX-descended OS: Linux/MacOS/BSD sdnotify only works with systemd (so Linux, and not even all distros) however they are pure-python modules so they will install anywhere, they just won't work with sdnotify my code will then silently ignore it and won't offer systemd-related features. without pythonconfig the CLI configurator won't work at all: but it's unlikely Windows users will care: they will just run the GUI --- setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ecf51e9d5..f3c215fb0 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,9 @@ "pyyaml", "sqlalchemy", "appdirs", - "pyqt5" + "pyqt5", + "pythonconfig", + "sdnotify" ], include_package_data=True, ) From f57df8a65ed7a7ffd70fd9fa765be652ec8ed92a Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 11 Jan 2018 14:50:24 +1100 Subject: [PATCH 0011/1846] use Codaone repo --- docs/setup.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/setup.rst b/docs/setup.rst index 311ed99cf..0c8a5e6bc 100644 --- a/docs/setup.rst +++ b/docs/setup.rst @@ -26,7 +26,7 @@ Installation :: - pip3 install git+https://github.com/ihaywood3/DEXBot.git [--user] + pip3 install git+https://github.com/Codaone/DEXBot.git [--user] If you install using the ``--user`` flag, the binaries of ``dexbot`` and ``uptick`` are located in ``~/.local/bin``. From 169f658e507707518e34888e8162e23bdfa0f102 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 11 Jan 2018 15:30:28 +1100 Subject: [PATCH 0012/1846] fix package name --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f3c215fb0..f5c41dc14 100755 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ "sqlalchemy", "appdirs", "pyqt5", - "pythonconfig", + "pythondialog", "sdnotify" ], include_package_data=True, From 63a9e8cfe1bd259be2dbadc2a0fe34c588d70a45 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 12 Jan 2018 09:34:31 +0200 Subject: [PATCH 0013/1846] Change stakemachine folder name to dexbot in Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index cbebf9a00..b9760ea75 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ clean-pyc: find . -name '*~' -exec rm -f {} + lint: - flake8 stakemachine/ + flake8 dexbot/ build: python3 setup.py build From 202c3c8ab96f98cca37ab60bf672916560d7328d Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 12 Jan 2018 09:35:36 +0200 Subject: [PATCH 0014/1846] Change config.yml to have a working example --- config.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/config.yml b/config.yml index a4fb47797..b2041664f 100644 --- a/config.yml +++ b/config.yml @@ -2,12 +2,7 @@ node: "wss://node.testnet.bitshares.eu" bots: Echo: - module: "stakemachine.strategies.echo" + module: "dexbot.strategies.echo" bot: "Echo" - market: GOLD:TEST - account: codaonetest-1 - Demo: - module: "stakemachine.strategies.storagedemo" - bot: "StorageDemo" - market: GOLD:TEST - account: codaonetest-1 + market: "TEST:PEG.FAKEUSD" + account: "dexbot-test" \ No newline at end of file From 7bd8e9122a473d20e8399bf47ccabece2f0b85bb Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 15 Jan 2018 14:14:42 +0200 Subject: [PATCH 0015/1846] Fix import error in bot.py Bot.py was missing sys import --- dexbot/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/bot.py b/dexbot/bot.py index 95a8086c6..eb9513874 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -1,6 +1,6 @@ import traceback import importlib -import time +import sys import logging import os.path from bitshares.notify import Notify From 05d6aa4118032157de57baa314c255d000c98c60 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 17 Jan 2018 09:40:40 +0200 Subject: [PATCH 0016/1846] Add ruamel.yaml library --- dexbot/controllers/main_controller.py | 3 ++- setup.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 4decdb977..9fa899aca 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -1,5 +1,5 @@ from PyQt5 import QtWidgets -import yaml +from ruamel.yaml import YAML class MainController(object): @@ -11,4 +11,5 @@ def get_bots_data(self): Returns dict of all the bots data """ with open('config.yml', 'r') as f: + yaml = YAML() return yaml.load(f)['bots'] diff --git a/setup.py b/setup.py index ecf51e9d5..d963a7780 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,8 @@ "pyyaml", "sqlalchemy", "appdirs", - "pyqt5" + "pyqt5", + "ruamel.yaml" ], include_package_data=True, ) From e43eaa9f9590711250ec9cd3dec3cf83e6a373a6 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 19 Jan 2018 13:07:32 +0200 Subject: [PATCH 0017/1846] Change the gui mvc logic --- app.py | 7 +------ dexbot/controllers/main_controller.py | 10 +++++++--- dexbot/model/__init__.py | 0 dexbot/model/model.py | 7 ------- dexbot/views/bot_list.py | 7 +++---- 5 files changed, 11 insertions(+), 20 deletions(-) delete mode 100644 dexbot/model/__init__.py delete mode 100644 dexbot/model/model.py diff --git a/app.py b/app.py index aea053925..d2ad781b6 100644 --- a/app.py +++ b/app.py @@ -2,18 +2,13 @@ from PyQt5 import Qt -from dexbot.views.bot_list import MainView from dexbot.controllers.main_controller import MainController -from dexbot.model.model import Model class App(Qt.QApplication): def __init__(self, sys_argv): super(App, self).__init__(sys_argv) - self.model = Model() - self.main_ctrl = MainController(self.model) - self.main_view = MainView(self.model, self.main_ctrl) - self.main_view.show() + self.main_ctrl = MainController() if __name__ == '__main__': app = App(sys.argv) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 9fa899aca..fbd5733ba 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -1,10 +1,14 @@ -from PyQt5 import QtWidgets +from dexbot.views.bot_list import MainView +from dexbot.bot import BotInfrastructure + from ruamel.yaml import YAML class MainController(object): - def __init__(self, model): - pass + def __init__(self): + self.model = BotInfrastructure + self.view = MainView(self) + self.view.show() def get_bots_data(self): """ diff --git a/dexbot/model/__init__.py b/dexbot/model/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/dexbot/model/model.py b/dexbot/model/model.py deleted file mode 100644 index 94b1de0bd..000000000 --- a/dexbot/model/model.py +++ /dev/null @@ -1,7 +0,0 @@ -from PyQt5 import QtGui - - -class Model(object): - - def __init__(self): - pass diff --git a/dexbot/views/bot_list.py b/dexbot/views/bot_list.py index 1b595359a..b6a8df287 100644 --- a/dexbot/views/bot_list.py +++ b/dexbot/views/bot_list.py @@ -7,8 +7,7 @@ class MainView(QtWidgets.QMainWindow): - def __init__(self, model, main_ctrl): - self.model = model + def __init__(self, main_ctrl): self.main_ctrl = main_ctrl super(MainView, self).__init__() self.ui = Ui_MainWindow() @@ -27,8 +26,8 @@ def add_bot_widget(self): widget.setFixedSize(widget.frameSize()) def handle_add_bot(self): - self.create_bot_dialog = CreateBotView(self.model, self.main_ctrl) - return_value = self.create_bot_dialog.exec_() + create_bot_dialog = CreateBotView(self.main_ctrl) + return_value = create_bot_dialog.exec_() if return_value == 1: self.add_bot_widget() From af7010e24ee4e658378010f1e085e3f3f8eecf88 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 19 Jan 2018 14:33:57 +0200 Subject: [PATCH 0018/1846] Change BotItemWidget location to it's own file And add more functionality to the widget --- dexbot/controllers/main_controller.py | 14 ++++++ dexbot/views/bot_item.py | 61 +++++++++++++++++++++++++++ dexbot/views/bot_list.py | 19 --------- 3 files changed, 75 insertions(+), 19 deletions(-) create mode 100644 dexbot/views/bot_item.py diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index fbd5733ba..372260a67 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -2,6 +2,8 @@ from dexbot.bot import BotInfrastructure from ruamel.yaml import YAML +from bitshares import BitShares + class MainController(object): @@ -11,6 +13,18 @@ def __init__(self): self.view.show() def get_bots_data(self): + def create_bot(self, botname, config): + bitshares = BitShares( + node=config['node'] + ) + + def stop_bot(self, bot_id): + self.bots[bot_id].terminate() + + def remove_bot(self, botname): + # Todo: cancell all orders on removal + self.bots[botname].terminate() + """ Returns dict of all the bots data """ diff --git a/dexbot/views/bot_item.py b/dexbot/views/bot_item.py new file mode 100644 index 000000000..d9d65f00a --- /dev/null +++ b/dexbot/views/bot_item.py @@ -0,0 +1,61 @@ +from PyQt5 import QtWidgets + +from dexbot.views.gen.bot_item_widget import Ui_widget + + +class BotItemWidget(QtWidgets.QWidget, Ui_widget): + + def __init__(self, botname, config, controller): + super(BotItemWidget, self).__init__() + + self.running = False + self.botname = botname + self.config = config + self.controller = controller + + 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) + + 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) + + def start_bot(self): + self.running = True + self.pause_button.show() + self.play_button.hide() + + self.controller.create_bot(self.botname, self.config) + + 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): + self.bot_profit.setText(value) + + def remove_widget(self): + if self.running: + self.controller.remove_bot(self.botname) + self.deleteLater() diff --git a/dexbot/views/bot_list.py b/dexbot/views/bot_list.py index b6a8df287..a5395c9e8 100644 --- a/dexbot/views/bot_list.py +++ b/dexbot/views/bot_list.py @@ -36,25 +36,6 @@ def refresh_bot_list(self): pass -class BotItemWidget(QtWidgets.QWidget, Ui_widget): - def __init__(self): - super(BotItemWidget, self).__init__() - 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) - - def start_bot(self): - self.pause_button.show() - self.play_button.hide() - - def pause_bot(self): - self.pause_button.hide() - self.play_button.show() - - def remove_widget(self): - self.deleteLater() From 315462c1526b136eb3237bd9fe4e45ec87e89f47 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 19 Jan 2018 14:37:13 +0200 Subject: [PATCH 0019/1846] Add process queue for ui managing --- dexbot/queue/idle_queue.py | 9 +++++++++ dexbot/queue/queue_dispatcher.py | 30 ++++++++++++++++++++++++++++++ dexbot/views/bot_list.py | 4 ++++ 3 files changed, 43 insertions(+) create mode 100644 dexbot/queue/idle_queue.py create mode 100644 dexbot/queue/queue_dispatcher.py diff --git a/dexbot/queue/idle_queue.py b/dexbot/queue/idle_queue.py new file mode 100644 index 000000000..1dd980d84 --- /dev/null +++ b/dexbot/queue/idle_queue.py @@ -0,0 +1,9 @@ +import queue + +idle_loop = queue.Queue() + + +def idle_add(func, *args, **kwargs): + def idle(): + func(*args, **kwargs) + idle_loop.put(idle) diff --git a/dexbot/queue/queue_dispatcher.py b/dexbot/queue/queue_dispatcher.py new file mode 100644 index 000000000..7a3e028da --- /dev/null +++ b/dexbot/queue/queue_dispatcher.py @@ -0,0 +1,30 @@ +from PyQt5.Qt import QApplication +from PyQt5.QtCore import QThread, QEvent + +from dexbot.queue.idle_queue import idle_loop + + +class ThreadDispatcher(QThread): + def __init__(self, parent): + QThread.__init__(self) + self.parent = parent + + def run(self): + while True: + callback = idle_loop.get() + if callback is None: + break + QApplication.postEvent(self.parent, _Event(callback)) + + def stop(self): + idle_loop.put(None) + self.wait() + + +class _Event(QEvent): + EVENT_TYPE = QEvent.Type(QEvent.registerEventType()) + + def __init__(self, callback): + # Thread-safe + QEvent.__init__(self, _Event.EVENT_TYPE) + self.callback = callback \ No newline at end of file diff --git a/dexbot/views/bot_list.py b/dexbot/views/bot_list.py index a5395c9e8..171de9d96 100644 --- a/dexbot/views/bot_list.py +++ b/dexbot/views/bot_list.py @@ -1,5 +1,6 @@ from PyQt5 import QtGui, QtWidgets, QtCore +from dexbot.queue.queue_dispatcher import ThreadDispatcher from dexbot.views.gen.bot_list_window import Ui_MainWindow from dexbot.views.gen.bot_item_widget import Ui_widget from dexbot.views.create_bot import CreateBotView @@ -23,6 +24,9 @@ def __init__(self, main_ctrl): def add_bot_widget(self): widget = BotItemWidget() self.bot_container.addWidget(widget) + # Dispatcher polls for events from the bots that are used to change the ui + self.dispatcher = ThreadDispatcher(self) + self.dispatcher.start() widget.setFixedSize(widget.frameSize()) def handle_add_bot(self): From 995ae94c9ddba2ef43c25719fe842b23532e1378 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 19 Jan 2018 14:38:07 +0200 Subject: [PATCH 0020/1846] Add config fetching functions --- dexbot/controllers/main_controller.py | 40 +++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 372260a67..8402a4daa 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -25,9 +25,49 @@ def remove_bot(self, botname): # Todo: cancell all orders on removal self.bots[botname].terminate() + @staticmethod + def get_unique_bot_name(): + """ + Returns unique bot name "Bot %n", where %n is the next available index + """ + bots = MainController.get_bots_data().keys() + bot = '' + index = 1 + while not bot: + if "Bot {0}".format(index) not in bots: + bot = "Bot {0}".format(index) + index += 1 + + return bot + + @staticmethod + def get_bots_data(): """ Returns dict of all the bots data """ with open('config.yml', 'r') as f: yaml = YAML() return yaml.load(f)['bots'] + + @staticmethod + def get_latest_bot_config(): + """ + Returns config file data with only the latest bot data + """ + 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]} + return config + + @staticmethod + def get_bot_config(botname): + """ + Returns config file data with only the data from a specific bot + """ + with open('config.yml', 'r') as f: + yaml = YAML() + config = yaml.load(f) + config['bots'] = {botname: config['bots'][botname]} + return config From 887abcb6720d9c12fa5d9c9f04851748de287fb1 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 19 Jan 2018 14:39:47 +0200 Subject: [PATCH 0021/1846] Fix code spacing --- dexbot/bot.py | 2 +- dexbot/strategies/echo.py | 2 +- dexbot/strategies/manualorders.py | 1 + dexbot/strategies/storagedemo.py | 1 + dexbot/strategies/walls.py | 2 +- 5 files changed, 5 insertions(+), 3 deletions(-) diff --git a/dexbot/bot.py b/dexbot/bot.py index 4c56eae34..0081f143d 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -16,6 +16,7 @@ class BotInfrastructure(): + bots = dict() def __init__( @@ -112,4 +113,3 @@ def on_account(self, accountupdate): def run(self): self.notify.listen() - diff --git a/dexbot/strategies/echo.py b/dexbot/strategies/echo.py index 3a37e0155..278306d3b 100644 --- a/dexbot/strategies/echo.py +++ b/dexbot/strategies/echo.py @@ -1,7 +1,7 @@ from dexbot.basestrategy import BaseStrategy -class Echo(BaseStrategy): +class Echo(BaseStrategy): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/dexbot/strategies/manualorders.py b/dexbot/strategies/manualorders.py index 261b55827..f99c9eba2 100644 --- a/dexbot/strategies/manualorders.py +++ b/dexbot/strategies/manualorders.py @@ -4,6 +4,7 @@ class ManualOrders(BaseStrategy): + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/dexbot/strategies/storagedemo.py b/dexbot/strategies/storagedemo.py index 3579a4c87..bc8941d73 100644 --- a/dexbot/strategies/storagedemo.py +++ b/dexbot/strategies/storagedemo.py @@ -2,6 +2,7 @@ class StorageDemo(BaseStrategy): + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.ontick += self.tick diff --git a/dexbot/strategies/walls.py b/dexbot/strategies/walls.py index 41b9392c6..c8c455d32 100644 --- a/dexbot/strategies/walls.py +++ b/dexbot/strategies/walls.py @@ -6,8 +6,8 @@ from dexbot.errors import InsufficientFundsError - class Walls(BaseStrategy): + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) From e46b2e9135cd10db434842e8b31e82cca0584ad8 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 19 Jan 2018 14:42:16 +0200 Subject: [PATCH 0022/1846] Fix variable name typo --- dexbot/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/bot.py b/dexbot/bot.py index 0081f143d..7bf872d38 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -102,7 +102,7 @@ def on_account(self, accountupdate): account = accountupdate.account for botname, bot in self.config["bots"].items(): if self.bots[botname].disabled: - self.bot[botname].log.info("The bot %s has been disabled" % botname) + self.bots[botname].log.info("The bot %s has been disabled" % botname) continue if bot["account"] == account["name"]: try: From 47776d80fd9693fdca6f0d150d6739d70126e7b3 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 19 Jan 2018 14:42:42 +0200 Subject: [PATCH 0023/1846] Remove unneeded import --- dexbot/bot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dexbot/bot.py b/dexbot/bot.py index 7bf872d38..1065d462a 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -1,4 +1,3 @@ -import traceback import importlib import sys import logging From a7097c33a3cbe1a941dd3b722db4adab069f10e8 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 19 Jan 2018 14:52:55 +0200 Subject: [PATCH 0024/1846] Add multiple different gui features - Add botname to the bot creation dialog - Add bot widget ui setters - Change the way widgets area loaded --- dexbot/views/bot_list.py | 35 ++++++++++++++++++++------ dexbot/views/create_bot.py | 22 +++++++++++++--- dexbot/views/gen/create_bot_window.py | 15 ++++++++--- dexbot/views/orig/create_bot_window.ui | 18 ++++++++++--- 4 files changed, 72 insertions(+), 18 deletions(-) diff --git a/dexbot/views/bot_list.py b/dexbot/views/bot_list.py index 171de9d96..42ebc004e 100644 --- a/dexbot/views/bot_list.py +++ b/dexbot/views/bot_list.py @@ -2,12 +2,15 @@ from dexbot.queue.queue_dispatcher import ThreadDispatcher from dexbot.views.gen.bot_list_window import Ui_MainWindow -from dexbot.views.gen.bot_item_widget import Ui_widget from dexbot.views.create_bot import CreateBotView +from dexbot.views.bot_item import BotItemWidget + class MainView(QtWidgets.QMainWindow): + bot_widgets = dict() + def __init__(self, main_ctrl): self.main_ctrl = main_ctrl super(MainView, self).__init__() @@ -17,29 +20,47 @@ def __init__(self, main_ctrl): self.ui.add_bot_button.clicked.connect(self.handle_add_bot) + # Load bot widgets from config file bots = main_ctrl.get_bots_data() - for bot in bots: - self.add_bot_widget() + for botname in bots: + config = self.main_ctrl.get_bot_config(botname) + self.add_bot_widget(botname, config) - def add_bot_widget(self): - widget = BotItemWidget() - self.bot_container.addWidget(widget) # 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) widget.setFixedSize(widget.frameSize()) + self.bot_container.addWidget(widget) + self.bot_widgets['botname'] = widget def handle_add_bot(self): create_bot_dialog = CreateBotView(self.main_ctrl) return_value = create_bot_dialog.exec_() + # User clicked save if return_value == 1: - self.add_bot_widget() + botname = create_bot_dialog.botname + 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_id, value): + self.bot_widgets[bot_id].set_bot_name(value) + def set_bot_account(self, bot_id, value): + self.bot_widgets[bot_id].set_bot_account(value) + def set_bot_profit(self, bot_id, value): + self.bot_widgets[bot_id].set_bot_profit(value) + def set_bot_market(self, bot_id, value): + self.bot_widgets[bot_id].set_bot_market(value) + def customEvent(self, event): + # Process idle_queue_dispatcher events + event.callback() diff --git a/dexbot/views/create_bot.py b/dexbot/views/create_bot.py index fba390b00..dbb160459 100644 --- a/dexbot/views/create_bot.py +++ b/dexbot/views/create_bot.py @@ -5,17 +5,33 @@ class CreateBotView(QtWidgets.QDialog): - def __init__(self, model, main_ctrl): - self.model = model - self.main_ctrl = main_ctrl + def __init__(self, controller): + self.controller = controller super(CreateBotView, self).__init__() self.ui = Ui_Dialog() self.ui.setupUi(self) + botname = controller.get_unique_bot_name() + self.ui.botname_input.setText(botname) + self.ui.save_button.clicked.connect(self.handle_save) self.ui.cancel_button.clicked.connect(self.handle_cancel) + def validate_form(self): + return True + def handle_save(self): + if not self.validate_form(): + # Todo: add validation error notice for user + return + self.botname = self.ui.botname_input.getText() + bot_data = { + 'account': self.ui.account_input, + 'market': '', + 'module': '', + 'bot': '' + } + self.controller.add_bot_config(self.botname, bot_data) self.accept() def handle_cancel(self): diff --git a/dexbot/views/gen/create_bot_window.py b/dexbot/views/gen/create_bot_window.py index 5d8f2a42b..021ac7ca6 100644 --- a/dexbot/views/gen/create_bot_window.py +++ b/dexbot/views/gen/create_bot_window.py @@ -47,16 +47,22 @@ def setupUi(self, Dialog): self.formLayout.setObjectName("formLayout") self.strategy_label = QtWidgets.QLabel(Dialog) self.strategy_label.setObjectName("strategy_label") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.strategy_label) + self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.strategy_label) self.strategy_input = QtWidgets.QComboBox(Dialog) self.strategy_input.setObjectName("strategy_input") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.strategy_input) + self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.strategy_input) self.account_label = QtWidgets.QLabel(Dialog) self.account_label.setObjectName("account_label") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.account_label) + self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.account_label) self.account_input = QtWidgets.QLineEdit(Dialog) self.account_input.setObjectName("account_input") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.account_input) + self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.account_input) + self.botname_label = QtWidgets.QLabel(Dialog) + self.botname_label.setObjectName("botname_label") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.botname_label) + self.botname_input = QtWidgets.QLineEdit(Dialog) + self.botname_input.setObjectName("botname_input") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.botname_input) self.gridLayout.addLayout(self.formLayout, 0, 0, 1, 1) self.retranslateUi(Dialog) @@ -69,4 +75,5 @@ def retranslateUi(self, Dialog): self.save_button.setText(_translate("Dialog", "Save")) self.strategy_label.setText(_translate("Dialog", "Strategy")) self.account_label.setText(_translate("Dialog", "Account")) + self.botname_label.setText(_translate("Dialog", "Bot name")) diff --git a/dexbot/views/orig/create_bot_window.ui b/dexbot/views/orig/create_bot_window.ui index abfb11fe3..cdf65ad6c 100644 --- a/dexbot/views/orig/create_bot_window.ui +++ b/dexbot/views/orig/create_bot_window.ui @@ -70,26 +70,36 @@ - + Strategy - + - + Account - + + + + + Bot name + + + + + + From a77ebe988ab5e6a25a83256d6f0e04bce62be343 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 19 Jan 2018 14:56:40 +0200 Subject: [PATCH 0025/1846] Add bot creation logic --- dexbot/bot.py | 11 ++++++++--- dexbot/controllers/main_controller.py | 12 +++++++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/dexbot/bot.py b/dexbot/bot.py index 1065d462a..c44c67aa2 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -2,6 +2,8 @@ import sys import logging import os.path +from multiprocessing import Process + from bitshares.notify import Notify from bitshares.instance import shared_bitshares_instance @@ -13,16 +15,18 @@ # 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(): +class BotInfrastructure(Process): bots = dict() def __init__( self, config, - bitshares_instance=None + bitshares_instance=None, + gui_data=None ): + super().__init__() # BitShares instance self.bitshares = bitshares_instance or shared_bitshares_instance() @@ -48,7 +52,8 @@ def __init__( self.bots[botname] = klass( config=config, name=botname, - bitshares_instance=self.bitshares + bitshares_instance=self.bitshares, + gui_data=gui_data ) markets.add(bot['market']) accounts.add(bot['account']) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 8402a4daa..19d01512e 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -3,20 +3,30 @@ from ruamel.yaml import YAML from bitshares import BitShares +from bitshares.instance import set_shared_bitshares_instance class MainController(object): + bots = dict() + def __init__(self): self.model = BotInfrastructure self.view = MainView(self) self.view.show() - def get_bots_data(self): def create_bot(self, botname, config): bitshares = BitShares( node=config['node'] ) + set_shared_bitshares_instance(bitshares) + bitshares.wallet.unlock('test') # Temporal code until password input is implemented + + gui_data = {'id': botname, 'controller': self} + bot = self.model(config, bitshares, gui_data) + bot.daemon = True + bot.start() + self.bots[botname] = bot def stop_bot(self, bot_id): self.bots[bot_id].terminate() From c443cda6b4ff6305bab28d7c7bc22cdddd3d5b13 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 19 Jan 2018 15:31:03 +0200 Subject: [PATCH 0026/1846] Change get_unique_bot_name logic --- dexbot/controllers/main_controller.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 19d01512e..44a710784 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -40,15 +40,14 @@ def get_unique_bot_name(): """ Returns unique bot name "Bot %n", where %n is the next available index """ - bots = MainController.get_bots_data().keys() - bot = '' index = 1 - while not bot: - if "Bot {0}".format(index) not in bots: - bot = "Bot {0}".format(index) + bots = MainController.get_bots_data().keys() + botname = "Bot {0}".format(index) + while botname in bots: + botname = "Bot {0}".format(index) index += 1 - return bot + return botname @staticmethod def get_bots_data(): From fd9ca088568bcbaddaef66d52095752a9644fe97 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 22 Jan 2018 08:19:52 +0200 Subject: [PATCH 0027/1846] Fix bot creation --- dexbot/controllers/main_controller.py | 11 +++++++++++ dexbot/views/create_bot.py | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 44a710784..2f346c01b 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -35,6 +35,17 @@ def remove_bot(self, botname): # Todo: cancell all orders on removal self.bots[botname].terminate() + @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) + @staticmethod def get_unique_bot_name(): """ diff --git a/dexbot/views/create_bot.py b/dexbot/views/create_bot.py index dbb160459..6fe66add3 100644 --- a/dexbot/views/create_bot.py +++ b/dexbot/views/create_bot.py @@ -24,9 +24,9 @@ def handle_save(self): if not self.validate_form(): # Todo: add validation error notice for user return - self.botname = self.ui.botname_input.getText() + self.botname = self.ui.botname_input.text() bot_data = { - 'account': self.ui.account_input, + 'account': self.ui.account_input.text(), 'market': '', 'module': '', 'bot': '' From 13842cf0df3dce12ae556ecbb53141d43fc61805 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 22 Jan 2018 15:45:25 +0200 Subject: [PATCH 0028/1846] Add wallet unlocking/creation to the GUI Also change the layout of the code to be more qt like --- app.py | 26 +++- dexbot/controllers/main_controller.py | 25 ++- dexbot/controllers/wallet_controller.py | 24 +++ dexbot/views/create_wallet.py | 23 +++ dexbot/views/gen/create_wallet_window.py | 101 +++++++++++++ dexbot/views/gen/notice_window.py | 45 ++++++ dexbot/views/gen/unlock_wallet_window.py | 86 +++++++++++ dexbot/views/notice.py | 14 ++ dexbot/views/orig/create_wallet_window.ui | 176 ++++++++++++++++++++++ dexbot/views/orig/notice_window.ui | 56 +++++++ dexbot/views/orig/unlock_wallet_window.ui | 143 ++++++++++++++++++ dexbot/views/unlock_wallet.py | 23 +++ 12 files changed, 727 insertions(+), 15 deletions(-) create mode 100644 dexbot/controllers/wallet_controller.py create mode 100644 dexbot/views/create_wallet.py create mode 100644 dexbot/views/gen/create_wallet_window.py create mode 100644 dexbot/views/gen/notice_window.py create mode 100644 dexbot/views/gen/unlock_wallet_window.py create mode 100644 dexbot/views/notice.py create mode 100644 dexbot/views/orig/create_wallet_window.ui create mode 100644 dexbot/views/orig/notice_window.ui create mode 100644 dexbot/views/orig/unlock_wallet_window.ui create mode 100644 dexbot/views/unlock_wallet.py diff --git a/app.py b/app.py index d2ad781b6..4fa494e20 100644 --- a/app.py +++ b/app.py @@ -1,15 +1,37 @@ import sys from PyQt5 import Qt +from bitshares import BitShares from dexbot.controllers.main_controller import MainController +from dexbot.views.bot_list import MainView +from dexbot.controllers.wallet_controller import WalletController +from dexbot.views.unlock_wallet import UnlockWalletView +from dexbot.views.create_wallet import CreateWalletView class App(Qt.QApplication): def __init__(self, sys_argv): super(App, self).__init__(sys_argv) - self.main_ctrl = MainController() + + config = MainController.load_config() + bitshares_instance = BitShares(config['node']) + + # Wallet unlock + unlock_ctrl = WalletController(bitshares_instance) + if unlock_ctrl.wallet_created(): + unlock_view = UnlockWalletView(unlock_ctrl) + else: + unlock_view = CreateWalletView(unlock_ctrl) + + if unlock_view.exec_(): + bitshares_instance = unlock_ctrl.bitshares + self.main_ctrl = MainController(bitshares_instance) + self.main_view = MainView(self.main_ctrl) + self.main_view.show() + else: + sys.exit() if __name__ == '__main__': app = App(sys.argv) - sys.exit(app.exec_()) \ No newline at end of file + sys.exit(app.exec_()) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 2f346c01b..86825c81d 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -2,28 +2,21 @@ from dexbot.bot import BotInfrastructure from ruamel.yaml import YAML -from bitshares import BitShares from bitshares.instance import set_shared_bitshares_instance -class MainController(object): +class MainController: bots = dict() - def __init__(self): - self.model = BotInfrastructure - self.view = MainView(self) - self.view.show() + def __init__(self, bitshares_instance): + self.bitshares_instance = bitshares_instance + set_shared_bitshares_instance(bitshares_instance) + self.bot_template = BotInfrastructure def create_bot(self, botname, config): - bitshares = BitShares( - node=config['node'] - ) - set_shared_bitshares_instance(bitshares) - bitshares.wallet.unlock('test') # Temporal code until password input is implemented - gui_data = {'id': botname, 'controller': self} - bot = self.model(config, bitshares, gui_data) + bot = self.bot_template(config, self.bitshares_instance, gui_data) bot.daemon = True bot.start() self.bots[botname] = bot @@ -35,6 +28,12 @@ def remove_bot(self, botname): # Todo: cancell all orders on removal self.bots[botname].terminate() + @staticmethod + def load_config(): + yaml = YAML() + with open('config.yml', 'r') as f: + return yaml.load(f) + @staticmethod def add_bot_config(botname, bot_data): yaml = YAML() diff --git a/dexbot/controllers/wallet_controller.py b/dexbot/controllers/wallet_controller.py new file mode 100644 index 000000000..3dec4ae7f --- /dev/null +++ b/dexbot/controllers/wallet_controller.py @@ -0,0 +1,24 @@ +import bitshares + + +class WalletController: + + def __init__(self, bitshares_instance): + self.bitshares = bitshares_instance + + def wallet_created(self): + return self.bitshares.wallet.created() + + def create_wallet(self, password, confirm_password): + if password == confirm_password: + self.bitshares.wallet.create(password) + return True + else: + return False + + def unlock_wallet(self, password): + try: + self.bitshares.wallet.unlock(password) + return True + except bitshares.exceptions.WrongMasterPasswordException: + return False diff --git a/dexbot/views/create_wallet.py b/dexbot/views/create_wallet.py new file mode 100644 index 000000000..c5444e4fa --- /dev/null +++ b/dexbot/views/create_wallet.py @@ -0,0 +1,23 @@ +from dexbot.views.gen.create_wallet_window import Ui_Dialog +from dexbot.views.notice import NoticeDialog + +from PyQt5 import QtWidgets + + +class CreateWalletView(QtWidgets.QDialog): + + 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) + + def validate_form(self): + password = self.ui.password_input.text() + confirm_password = self.ui.confirm_password_input.text() + if not self.controller.create_wallet(password, confirm_password): + dialog = NoticeDialog('Passwords do not match!') + dialog.exec_() + else: + self.accept() diff --git a/dexbot/views/gen/create_wallet_window.py b/dexbot/views/gen/create_wallet_window.py new file mode 100644 index 000000000..970533767 --- /dev/null +++ b/dexbot/views/gen/create_wallet_window.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'dexbot/views/orig/create_wallet_window.ui' +# +# Created by: PyQt5 UI code generator 5.9.2 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName("Dialog") + Dialog.resize(530, 196) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(Dialog.sizePolicy().hasHeightForWidth()) + Dialog.setSizePolicy(sizePolicy) + Dialog.setModal(True) + self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) + self.verticalLayout.setObjectName("verticalLayout") + self.helper_text = QtWidgets.QLabel(Dialog) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Maximum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.helper_text.sizePolicy().hasHeightForWidth()) + self.helper_text.setSizePolicy(sizePolicy) + self.helper_text.setAlignment(QtCore.Qt.AlignCenter) + self.helper_text.setObjectName("helper_text") + self.verticalLayout.addWidget(self.helper_text) + self.label = QtWidgets.QLabel(Dialog) + self.label.setAlignment(QtCore.Qt.AlignCenter) + self.label.setObjectName("label") + self.verticalLayout.addWidget(self.label) + self.widget = QtWidgets.QWidget(Dialog) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.widget.sizePolicy().hasHeightForWidth()) + self.widget.setSizePolicy(sizePolicy) + self.widget.setMaximumSize(QtCore.QSize(16777215, 126)) + self.widget.setObjectName("widget") + self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.widget) + self.horizontalLayout_3.setContentsMargins(-1, 5, -1, 5) + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.form_wrap = QtWidgets.QWidget(self.widget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.form_wrap.sizePolicy().hasHeightForWidth()) + self.form_wrap.setSizePolicy(sizePolicy) + self.form_wrap.setObjectName("form_wrap") + self.formLayout = QtWidgets.QFormLayout(self.form_wrap) + self.formLayout.setLabelAlignment(QtCore.Qt.AlignCenter) + self.formLayout.setFormAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignTop) + self.formLayout.setObjectName("formLayout") + self.password_label = QtWidgets.QLabel(self.form_wrap) + self.password_label.setObjectName("password_label") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.password_label) + self.password_input = QtWidgets.QLineEdit(self.form_wrap) + self.password_input.setMinimumSize(QtCore.QSize(200, 0)) + self.password_input.setEchoMode(QtWidgets.QLineEdit.Password) + self.password_input.setObjectName("password_input") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.password_input) + self.ok_button = QtWidgets.QPushButton(self.form_wrap) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.ok_button.sizePolicy().hasHeightForWidth()) + self.ok_button.setSizePolicy(sizePolicy) + self.ok_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.ok_button.setObjectName("ok_button") + self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.ok_button) + self.confirm_password_label = QtWidgets.QLabel(self.form_wrap) + self.confirm_password_label.setObjectName("confirm_password_label") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.confirm_password_label) + self.confirm_password_input = QtWidgets.QLineEdit(self.form_wrap) + self.confirm_password_input.setMinimumSize(QtCore.QSize(200, 0)) + self.confirm_password_input.setEchoMode(QtWidgets.QLineEdit.Password) + self.confirm_password_input.setObjectName("confirm_password_input") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.confirm_password_input) + self.horizontalLayout_3.addWidget(self.form_wrap) + self.verticalLayout.addWidget(self.widget) + spacerItem = QtWidgets.QSpacerItem(20, 25, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem) + self.password_label.setBuddy(self.password_input) + self.confirm_password_label.setBuddy(self.confirm_password_input) + + self.retranslateUi(Dialog) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + _translate = QtCore.QCoreApplication.translate + Dialog.setWindowTitle(_translate("Dialog", "Create wallet")) + self.helper_text.setText(_translate("Dialog", "Before you can start using DEXBot, you need to create a wallet.")) + self.label.setText(_translate("Dialog", "Wallet password is used to encrypt your BitShares account passwords.")) + self.password_label.setText(_translate("Dialog", "Wallet password")) + self.ok_button.setText(_translate("Dialog", "OK")) + self.confirm_password_label.setText(_translate("Dialog", "Confirm password")) + diff --git a/dexbot/views/gen/notice_window.py b/dexbot/views/gen/notice_window.py new file mode 100644 index 000000000..2d10c0a95 --- /dev/null +++ b/dexbot/views/gen/notice_window.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'dexbot/views/orig/notice_window.ui' +# +# Created by: PyQt5 UI code generator 5.9.2 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName("Dialog") + Dialog.resize(600, 107) + self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) + self.verticalLayout.setObjectName("verticalLayout") + self.notice_label = QtWidgets.QLabel(Dialog) + self.notice_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + self.notice_label.setText("") + self.notice_label.setAlignment(QtCore.Qt.AlignCenter) + self.notice_label.setObjectName("notice_label") + self.verticalLayout.addWidget(self.notice_label) + self.widget = QtWidgets.QWidget(Dialog) + self.widget.setObjectName("widget") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.widget) + self.horizontalLayout.setObjectName("horizontalLayout") + self.ok_button = QtWidgets.QPushButton(self.widget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.ok_button.sizePolicy().hasHeightForWidth()) + self.ok_button.setSizePolicy(sizePolicy) + self.ok_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.ok_button.setObjectName("ok_button") + self.horizontalLayout.addWidget(self.ok_button) + self.verticalLayout.addWidget(self.widget) + + self.retranslateUi(Dialog) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + _translate = QtCore.QCoreApplication.translate + Dialog.setWindowTitle(_translate("Dialog", "Notice")) + self.ok_button.setText(_translate("Dialog", "OK")) + diff --git a/dexbot/views/gen/unlock_wallet_window.py b/dexbot/views/gen/unlock_wallet_window.py new file mode 100644 index 000000000..bcd85935c --- /dev/null +++ b/dexbot/views/gen/unlock_wallet_window.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'dexbot/views/orig/unlock_wallet_window.ui' +# +# Created by: PyQt5 UI code generator 5.9.2 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName("Dialog") + Dialog.resize(473, 141) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(Dialog.sizePolicy().hasHeightForWidth()) + Dialog.setSizePolicy(sizePolicy) + Dialog.setModal(True) + self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) + self.verticalLayout.setObjectName("verticalLayout") + self.helper_text = QtWidgets.QLabel(Dialog) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Maximum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.helper_text.sizePolicy().hasHeightForWidth()) + self.helper_text.setSizePolicy(sizePolicy) + self.helper_text.setAlignment(QtCore.Qt.AlignCenter) + self.helper_text.setObjectName("helper_text") + self.verticalLayout.addWidget(self.helper_text) + self.widget = QtWidgets.QWidget(Dialog) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.widget.sizePolicy().hasHeightForWidth()) + self.widget.setSizePolicy(sizePolicy) + self.widget.setMaximumSize(QtCore.QSize(16777215, 126)) + self.widget.setObjectName("widget") + self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.widget) + self.horizontalLayout_3.setContentsMargins(-1, 5, -1, 5) + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.form_wrap = QtWidgets.QWidget(self.widget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.form_wrap.sizePolicy().hasHeightForWidth()) + self.form_wrap.setSizePolicy(sizePolicy) + self.form_wrap.setObjectName("form_wrap") + self.formLayout = QtWidgets.QFormLayout(self.form_wrap) + self.formLayout.setLabelAlignment(QtCore.Qt.AlignCenter) + self.formLayout.setFormAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignTop) + self.formLayout.setObjectName("formLayout") + self.password_label = QtWidgets.QLabel(self.form_wrap) + self.password_label.setObjectName("password_label") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.password_label) + self.password_input = QtWidgets.QLineEdit(self.form_wrap) + self.password_input.setMinimumSize(QtCore.QSize(200, 0)) + self.password_input.setEchoMode(QtWidgets.QLineEdit.Password) + self.password_input.setObjectName("password_input") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.password_input) + self.ok_button = QtWidgets.QPushButton(self.form_wrap) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.ok_button.sizePolicy().hasHeightForWidth()) + self.ok_button.setSizePolicy(sizePolicy) + self.ok_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.ok_button.setObjectName("ok_button") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.ok_button) + self.horizontalLayout_3.addWidget(self.form_wrap) + self.verticalLayout.addWidget(self.widget) + spacerItem = QtWidgets.QSpacerItem(20, 25, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem) + self.password_label.setBuddy(self.password_input) + + self.retranslateUi(Dialog) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + _translate = QtCore.QCoreApplication.translate + Dialog.setWindowTitle(_translate("Dialog", "Unlock wallet")) + self.helper_text.setText(_translate("Dialog", "Please enter your wallet password before continuing.")) + self.password_label.setText(_translate("Dialog", "Wallet password")) + self.ok_button.setText(_translate("Dialog", "OK")) + diff --git a/dexbot/views/notice.py b/dexbot/views/notice.py new file mode 100644 index 000000000..2c6952797 --- /dev/null +++ b/dexbot/views/notice.py @@ -0,0 +1,14 @@ +from PyQt5 import QtWidgets + +from dexbot.views.gen.notice_window import Ui_Dialog + + +class NoticeDialog(QtWidgets.QDialog): + + def __init__(self, text): + super().__init__() + self.ui = Ui_Dialog() + self.ui.setupUi(self) + + self.ui.notice_label.setText(text) + self.ui.ok_button.clicked.connect(self.accept) diff --git a/dexbot/views/orig/create_wallet_window.ui b/dexbot/views/orig/create_wallet_window.ui new file mode 100644 index 000000000..bcdce3848 --- /dev/null +++ b/dexbot/views/orig/create_wallet_window.ui @@ -0,0 +1,176 @@ + + + Dialog + + + + 0 + 0 + 530 + 196 + + + + + 0 + 0 + + + + Create wallet + + + true + + + + + + + 0 + 0 + + + + Before you can start using DEXBot, you need to create a wallet. + + + Qt::AlignCenter + + + + + + + Wallet password is used to encrypt your BitShares account passwords. + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 16777215 + 126 + + + + + 5 + + + 5 + + + + + + 0 + 0 + + + + + Qt::AlignCenter + + + Qt::AlignHCenter|Qt::AlignTop + + + + + Wallet password + + + password_input + + + + + + + + 200 + 0 + + + + QLineEdit::Password + + + + + + + + 0 + 0 + + + + PointingHandCursor + + + OK + + + + + + + Confirm password + + + confirm_password_input + + + + + + + + 200 + 0 + + + + QLineEdit::Password + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 25 + + + + + + + + + diff --git a/dexbot/views/orig/notice_window.ui b/dexbot/views/orig/notice_window.ui new file mode 100644 index 000000000..562e6c25d --- /dev/null +++ b/dexbot/views/orig/notice_window.ui @@ -0,0 +1,56 @@ + + + Dialog + + + + 0 + 0 + 600 + 107 + + + + Notice + + + + + + ArrowCursor + + + + + + Qt::AlignCenter + + + + + + + + + + + 0 + 0 + + + + PointingHandCursor + + + OK + + + + + + + + + + + diff --git a/dexbot/views/orig/unlock_wallet_window.ui b/dexbot/views/orig/unlock_wallet_window.ui new file mode 100644 index 000000000..a11a262b2 --- /dev/null +++ b/dexbot/views/orig/unlock_wallet_window.ui @@ -0,0 +1,143 @@ + + + Dialog + + + + 0 + 0 + 473 + 141 + + + + + 0 + 0 + + + + Unlock wallet + + + true + + + + + + + 0 + 0 + + + + Please enter your wallet password before continuing. + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 16777215 + 126 + + + + + 5 + + + 5 + + + + + + 0 + 0 + + + + + Qt::AlignCenter + + + Qt::AlignHCenter|Qt::AlignTop + + + + + Wallet password + + + password_input + + + + + + + + 200 + 0 + + + + QLineEdit::Password + + + + + + + + 0 + 0 + + + + PointingHandCursor + + + OK + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 25 + + + + + + + + + diff --git a/dexbot/views/unlock_wallet.py b/dexbot/views/unlock_wallet.py new file mode 100644 index 000000000..e09b4460f --- /dev/null +++ b/dexbot/views/unlock_wallet.py @@ -0,0 +1,23 @@ +from PyQt5 import QtWidgets + +from dexbot.views.gen.unlock_wallet_window import Ui_Dialog +from dexbot.views.notice import NoticeDialog + + +class UnlockWalletView(QtWidgets.QDialog): + + 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) + + def validate_form(self): + password = self.ui.password_input.text() + if not self.controller.unlock_wallet(password): + dialog = NoticeDialog('Invalid password!') + dialog.exec_() + self.ui.password_input.setText('') + else: + self.accept() From cc1457c85bcd6b3b86e606e42990926ee448cef2 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 23 Jan 2018 09:38:53 +0200 Subject: [PATCH 0029/1846] Change bot limit to 1 for now The limit will be removed when multi-bot support is added --- dexbot/controllers/main_controller.py | 1 - dexbot/views/bot_item.py | 6 +++++- dexbot/views/bot_list.py | 10 +++++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 86825c81d..113b7d11d 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -1,4 +1,3 @@ -from dexbot.views.bot_list import MainView from dexbot.bot import BotInfrastructure from ruamel.yaml import YAML diff --git a/dexbot/views/bot_item.py b/dexbot/views/bot_item.py index d9d65f00a..0a45d7d10 100644 --- a/dexbot/views/bot_item.py +++ b/dexbot/views/bot_item.py @@ -5,13 +5,14 @@ class BotItemWidget(QtWidgets.QWidget, Ui_widget): - def __init__(self, botname, config, controller): + def __init__(self, botname, config, controller, view): super(BotItemWidget, self).__init__() self.running = False self.botname = botname self.config = config self.controller = controller + self.view = view self.setupUi(self) self.pause_button.hide() @@ -59,3 +60,6 @@ def remove_widget(self): if self.running: 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) diff --git a/dexbot/views/bot_list.py b/dexbot/views/bot_list.py index 42ebc004e..908d7379b 100644 --- a/dexbot/views/bot_list.py +++ b/dexbot/views/bot_list.py @@ -26,16 +26,24 @@ def __init__(self, main_ctrl): 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) + 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): create_bot_dialog = CreateBotView(self.main_ctrl) return_value = create_bot_dialog.exec_() From 37295d25f5b63b701ea89d887cc4cd49625a4264 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 23 Jan 2018 12:15:43 +0200 Subject: [PATCH 0030/1846] Fix user defined bot module search path --- dexbot/bot.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dexbot/bot.py b/dexbot/bot.py index c44c67aa2..a5e2e5a16 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -32,6 +32,11 @@ def __init__( self.config = config + # 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() @@ -72,11 +77,6 @@ def __init__( bitshares_instance=self.bitshares ) - # set the module search path - userbotpath = os.path.expanduser("~/bots") - if os.path.exists(userbotpath): - sys.path.append(userbotpath) - # Events def on_block(self, data): for botname, bot in self.config["bots"].items(): From ec77f53bda7c6834582c7df40f67a1f146e3b4e5 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 24 Jan 2018 10:06:24 +0200 Subject: [PATCH 0031/1846] Change strategy class names to just "Strategy" to simplify the config file Also add small docstring to the strategy classes describing their funtionality --- dexbot/bot.py | 2 +- dexbot/strategies/echo.py | 6 +++++- dexbot/strategies/storagedemo.py | 6 +++++- dexbot/strategies/walls.py | 7 +++++-- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/dexbot/bot.py b/dexbot/bot.py index a5e2e5a16..044d6532b 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -52,7 +52,7 @@ def __init__( try: klass = getattr( importlib.import_module(bot["module"]), - bot["bot"] + 'Strategy' ) self.bots[botname] = klass( config=config, diff --git a/dexbot/strategies/echo.py b/dexbot/strategies/echo.py index 278306d3b..e940145ed 100644 --- a/dexbot/strategies/echo.py +++ b/dexbot/strategies/echo.py @@ -1,7 +1,11 @@ from dexbot.basestrategy import BaseStrategy -class Echo(BaseStrategy): +class Strategy(BaseStrategy): + """ + Echo strategy + Strategy that logs all events within the blockchain + """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/dexbot/strategies/storagedemo.py b/dexbot/strategies/storagedemo.py index bc8941d73..98677ccf7 100644 --- a/dexbot/strategies/storagedemo.py +++ b/dexbot/strategies/storagedemo.py @@ -1,7 +1,11 @@ from dexbot.basestrategy import BaseStrategy -class StorageDemo(BaseStrategy): +class Strategy(BaseStrategy): + """ + Storage demo strategy + Strategy that prints all new blocks in the blockchain + """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/dexbot/strategies/walls.py b/dexbot/strategies/walls.py index c8c455d32..8a7974391 100644 --- a/dexbot/strategies/walls.py +++ b/dexbot/strategies/walls.py @@ -1,12 +1,15 @@ from math import fabs -from pprint import pprint from collections import Counter from bitshares.amount import Amount from dexbot.basestrategy import BaseStrategy from dexbot.errors import InsufficientFundsError -class Walls(BaseStrategy): +class Strategy(BaseStrategy): + """ + Walls strategy + This strategy simply places a buy and a sell wall + """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) From c33689b5b04a3dfd42df43686d2e35b80acf3af1 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 25 Jan 2018 13:47:13 +0200 Subject: [PATCH 0032/1846] Update python-bitshares --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d963a7780..355c7c6a4 100755 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ ], }, install_requires=[ - "bitshares>=0.1.7", + "bitshares>=0.1.10", "uptick>=0.1.4", "prettytable", "click", From 809cd463b347a91f4e09b62bebd84baff53afe7e Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 25 Jan 2018 13:47:27 +0200 Subject: [PATCH 0033/1846] Remove manual orders strategy --- dexbot/strategies/manualorders.py | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 dexbot/strategies/manualorders.py diff --git a/dexbot/strategies/manualorders.py b/dexbot/strategies/manualorders.py deleted file mode 100644 index f99c9eba2..000000000 --- a/dexbot/strategies/manualorders.py +++ /dev/null @@ -1,11 +0,0 @@ -from dexbot.basestrategy import BaseStrategy -import logging -log = logging.getLogger(__name__) - - -class ManualOrders(BaseStrategy): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # TODO: do the strategy From abcb4c423e523791a6307d88700e1d3c2b0910c4 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Sun, 28 Jan 2018 18:40:30 +1100 Subject: [PATCH 0034/1846] don't log things twce --- dexbot/ui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dexbot/ui.py b/dexbot/ui.py index b5ee0b6bb..d3e5c3682 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -40,6 +40,7 @@ def new_func(ctx, *args, **kwargs): ch.setLevel(getattr(logging, verbosity.upper())) ch.setFormatter(formatter1) logging.getLogger("dexbot").addHandler(ch) + logging.getLogger("").handlers = [] # GrapheneAPI logging if ctx.obj["verbose"] > 4: From e711fd1fc9c17933cc9c6228e493e2c02fee72a6 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Sun, 28 Jan 2018 18:42:42 +1100 Subject: [PATCH 0035/1846] if a bot crashes on initalisation handle this more gracefully exit if no runnable bots Conflicts: dexbot/bot.py --- dexbot/bot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dexbot/bot.py b/dexbot/bot.py index 044d6532b..37f343ae4 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -65,6 +65,9 @@ def __init__( except: 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") + sys.exit(70) # 70= "Software error" in /usr/include/sysexts.h # Create notification instance # Technically, this will multiplex markets and accounts and # we need to demultiplex the events after we have received them @@ -80,7 +83,7 @@ def __init__( # Events def on_block(self, data): for botname, bot in self.config["bots"].items(): - if self.bots[botname].disabled: + if (not botname in self.bots) or self.bots[botname].disabled: continue try: self.bots[botname].ontick(data) From 5a12efe059228fbd834a7d6e43082d9668ca2db5 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Sun, 28 Jan 2018 18:58:09 +1100 Subject: [PATCH 0036/1846] use and exception so more portable to GUI --- dexbot/bot.py | 4 +++- dexbot/cli.py | 11 ++++++++--- dexbot/errors.py | 2 ++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/dexbot/bot.py b/dexbot/bot.py index 37f343ae4..b5509276c 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -7,6 +7,8 @@ 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') @@ -67,7 +69,7 @@ def __init__( if len(markets) == 0: log.critical("No bots to launch, exiting") - sys.exit(70) # 70= "Software error" in /usr/include/sysexts.h + 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 diff --git a/dexbot/cli.py b/dexbot/cli.py index c280f16e5..03be8f35b 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -2,6 +2,7 @@ import yaml import logging import click +import sys from .ui import ( verbose, chain, @@ -13,6 +14,8 @@ alert, ) from dexbot.bot import BotInfrastructure +import dexbot.errors as errors + log = logging.getLogger(__name__) logging.basicConfig( @@ -48,9 +51,11 @@ def main(ctx, **kwargs): def run(ctx): """ Continuously run the bot """ - bot = BotInfrastructure(ctx.config) - bot.run() - + try: + bot = BotInfrastructure(ctx.config) + bot.run() + except errors.NoBotsAvailable: + sys.exit(70) # 70= "Software error" in /usr/include/sysexts.h if __name__ == '__main__': main() diff --git a/dexbot/errors.py b/dexbot/errors.py index c0ecf4352..b56af00ee 100644 --- a/dexbot/errors.py +++ b/dexbot/errors.py @@ -6,3 +6,5 @@ def InsufficientFundsError(amount): log.error( "[InsufficientFunds] Need {}".format(str(amount)) ) + +class NoBotsAvailable(Exception): pass From a9dee09d579c750f6708f22b950346f7a0907c24 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 29 Jan 2018 09:26:48 +0200 Subject: [PATCH 0037/1846] Add clear method to storage --- dexbot/storage.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dexbot/storage.py b/dexbot/storage.py index e247d6084..ec9acfc3f 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -92,6 +92,14 @@ def items(self): ).all() return [(e.key, e.value) for e in es] + def clear(self): + rows = session.query(Config).filter_by( + category=self.category + ) + for row in rows: + session.delete(row) + session.commit() + # Derive sqlite file directory data_dir = user_data_dir(appname, appauthor) From 05e695c7e69030b89d7310a94f7111970da4d0ae Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 29 Jan 2018 09:27:57 +0200 Subject: [PATCH 0038/1846] Add get_order method to basestrategy --- dexbot/basestrategy.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 06e4efb20..5929c4694 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -130,6 +130,12 @@ def orders(self): self.account.refresh() return [o for o in self.account.openorders if self.bot["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 + @property def market(self): """ Return the market object as :class:`bitshares.market.Market` From 851a8fc8ebd09c036dea5e5ea7500cd4f794be29 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 31 Jan 2018 11:00:21 +0200 Subject: [PATCH 0039/1846] Add order cancel method Also rename cancelall method to cancel_all --- dexbot/basestrategy.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 5929c4694..f91d60864 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -182,7 +182,15 @@ def execute(self): self.bitshares.blocking = False return r - def cancelall(self): + def cancel(self, order_id): + """ Cancel specific order + """ + return self.bitshares.cancel( + order_id, + account=self.account + ) + + def cancel_all(self): """ Cancel all orders of this bot """ if self.orders: From 1235aaaff790b80862d3e2237edced976caa3d13 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 1 Feb 2018 08:52:06 +0200 Subject: [PATCH 0040/1846] Change the cancel method --- dexbot/basestrategy.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index f91d60864..70f51b71c 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -182,11 +182,13 @@ def execute(self): self.bitshares.blocking = False return r - def cancel(self, order_id): - """ Cancel specific order + def cancel(self, orders): + """ Cancel specific orders """ + if not isinstance(orders, list): + orders = [orders] return self.bitshares.cancel( - order_id, + [o["id"] for o in orders if "id" in o], account=self.account ) From 8516956326be0950218f76eab6f93950bc7f08fb Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 1 Feb 2018 08:58:06 +0200 Subject: [PATCH 0041/1846] Add simple strategy Adds strategy with walls like logic. The base logic is there, but there are still likely some inaccurate calculations. --- dexbot/basestrategy.py | 49 +++++++++ dexbot/strategies/simple.py | 213 ++++++++++++++++++++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 dexbot/strategies/simple.py diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 70f51b71c..602072e11 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -1,5 +1,6 @@ import logging from events import Events +from bitshares.asset import Asset from bitshares.market import Market from bitshares.account import Account from bitshares.price import FilledOrder, Order, UpdateCallOrder @@ -136,6 +137,38 @@ def get_order(self, order_id): return order return False + def get_updated_order(self, order): + if not order: + return False + for updated_order in self.updated_open_orders: + if updated_order['id'] == order['id']: + return updated_order + return False + + @property + def updated_open_orders(self): + """ + Returns updated open Orders. + account.openorders doesn't return updated values for the order so we calculate the values manually + """ + self.account.refresh() + self.account.ensure_full() + + 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'] + quote_amount = base_amount / price + o['sell_price']['base']['amount'] = base_amount + o['sell_price']['quote']['amount'] = quote_amount + + orders = [ + Order(o, bitshares_instance=self.bitshares) + for o in limit_orders + ] + + return [o for o in orders if self.bot["market"] == o.market] + @property def market(self): """ Return the market object as :class:`bitshares.market.Market` @@ -155,6 +188,22 @@ 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" + @property def balances(self): """ Return the balances of your bot's account diff --git a/dexbot/strategies/simple.py b/dexbot/strategies/simple.py new file mode 100644 index 000000000..ac7edd681 --- /dev/null +++ b/dexbot/strategies/simple.py @@ -0,0 +1,213 @@ +from collections import Counter + +from bitshares.amount import Amount +from bitshares.price import Price + +from dexbot.basestrategy import BaseStrategy +from dexbot.errors import InsufficientFundsError + + +class Strategy(BaseStrategy): + """ + Simple strategy + This strategy places a buy and a sell wall that change height over time + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Define Callbacks + self.onMarketUpdate += self.test + self.ontick += self.tick + + self.error_ontick = self.error + self.error_onMarketUpdate = self.error + self.error_onAccount = self.error + + # Counter for blocks + self.counter = Counter() + + # Tests for actions + self.price = self.bot.get("target", {}).get("center_price", 0) + self.cancel_all() + self.clear() + + def error(self, *args, **kwargs): + self.disabled = True + self.cancel_all() + self.clear() + self.log.info(self.execute()) + + def init_strategy(self): + # Target + target = self.bot.get("target", {}) + + # prices + buy_price = self.price * (1 - (target["spread"] / 2) / 100) + sell_price = self.price * (1 + (target["spread"] / 2) / 100) + + amount = target['amount'] / 2 + + # Buy Side + if float(self.balance(self.market["base"])) < buy_price * amount: + InsufficientFundsError(Amount(amount=amount * float(buy_price), asset=self.market["base"])) + else: + buy_transaction = self.market.buy( + buy_price, + Amount(amount=amount, asset=self.market["quote"]), + account=self.account, + returnOrderId="head" + ) + buy_order = self.get_order(buy_transaction['orderid']) + if buy_order: + self['buy_order'] = buy_order + + # Sell Side + if float(self.balance(self.market["quote"])) < sell_price * amount: + InsufficientFundsError(Amount(amount=amount * float(sell_price), asset=self.market["quote"])) + else: + sell_transaction = self.market.sell( + sell_price, + Amount(amount=amount, asset=self.market["quote"]), + account=self.account, + returnOrderId="head" + ) + sell_order = self.get_order(sell_transaction['orderid']) + if sell_order: + self['sell_order'] = sell_order + + self['initial_balance'] = self.orders_balance() + + def update_orders(self, new_sell_order, new_buy_order): + """ + Update the orders + """ + print('Updating orders!') + target = self.bot.get("target", {}) + + # Stored orders + sell_order = self['sell_order'] + buy_order = self['buy_order'] + + # prices + buy_price = self.price * (1 - (target["spread"] / 2) / 100) + sell_price = self.price * (1 + (target["spread"] / 2) / 100) + + 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['quote']['amount'] - new_sell_order['quote']['amount'] + elif not new_sell_order and sell_order: + # All of the sell order was sold + sold_amount = sell_order['quote']['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 + try: + buy_order_amount = buy_order['quote']['amount'] + except KeyError: + buy_order_amount = 0 + new_buy_amount = buy_order_amount - bought_amount + sold_amount + if float(self.balance(self.market["base"])) < new_buy_amount: + InsufficientFundsError(Amount(amount=new_buy_amount, asset=self.market["base"])) + else: + if buy_order: + # Cancel the old order + self.cancel(buy_order) + + buy_transaction = self.market.buy( + buy_price, + Amount(amount=new_buy_amount, asset=self.market["quote"]), + account=self.account, + returnOrderId="head" + ) + buy_order = self.get_order(buy_transaction['orderid']) + 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 + try: + sell_order_amount = sell_order['base']['amount'] + except KeyError: + sell_order_amount = 0 + new_sell_amount = sell_order_amount + bought_amount - sold_amount + if float(self.balance(self.market["quote"])) < new_sell_amount: + InsufficientFundsError(Amount(amount=new_sell_amount, asset=self.market["quote"])) + else: + if sell_order: + # Cancel the old order + self.cancel(sell_order) + + sell_transaction = self.market.sell( + sell_price, + Amount(amount=new_sell_amount, asset=self.market["quote"]), + account=self.account, + returnOrderId="head" + ) + sell_order = self.get_order(sell_transaction['orderid']) + 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 + for order in [self['buy_order'], self['sell_order']]: + if order: + 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']) + order.invert() + balance += self.get_converted_asset_amount(order['quote']) + + 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 + + def test(self, *args, **kwargs): + """ + 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']) + + # 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'] + + 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) + + self.update_gui_profit() + + # GUI updaters + def update_gui_profit(self): + # Todo: update gui profit + profit = (self.orders_balance() - self['initial_balance']) / self['initial_balance'] + print(profit) From a09f1fa9005f5ab942c07f7f5d7b7525d8bbacfa Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 1 Feb 2018 09:26:54 +0200 Subject: [PATCH 0042/1846] Change text in wallet creation window --- dexbot/views/gen/create_wallet_window.py | 2 +- dexbot/views/orig/create_wallet_window.ui | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/views/gen/create_wallet_window.py b/dexbot/views/gen/create_wallet_window.py index 970533767..8cba44efd 100644 --- a/dexbot/views/gen/create_wallet_window.py +++ b/dexbot/views/gen/create_wallet_window.py @@ -94,7 +94,7 @@ def retranslateUi(self, Dialog): _translate = QtCore.QCoreApplication.translate Dialog.setWindowTitle(_translate("Dialog", "Create wallet")) self.helper_text.setText(_translate("Dialog", "Before you can start using DEXBot, you need to create a wallet.")) - self.label.setText(_translate("Dialog", "Wallet password is used to encrypt your BitShares account passwords.")) + self.label.setText(_translate("Dialog", "Wallet password is used to encrypt your BitShares account private keys.")) self.password_label.setText(_translate("Dialog", "Wallet password")) self.ok_button.setText(_translate("Dialog", "OK")) self.confirm_password_label.setText(_translate("Dialog", "Confirm password")) diff --git a/dexbot/views/orig/create_wallet_window.ui b/dexbot/views/orig/create_wallet_window.ui index bcdce3848..4a22f3ead 100644 --- a/dexbot/views/orig/create_wallet_window.ui +++ b/dexbot/views/orig/create_wallet_window.ui @@ -42,7 +42,7 @@ - Wallet password is used to encrypt your BitShares account passwords. + Wallet password is used to encrypt your BitShares account private keys. Qt::AlignCenter From 4a3e1daffaacda8a55e1d838ba6889330415c785 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 1 Feb 2018 15:34:33 +0200 Subject: [PATCH 0043/1846] Add bot creation GUI elements (WIP) --- dexbot/controllers/create_bot_controller.py | 73 +++ dexbot/controllers/main_controller.py | 25 - dexbot/views/bot_list.py | 7 +- dexbot/views/create_bot.py | 76 +++- dexbot/views/gen/create_bot_window.py | 222 ++++++++- dexbot/views/orig/create_bot_window.ui | 481 ++++++++++++++++++-- 6 files changed, 785 insertions(+), 99 deletions(-) create mode 100644 dexbot/controllers/create_bot_controller.py diff --git a/dexbot/controllers/create_bot_controller.py b/dexbot/controllers/create_bot_controller.py new file mode 100644 index 000000000..0789dd87d --- /dev/null +++ b/dexbot/controllers/create_bot_controller.py @@ -0,0 +1,73 @@ +from dexbot.controllers.main_controller import MainController + +import bitshares +from bitshares.instance import shared_bitshares_instance +from bitshares.asset import Asset +from bitshares.account import Account +from ruamel.yaml import YAML + + +class CreateBotController: + + def __init__(self, bitshares_instance): + self.bitshares = bitshares_instance or shared_bitshares_instance() + + @property + def strategies(self): + strategies = { + 'Simple Strategy': 'dexbot.strategies.simple' + } + return strategies + + def get_strategy_module(self, strategy): + return self.strategies[strategy] + + @staticmethod + def is_bot_name_valid(bot_name): + bot_names = MainController.get_bots_data().keys() + if bot_name in bot_names: + return False + return True + + def is_asset_valid(self, asset): + try: + Asset(asset, bitshares_instance=self.bitshares) + return True + except bitshares.exceptions.AssetDoesNotExistsException: + return False + + def account_exists(self, account): + try: + Account(account, bitshares_instance=self.bitshares) + return True + except bitshares.exceptions.AccountDoesNotExistsException: + return False + + def is_account_valid(self, account, private_key): + # Todo: finish this + return True + + @staticmethod + def get_unique_bot_name(): + """ + Returns unique bot name "Bot %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) + 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) \ No newline at end of file diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 113b7d11d..b54ca7dd3 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -33,31 +33,6 @@ def load_config(): with open('config.yml', 'r') as f: return yaml.load(f) - @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) - - @staticmethod - def get_unique_bot_name(): - """ - Returns unique bot name "Bot %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) - index += 1 - - return botname - @staticmethod def get_bots_data(): """ diff --git a/dexbot/views/bot_list.py b/dexbot/views/bot_list.py index 908d7379b..70658984d 100644 --- a/dexbot/views/bot_list.py +++ b/dexbot/views/bot_list.py @@ -3,7 +3,7 @@ from dexbot.queue.queue_dispatcher import ThreadDispatcher from dexbot.views.gen.bot_list_window import Ui_MainWindow from dexbot.views.create_bot import CreateBotView - +from dexbot.controllers.create_bot_controller import CreateBotController from dexbot.views.bot_item import BotItemWidget @@ -45,12 +45,13 @@ def add_bot_widget(self, botname, config): self.ui.add_bot_button.setEnabled(False) def handle_add_bot(self): - create_bot_dialog = CreateBotView(self.main_ctrl) + controller = CreateBotController(self.main_ctrl.bitshares_instance) + create_bot_dialog = CreateBotView(controller) return_value = create_bot_dialog.exec_() # User clicked save if return_value == 1: - botname = create_bot_dialog.botname + botname = create_bot_dialog.bot_name config = self.main_ctrl.get_bot_config(botname) self.add_bot_widget(botname, config) diff --git a/dexbot/views/create_bot.py b/dexbot/views/create_bot.py index 6fe66add3..792da387e 100644 --- a/dexbot/views/create_bot.py +++ b/dexbot/views/create_bot.py @@ -1,37 +1,89 @@ -from PyQt5 import QtGui, QtWidgets, QtCore +from PyQt5 import QtWidgets +from dexbot.views.notice import NoticeDialog from dexbot.views.gen.create_bot_window import Ui_Dialog class CreateBotView(QtWidgets.QDialog): def __init__(self, controller): + super().__init__() self.controller = controller - super(CreateBotView, self).__init__() + self.ui = Ui_Dialog() self.ui.setupUi(self) - botname = controller.get_unique_bot_name() - self.ui.botname_input.setText(botname) + # Todo: Using a model here would be more Qt like + self.ui.strategy_input.addItems(self.controller.strategies) + + self.bot_name = controller.get_unique_bot_name() + self.ui.bot_name_input.setText(self.bot_name) self.ui.save_button.clicked.connect(self.handle_save) self.ui.cancel_button.clicked.connect(self.handle_cancel) + def validate_bot_name(self): + bot_name = self.ui.bot_name_input.text() + return self.controller.is_bot_name_valid(bot_name) + + def validate_asset(self, asset): + return self.controller.is_asset_valid(asset) + + def validate_market(self): + base_asset = self.ui.base_asset_input.text() + 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) + def validate_form(self): - return True + error_text = '' + base_asset = self.ui.base_asset_input.text() + 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) + elif not self.validate_asset(base_asset): + error_text = 'Field "Base Asset" does not have a valid asset.' + elif not self.validate_asset(quote_asset): + error_text = 'Field "Base Quote" does not have a valid asset.' + elif not self.validate_market(): + error_text = "Market {}/{} doesn't exist.".format(base_asset, quote_asset) + elif not self.validate_account_name(): + error_text = "Account doesn't exist." + elif not self.validate_account(): + error_text = 'Private key is invalid.' + + if error_text: + dialog = NoticeDialog(error_text) + dialog.exec_() + return False + else: + return True def handle_save(self): if not self.validate_form(): - # Todo: add validation error notice for user return - self.botname = self.ui.botname_input.text() + + self.bot_name = self.ui.bot_name_input.text() + account = self.ui.account_input.text() + market = '{}:{}'.format(self.ui.base_asset_input, self.ui.quote_asset_input) + strategy = self.ui.strategy_input.currentText() + bot_module = self.controller.get_strategy_module(strategy) bot_data = { - 'account': self.ui.account_input.text(), - 'market': '', - 'module': '', - 'bot': '' + 'account': account, + 'market': market, + 'module': bot_module, + 'strategy': strategy } - self.controller.add_bot_config(self.botname, bot_data) + self.controller.add_bot_config(self.bot_name, bot_data) self.accept() def handle_cancel(self): diff --git a/dexbot/views/gen/create_bot_window.py b/dexbot/views/gen/create_bot_window.py index 021ac7ca6..081da8ea3 100644 --- a/dexbot/views/gen/create_bot_window.py +++ b/dexbot/views/gen/create_bot_window.py @@ -11,7 +11,7 @@ class Ui_Dialog(object): def setupUi(self, Dialog): Dialog.setObjectName("Dialog") - Dialog.resize(400, 300) + Dialog.resize(418, 474) Dialog.setModal(True) self.gridLayout = QtWidgets.QGridLayout(Dialog) self.gridLayout.setObjectName("gridLayout") @@ -32,6 +32,7 @@ def setupUi(self, Dialog): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.cancel_button.sizePolicy().hasHeightForWidth()) self.cancel_button.setSizePolicy(sizePolicy) + self.cancel_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) self.cancel_button.setObjectName("cancel_button") self.horizontalLayout_2.addWidget(self.cancel_button) self.save_button = QtWidgets.QPushButton(self.widget) @@ -40,40 +41,215 @@ def setupUi(self, Dialog): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.save_button.sizePolicy().hasHeightForWidth()) self.save_button.setSizePolicy(sizePolicy) + self.save_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) self.save_button.setObjectName("save_button") self.horizontalLayout_2.addWidget(self.save_button) - self.gridLayout.addWidget(self.widget, 1, 0, 1, 1) - self.formLayout = QtWidgets.QFormLayout() + self.gridLayout.addWidget(self.widget, 4, 0, 1, 1) + self.groupBox_3 = QtWidgets.QGroupBox(Dialog) + self.groupBox_3.setObjectName("groupBox_3") + self.formLayout = QtWidgets.QFormLayout(self.groupBox_3) self.formLayout.setObjectName("formLayout") - self.strategy_label = QtWidgets.QLabel(Dialog) - self.strategy_label.setObjectName("strategy_label") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.strategy_label) - self.strategy_input = QtWidgets.QComboBox(Dialog) - self.strategy_input.setObjectName("strategy_input") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.strategy_input) - self.account_label = QtWidgets.QLabel(Dialog) + self.amount_label = QtWidgets.QLabel(self.groupBox_3) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.amount_label.sizePolicy().hasHeightForWidth()) + self.amount_label.setSizePolicy(sizePolicy) + self.amount_label.setMinimumSize(QtCore.QSize(110, 0)) + self.amount_label.setMaximumSize(QtCore.QSize(110, 16777215)) + self.amount_label.setObjectName("amount_label") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.amount_label) + self.amount_input = QtWidgets.QDoubleSpinBox(self.groupBox_3) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.amount_input.sizePolicy().hasHeightForWidth()) + self.amount_input.setSizePolicy(sizePolicy) + self.amount_input.setMinimumSize(QtCore.QSize(140, 0)) + self.amount_input.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) + self.amount_input.setDecimals(5) + self.amount_input.setMaximum(999999999.999) + self.amount_input.setObjectName("amount_input") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.amount_input) + self.center_price_label = QtWidgets.QLabel(self.groupBox_3) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.center_price_label.sizePolicy().hasHeightForWidth()) + self.center_price_label.setSizePolicy(sizePolicy) + self.center_price_label.setMinimumSize(QtCore.QSize(110, 0)) + self.center_price_label.setMaximumSize(QtCore.QSize(110, 16777215)) + self.center_price_label.setObjectName("center_price_label") + self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.center_price_label) + self.center_price_input = QtWidgets.QDoubleSpinBox(self.groupBox_3) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.center_price_input.sizePolicy().hasHeightForWidth()) + self.center_price_input.setSizePolicy(sizePolicy) + self.center_price_input.setMinimumSize(QtCore.QSize(140, 0)) + self.center_price_input.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) + self.center_price_input.setAccelerated(False) + self.center_price_input.setProperty("showGroupSeparator", False) + self.center_price_input.setDecimals(5) + self.center_price_input.setMinimum(-999999999.999) + self.center_price_input.setMaximum(999999999.999) + self.center_price_input.setObjectName("center_price_input") + self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.center_price_input) + self.spread_label = QtWidgets.QLabel(self.groupBox_3) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.spread_label.sizePolicy().hasHeightForWidth()) + self.spread_label.setSizePolicy(sizePolicy) + self.spread_label.setMinimumSize(QtCore.QSize(110, 0)) + self.spread_label.setMaximumSize(QtCore.QSize(110, 16777215)) + self.spread_label.setObjectName("spread_label") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.spread_label) + self.spread_input = QtWidgets.QDoubleSpinBox(self.groupBox_3) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.spread_input.sizePolicy().hasHeightForWidth()) + self.spread_input.setSizePolicy(sizePolicy) + self.spread_input.setMinimumSize(QtCore.QSize(140, 0)) + self.spread_input.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) + self.spread_input.setMaximum(100000.0) + self.spread_input.setProperty("value", 5.0) + self.spread_input.setObjectName("spread_input") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.spread_input) + self.gridLayout.addWidget(self.groupBox_3, 2, 0, 1, 1) + self.groupBox_2 = QtWidgets.QGroupBox(Dialog) + self.groupBox_2.setObjectName("groupBox_2") + self.formLayout_2 = QtWidgets.QFormLayout(self.groupBox_2) + self.formLayout_2.setRowWrapPolicy(QtWidgets.QFormLayout.WrapLongRows) + self.formLayout_2.setObjectName("formLayout_2") + self.account_label = QtWidgets.QLabel(self.groupBox_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.account_label.sizePolicy().hasHeightForWidth()) + self.account_label.setSizePolicy(sizePolicy) + self.account_label.setMinimumSize(QtCore.QSize(110, 0)) + self.account_label.setMaximumSize(QtCore.QSize(110, 16777215)) self.account_label.setObjectName("account_label") - self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.account_label) - self.account_input = QtWidgets.QLineEdit(Dialog) + self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.account_label) + self.account_input = QtWidgets.QLineEdit(self.groupBox_2) self.account_input.setObjectName("account_input") - self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.account_input) - self.botname_label = QtWidgets.QLabel(Dialog) - self.botname_label.setObjectName("botname_label") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.botname_label) - self.botname_input = QtWidgets.QLineEdit(Dialog) - self.botname_input.setObjectName("botname_input") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.botname_input) - self.gridLayout.addLayout(self.formLayout, 0, 0, 1, 1) + self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.account_input) + self.private_key_label = QtWidgets.QLabel(self.groupBox_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.private_key_label.sizePolicy().hasHeightForWidth()) + self.private_key_label.setSizePolicy(sizePolicy) + self.private_key_label.setMinimumSize(QtCore.QSize(110, 0)) + self.private_key_label.setMaximumSize(QtCore.QSize(110, 16777215)) + self.private_key_label.setScaledContents(False) + self.private_key_label.setWordWrap(True) + self.private_key_label.setObjectName("private_key_label") + self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.private_key_label) + self.private_key_input = QtWidgets.QLineEdit(self.groupBox_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.private_key_input.sizePolicy().hasHeightForWidth()) + self.private_key_input.setSizePolicy(sizePolicy) + self.private_key_input.setEchoMode(QtWidgets.QLineEdit.Password) + self.private_key_input.setClearButtonEnabled(False) + self.private_key_input.setObjectName("private_key_input") + self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.private_key_input) + self.gridLayout.addWidget(self.groupBox_2, 1, 0, 1, 1) + self.groupBox = QtWidgets.QGroupBox(Dialog) + self.groupBox.setObjectName("groupBox") + self.formLayout_3 = QtWidgets.QFormLayout(self.groupBox) + self.formLayout_3.setObjectName("formLayout_3") + self.strategy_label = QtWidgets.QLabel(self.groupBox) + self.strategy_label.setMinimumSize(QtCore.QSize(110, 0)) + self.strategy_label.setMaximumSize(QtCore.QSize(110, 16777215)) + self.strategy_label.setObjectName("strategy_label") + self.formLayout_3.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.strategy_label) + self.strategy_input = QtWidgets.QComboBox(self.groupBox) + self.strategy_input.setObjectName("strategy_input") + self.formLayout_3.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.strategy_input) + self.bot_name_label = QtWidgets.QLabel(self.groupBox) + self.bot_name_label.setMinimumSize(QtCore.QSize(110, 0)) + self.bot_name_label.setMaximumSize(QtCore.QSize(110, 16777215)) + self.bot_name_label.setObjectName("bot_name_label") + self.formLayout_3.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.bot_name_label) + self.bot_name_input = QtWidgets.QLineEdit(self.groupBox) + self.bot_name_input.setObjectName("bot_name_input") + self.formLayout_3.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.bot_name_input) + self.base_asset_label = QtWidgets.QLabel(self.groupBox) + self.base_asset_label.setMinimumSize(QtCore.QSize(110, 0)) + self.base_asset_label.setMaximumSize(QtCore.QSize(110, 16777215)) + self.base_asset_label.setObjectName("base_asset_label") + self.formLayout_3.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.base_asset_label) + self.base_asset_input = QtWidgets.QLineEdit(self.groupBox) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.base_asset_input.sizePolicy().hasHeightForWidth()) + self.base_asset_input.setSizePolicy(sizePolicy) + self.base_asset_input.setMaximumSize(QtCore.QSize(80, 16777215)) + self.base_asset_input.setInputMethodHints(QtCore.Qt.ImhPreferUppercase) + self.base_asset_input.setObjectName("base_asset_input") + self.formLayout_3.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.base_asset_input) + self.quote_asset_label = QtWidgets.QLabel(self.groupBox) + self.quote_asset_label.setMinimumSize(QtCore.QSize(110, 0)) + self.quote_asset_label.setMaximumSize(QtCore.QSize(110, 16777215)) + self.quote_asset_label.setObjectName("quote_asset_label") + self.formLayout_3.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.quote_asset_label) + self.quote_asset_input = QtWidgets.QLineEdit(self.groupBox) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.quote_asset_input.sizePolicy().hasHeightForWidth()) + self.quote_asset_input.setSizePolicy(sizePolicy) + self.quote_asset_input.setMaximumSize(QtCore.QSize(80, 16777215)) + self.quote_asset_input.setObjectName("quote_asset_input") + self.formLayout_3.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.quote_asset_input) + self.strategy_label.raise_() + self.strategy_input.raise_() + self.bot_name_label.raise_() + self.bot_name_input.raise_() + self.base_asset_label.raise_() + self.base_asset_input.raise_() + self.quote_asset_label.raise_() + self.quote_asset_input.raise_() + self.gridLayout.addWidget(self.groupBox, 0, 0, 1, 1) + spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout.addItem(spacerItem1, 3, 0, 1, 1) + self.amount_label.setBuddy(self.amount_input) + self.center_price_label.setBuddy(self.center_price_input) + self.spread_label.setBuddy(self.spread_input) + self.account_label.setBuddy(self.account_input) + self.private_key_label.setBuddy(self.private_key_input) + self.strategy_label.setBuddy(self.strategy_input) + self.bot_name_label.setBuddy(self.bot_name_input) + self.base_asset_label.setBuddy(self.base_asset_input) + self.quote_asset_label.setBuddy(self.quote_asset_input) self.retranslateUi(Dialog) + self.strategy_input.setCurrentIndex(-1) QtCore.QMetaObject.connectSlotsByName(Dialog) def retranslateUi(self, Dialog): _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "Create Bot")) + Dialog.setWindowTitle(_translate("Dialog", "DEXBot - Create Bot")) self.cancel_button.setText(_translate("Dialog", "Cancel")) self.save_button.setText(_translate("Dialog", "Save")) - self.strategy_label.setText(_translate("Dialog", "Strategy")) + self.groupBox_3.setTitle(_translate("Dialog", "Bot Parameters")) + self.amount_label.setText(_translate("Dialog", "Amount")) + self.center_price_label.setText(_translate("Dialog", "Center Price")) + self.spread_label.setText(_translate("Dialog", "Spread")) + self.spread_input.setSuffix(_translate("Dialog", "%")) + self.groupBox_2.setTitle(_translate("Dialog", "Bitshares Account Details")) self.account_label.setText(_translate("Dialog", "Account")) - self.botname_label.setText(_translate("Dialog", "Bot name")) + self.private_key_label.setText(_translate("Dialog", "Private Active Key")) + self.groupBox.setTitle(_translate("Dialog", "Bot Details")) + self.strategy_label.setText(_translate("Dialog", "Strategy")) + self.bot_name_label.setText(_translate("Dialog", "Bot Name")) + self.base_asset_label.setText(_translate("Dialog", "Base Asset")) + self.quote_asset_label.setText(_translate("Dialog", "Quote Asset")) diff --git a/dexbot/views/orig/create_bot_window.ui b/dexbot/views/orig/create_bot_window.ui index cdf65ad6c..f98feb6cb 100644 --- a/dexbot/views/orig/create_bot_window.ui +++ b/dexbot/views/orig/create_bot_window.ui @@ -6,18 +6,18 @@ 0 0 - 400 - 300 + 418 + 474 - Create Bot + DEXBot - Create Bot true - + @@ -47,6 +47,9 @@ 0 + + PointingHandCursor + Cancel @@ -60,6 +63,9 @@ 0 + + PointingHandCursor + Save @@ -68,39 +74,442 @@ + + + + Bot Parameters + + + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Amount + + + amount_input + + + + + + + + 0 + 0 + + + + + 140 + 0 + + + + + + + 5 + + + 999999999.998999953269958 + + + + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Center Price + + + center_price_input + + + + + + + + 0 + 0 + + + + + 140 + 0 + + + + + + + false + + + false + + + 5 + + + -999999999.998999953269958 + + + 999999999.998999953269958 + + + + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Spread + + + spread_input + + + + + + + + 0 + 0 + + + + + 140 + 0 + + + + + + + % + + + 100000.000000000000000 + + + 5.000000000000000 + + + + + + + + + + Bitshares Account Details + + + + QFormLayout::WrapLongRows + + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Account + + + account_input + + + + + + + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Private Active Key + + + false + + + true + + + private_key_input + + + + + + + + 0 + 0 + + + + QLineEdit::Password + + + false + + + + + + - - - - - Strategy - - - - - - - - - - Account - - - - - - - - - - Bot name - - - - - - - + + + Bot Details + + + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Strategy + + + strategy_input + + + + + + + -1 + + + + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Bot Name + + + bot_name_input + + + + + + + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Base Asset + + + base_asset_input + + + + + + + + 0 + 0 + + + + + 80 + 16777215 + + + + Qt::ImhPreferUppercase + + + + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Quote Asset + + + quote_asset_input + + + + + + + + 0 + 0 + + + + + 80 + 16777215 + + + + + + + strategy_label + strategy_input + bot_name_label + bot_name_input + base_asset_label + base_asset_input + quote_asset_label + quote_asset_input + + + + + + Qt::Vertical + + + + 20 + 40 + + + From c2105cc8bbd2cdad3ccd4147ae429fa3c3f736d3 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 2 Feb 2018 08:12:48 +0200 Subject: [PATCH 0044/1846] Fix tab order of create bot window --- dexbot/views/gen/create_bot_window.py | 10 ++++++++++ dexbot/views/orig/create_bot_window.ui | 13 +++++++++++++ 2 files changed, 23 insertions(+) diff --git a/dexbot/views/gen/create_bot_window.py b/dexbot/views/gen/create_bot_window.py index 081da8ea3..55fd34401 100644 --- a/dexbot/views/gen/create_bot_window.py +++ b/dexbot/views/gen/create_bot_window.py @@ -233,6 +233,16 @@ def setupUi(self, Dialog): self.retranslateUi(Dialog) self.strategy_input.setCurrentIndex(-1) QtCore.QMetaObject.connectSlotsByName(Dialog) + Dialog.setTabOrder(self.strategy_input, self.bot_name_input) + Dialog.setTabOrder(self.bot_name_input, self.base_asset_input) + Dialog.setTabOrder(self.base_asset_input, self.quote_asset_input) + Dialog.setTabOrder(self.quote_asset_input, self.account_input) + Dialog.setTabOrder(self.account_input, self.private_key_input) + Dialog.setTabOrder(self.private_key_input, self.amount_input) + Dialog.setTabOrder(self.amount_input, self.center_price_input) + Dialog.setTabOrder(self.center_price_input, self.spread_input) + Dialog.setTabOrder(self.spread_input, self.save_button) + Dialog.setTabOrder(self.save_button, self.cancel_button) def retranslateUi(self, Dialog): _translate = QtCore.QCoreApplication.translate diff --git a/dexbot/views/orig/create_bot_window.ui b/dexbot/views/orig/create_bot_window.ui index f98feb6cb..f47ac1294 100644 --- a/dexbot/views/orig/create_bot_window.ui +++ b/dexbot/views/orig/create_bot_window.ui @@ -513,6 +513,19 @@ + + strategy_input + bot_name_input + base_asset_input + quote_asset_input + account_input + private_key_input + amount_input + center_price_input + spread_input + save_button + cancel_button + From 1fcd259531435dd46cb3503ad963a972fc593c9f Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 2 Feb 2018 10:49:01 +0200 Subject: [PATCH 0045/1846] Add the ability to remove bots with GUI This also adds confirmation dialogs --- dexbot/basestrategy.py | 7 ++ dexbot/bot.py | 8 ++ dexbot/controllers/create_bot_controller.py | 2 +- dexbot/controllers/main_controller.py | 27 ++++- dexbot/views/bot_item.py | 11 +- dexbot/views/confirmation.py | 13 +++ dexbot/views/gen/confirmation_window.py | 49 ++++++++ dexbot/views/orig/confirmation_window.ui | 118 ++++++++++++++++++++ 8 files changed, 225 insertions(+), 10 deletions(-) create mode 100644 dexbot/views/confirmation.py create mode 100644 dexbot/views/gen/confirmation_window.py create mode 100644 dexbot/views/orig/confirmation_window.ui diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 602072e11..885ed81e5 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -249,3 +249,10 @@ def cancel_all(self): [o["id"] for o in self.orders], account=self.account ) + + def purge(self): + """ + Clear all the bot data from the database and cancel all orders + """ + self.cancel_all() + self.clear() diff --git a/dexbot/bot.py b/dexbot/bot.py index 044d6532b..7f9ff0649 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -4,6 +4,8 @@ import os.path from multiprocessing import Process +from dexbot.basestrategy import BaseStrategy + from bitshares.notify import Notify from bitshares.instance import shared_bitshares_instance @@ -117,3 +119,9 @@ def on_account(self, accountupdate): def run(self): self.notify.listen() + + @staticmethod + def remove_bot(config, bot_name): + # Initialize the base strategy to get control over the data + strategy = BaseStrategy(config, bot_name) + strategy.purge() diff --git a/dexbot/controllers/create_bot_controller.py b/dexbot/controllers/create_bot_controller.py index 0789dd87d..5ad1a91d3 100644 --- a/dexbot/controllers/create_bot_controller.py +++ b/dexbot/controllers/create_bot_controller.py @@ -70,4 +70,4 @@ def add_bot_config(botname, bot_data): config['bots'][botname] = bot_data with open("config.yml", "w") as f: - yaml.dump(config, f) \ No newline at end of file + yaml.dump(config, f) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index b54ca7dd3..3eefd3cbe 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -20,12 +20,18 @@ def create_bot(self, botname, config): bot.start() self.bots[botname] = bot - def stop_bot(self, bot_id): - self.bots[bot_id].terminate() + def stop_bot(self, bot_name): + self.bots[bot_name].terminate() + self.bots.pop(bot_name, None) - def remove_bot(self, botname): - # Todo: cancell all orders on removal - self.bots[botname].terminate() + def remove_bot(self, bot_name): + if bot_name in self.bots: + self.bots[bot_name].terminate() + + # Todo: Add some threading here so that the GUI doesn't freeze + config = self.get_bot_config(bot_name) + self.bot_template.remove_bot(config, bot_name) + self.remove_bot_config(bot_name) @staticmethod def load_config(): @@ -64,3 +70,14 @@ def get_bot_config(botname): config = yaml.load(f) config['bots'] = {botname: config['bots'][botname]} return config + + @staticmethod + def remove_bot_config(bot_name): + yaml = YAML() + with open('config.yml', 'r') as f: + config = yaml.load(f) + + config['bots'].pop(bot_name, None) + + with open("config.yml", "w") as f: + yaml.dump(config, f) diff --git a/dexbot/views/bot_item.py b/dexbot/views/bot_item.py index 0a45d7d10..58280d703 100644 --- a/dexbot/views/bot_item.py +++ b/dexbot/views/bot_item.py @@ -1,6 +1,7 @@ from PyQt5 import QtWidgets from dexbot.views.gen.bot_item_widget import Ui_widget +from dexbot.views.confirmation import ConfirmationDialog class BotItemWidget(QtWidgets.QWidget, Ui_widget): @@ -57,9 +58,11 @@ def set_bot_profit(self, value): self.bot_profit.setText(value) def remove_widget(self): - if self.running: + dialog = ConfirmationDialog('Are you sure you want to remove bot "{}"?'.format(self.botname)) + return_value = dialog.exec_() + if return_value: self.controller.remove_bot(self.botname) - self.deleteLater() + self.deleteLater() - # Todo: Remove the line below this after multi-bot support is added - self.view.ui.add_bot_button.setEnabled(True) + # Todo: Remove the line below this after multi-bot support is added + self.view.ui.add_bot_button.setEnabled(True) diff --git a/dexbot/views/confirmation.py b/dexbot/views/confirmation.py new file mode 100644 index 000000000..55d292aed --- /dev/null +++ b/dexbot/views/confirmation.py @@ -0,0 +1,13 @@ +from PyQt5 import QtWidgets + +from dexbot.views.gen.confirmation_window import Ui_Dialog + + +class ConfirmationDialog(QtWidgets.QDialog): + + def __init__(self, text): + super().__init__() + self.ui = Ui_Dialog() + self.ui.setupUi(self) + + self.ui.confirmation_label.setText(text) diff --git a/dexbot/views/gen/confirmation_window.py b/dexbot/views/gen/confirmation_window.py new file mode 100644 index 000000000..03a67893e --- /dev/null +++ b/dexbot/views/gen/confirmation_window.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'dexbot/views/orig/confirmation_window.ui' +# +# Created by: PyQt5 UI code generator 5.9.2 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName("Dialog") + Dialog.resize(569, 107) + self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) + self.verticalLayout.setObjectName("verticalLayout") + self.confirmation_label = QtWidgets.QLabel(Dialog) + self.confirmation_label.setText("") + self.confirmation_label.setAlignment(QtCore.Qt.AlignCenter) + self.confirmation_label.setObjectName("confirmation_label") + self.verticalLayout.addWidget(self.confirmation_label) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem) + self.cancel_button = QtWidgets.QPushButton(Dialog) + self.cancel_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.cancel_button.setObjectName("cancel_button") + self.horizontalLayout.addWidget(self.cancel_button) + self.ok_button = QtWidgets.QPushButton(Dialog) + self.ok_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.ok_button.setObjectName("ok_button") + self.horizontalLayout.addWidget(self.ok_button) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem1) + self.verticalLayout.addLayout(self.horizontalLayout) + + self.retranslateUi(Dialog) + self.ok_button.clicked.connect(Dialog.accept) + self.cancel_button.clicked.connect(Dialog.reject) + QtCore.QMetaObject.connectSlotsByName(Dialog) + Dialog.setTabOrder(self.ok_button, self.cancel_button) + + def retranslateUi(self, Dialog): + _translate = QtCore.QCoreApplication.translate + Dialog.setWindowTitle(_translate("Dialog", "DEXBot - Confirmation")) + self.cancel_button.setText(_translate("Dialog", "Cancel")) + self.ok_button.setText(_translate("Dialog", "OK")) + diff --git a/dexbot/views/orig/confirmation_window.ui b/dexbot/views/orig/confirmation_window.ui new file mode 100644 index 000000000..26e7cb444 --- /dev/null +++ b/dexbot/views/orig/confirmation_window.ui @@ -0,0 +1,118 @@ + + + Dialog + + + + 0 + 0 + 569 + 107 + + + + DEXBot - Confirmation + + + + + + + + + Qt::AlignCenter + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + PointingHandCursor + + + Cancel + + + + + + + PointingHandCursor + + + OK + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + ok_button + cancel_button + + + + + ok_button + clicked() + Dialog + accept() + + + 330 + 50 + + + 284 + 53 + + + + + cancel_button + clicked() + Dialog + reject() + + + 239 + 50 + + + 284 + 53 + + + + + From 0a35d31939ba0043f5bdcf625a2b2f4803d59218 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 2 Feb 2018 13:07:37 +0200 Subject: [PATCH 0046/1846] Change styling of the bot item widget --- dexbot/img/bin.png | Bin 0 -> 768 bytes dexbot/img/pen.png | Bin 0 -> 430 bytes dexbot/views/gen/bot_item_widget.py | 266 ++++++---- dexbot/views/orig/bot_item_widget.ui | 729 ++++++++++++++++----------- 4 files changed, 597 insertions(+), 398 deletions(-) create mode 100644 dexbot/img/bin.png create mode 100644 dexbot/img/pen.png diff --git a/dexbot/img/bin.png b/dexbot/img/bin.png new file mode 100644 index 0000000000000000000000000000000000000000..006236bb79431d6610686de34bad19a7b3f280d6 GIT binary patch literal 768 zcmV+b1ONPqP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0+mTbK~z{rwU+D(ld!O2Wg#n5+1L;}Lu6x! z>#FnA)9KUs-g7>8UG=|r&vSq8c~1ZLzNgiL#=vnn-@yqO3;$iu;m<#K35{q0tgYYx zR6RIz5UP3?LM`ous^BKr2VM0Hs+ybN1$5P7SORk!#!RzoX${H>r()4(ZY&l;6KzzvO3Z@Y=?odji1c&9Z=<;5Zb;ut*&;mIuUrIkHvls(}c z(kRt-h=4a5Xg`D}v%x83&l+VtG&Om%Ytktv#;Iu%=k!;44JPXz+HMA2uJH zLiVh&Ha7na%qrGM_y7^?TW?ufi&t$ok-g<$ZLG~Ot0%pN2=Zb6UWm0Us_DX}mC*Ut^Ol<*Ce~ORYcqP*U5KF7FeI&|>MR%vtJ2zxp0y1k$cskjVdT*}*x_A- yd~q?OFI|EBfD-I&M2`OqIDtA?Yfl%oT7LnDT(q#AAhYcN0000!lvI6;>1s;*b3=DjSL74G){)!Z!24_zf$B+p3lN}dp`_BCL^Jd@j_@m_+LHmrC z3~CD=+Oo(P&cDU?fkA2qZ-?kao@1gy`i)s4D_%0NFGyU!&~9JPv_sMtotUN=eQ`g)O9%b+BCp@&(ij%2JUxQqLwv2xy+<+d+Iei_w!}tjbc)PmxE?RE^uNKd;a5- zmB%|Cfz5%J1zWr@>Oo|R)uk_{YRG9FYub(+1%kqSx zpM}nUZV`4O;}QC*<>!eOTrYX<9L Wl^GMOR$c*yF@vY8pUXO@geCwOT%0-p literal 0 HcmV?d00001 diff --git a/dexbot/views/gen/bot_item_widget.py b/dexbot/views/gen/bot_item_widget.py index eef6b02c5..f7e729e70 100644 --- a/dexbot/views/gen/bot_item_widget.py +++ b/dexbot/views/gen/bot_item_widget.py @@ -12,20 +12,22 @@ class Ui_widget(object): def setupUi(self, widget): widget.setObjectName("widget") widget.setEnabled(True) - widget.resize(500, 161) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + widget.resize(480, 138) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(widget.sizePolicy().hasHeightForWidth()) widget.setSizePolicy(sizePolicy) + self.gridLayout_2 = QtWidgets.QGridLayout(widget) + self.gridLayout_2.setContentsMargins(0, 0, 0, 0) + self.gridLayout_2.setObjectName("gridLayout_2") self.widget_frame = QtWidgets.QFrame(widget) - self.widget_frame.setGeometry(QtCore.QRect(0, 0, 500, 161)) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.widget_frame.sizePolicy().hasHeightForWidth()) self.widget_frame.setSizePolicy(sizePolicy) - self.widget_frame.setMinimumSize(QtCore.QSize(491, 161)) + self.widget_frame.setMinimumSize(QtCore.QSize(480, 137)) self.widget_frame.setMaximumSize(QtCore.QSize(16777215, 16777215)) self.widget_frame.setLayoutDirection(QtCore.Qt.LeftToRight) self.widget_frame.setAutoFillBackground(False) @@ -37,43 +39,111 @@ def setupUi(self, widget): self.widget_frame.setObjectName("widget_frame") self.gridLayout_6 = QtWidgets.QGridLayout(self.widget_frame) self.gridLayout_6.setObjectName("gridLayout_6") - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) - self.horizontalLayout.setContentsMargins(10, -1, 10, -1) - self.horizontalLayout.setObjectName("horizontalLayout") - self.play_button = QtWidgets.QPushButton(self.widget_frame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + self.widget_3 = QtWidgets.QWidget(self.widget_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.play_button.sizePolicy().hasHeightForWidth()) - self.play_button.setSizePolicy(sizePolicy) - self.play_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.play_button.setStatusTip("") - self.play_button.setStyleSheet("border: 0;") - self.play_button.setText("") + sizePolicy.setHeightForWidth(self.widget_3.sizePolicy().hasHeightForWidth()) + self.widget_3.setSizePolicy(sizePolicy) + self.widget_3.setObjectName("widget_3") + self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.widget_3) + self.horizontalLayout_4.setContentsMargins(0, -1, 0, 1) + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.botname_label = QtWidgets.QLabel(self.widget_3) + font = QtGui.QFont() + font.setPointSize(12) + font.setBold(True) + font.setWeight(75) + self.botname_label.setFont(font) + self.botname_label.setStyleSheet("color: #005B78;") + self.botname_label.setAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft) + self.botname_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) + self.botname_label.setObjectName("botname_label") + self.horizontalLayout_4.addWidget(self.botname_label) + self.strategy_label = QtWidgets.QLabel(self.widget_3) + self.strategy_label.setMaximumSize(QtCore.QSize(16777215, 16777215)) + font = QtGui.QFont() + font.setPointSize(9) + font.setBold(True) + font.setWeight(75) + self.strategy_label.setFont(font) + self.strategy_label.setAutoFillBackground(False) + self.strategy_label.setStyleSheet("color: #005B78;") + self.strategy_label.setAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing) + self.strategy_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) + self.strategy_label.setObjectName("strategy_label") + self.horizontalLayout_4.addWidget(self.strategy_label) + self.edit_button = QtWidgets.QPushButton(self.widget_3) + self.edit_button.setMaximumSize(QtCore.QSize(28, 16777215)) + self.edit_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.edit_button.setStyleSheet("border: 0") + self.edit_button.setText("") icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap("dexbot/img/play.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.play_button.setIcon(icon) - self.play_button.setIconSize(QtCore.QSize(30, 30)) - self.play_button.setObjectName("play_button") - self.horizontalLayout.addWidget(self.play_button) - self.pause_button = QtWidgets.QPushButton(self.widget_frame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + icon.addPixmap(QtGui.QPixmap("dexbot/img/pen.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.edit_button.setIcon(icon) + self.edit_button.setObjectName("edit_button") + self.horizontalLayout_4.addWidget(self.edit_button) + self.remove_button = QtWidgets.QPushButton(self.widget_3) + self.remove_button.setMaximumSize(QtCore.QSize(28, 16777215)) + self.remove_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.remove_button.setStyleSheet("border: 0;") + self.remove_button.setText("") + icon1 = QtGui.QIcon() + icon1.addPixmap(QtGui.QPixmap("dexbot/img/bin.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.remove_button.setIcon(icon1) + self.remove_button.setIconSize(QtCore.QSize(20, 20)) + self.remove_button.setObjectName("remove_button") + self.horizontalLayout_4.addWidget(self.remove_button) + self.gridLayout_6.addWidget(self.widget_3, 1, 0, 1, 1) + self.gridLayout = QtWidgets.QGridLayout() + self.gridLayout.setContentsMargins(0, -1, -1, -1) + self.gridLayout.setObjectName("gridLayout") + self.gridLayout_3 = QtWidgets.QGridLayout() + self.gridLayout_3.setObjectName("gridLayout_3") + self.widget_6 = QtWidgets.QWidget(self.widget_frame) + self.widget_6.setMinimumSize(QtCore.QSize(130, 0)) + self.widget_6.setObjectName("widget_6") + self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.widget_6) + self.verticalLayout_5.setContentsMargins(0, 0, 0, 0) + self.verticalLayout_5.setObjectName("verticalLayout_5") + self.currency_label = QtWidgets.QLabel(self.widget_6) + font = QtGui.QFont() + font.setPointSize(11) + font.setBold(True) + font.setWeight(75) + self.currency_label.setFont(font) + self.currency_label.setStyleSheet("color: #005B78;") + self.currency_label.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self.currency_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) + self.currency_label.setObjectName("currency_label") + self.verticalLayout_5.addWidget(self.currency_label) + self.profit_label = QtWidgets.QLabel(self.widget_6) + font = QtGui.QFont() + font.setPointSize(10) + font.setBold(True) + font.setWeight(75) + self.profit_label.setFont(font) + self.profit_label.setStyleSheet("color: #00D05A;") + self.profit_label.setLineWidth(1) + self.profit_label.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self.profit_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) + self.profit_label.setObjectName("profit_label") + self.verticalLayout_5.addWidget(self.profit_label) + self.gridLayout_3.addWidget(self.widget_6, 0, 0, 1, 1) + self.gridLayout.addLayout(self.gridLayout_3, 0, 0, 1, 1) + self.widget_4 = QtWidgets.QWidget(self.widget_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.pause_button.sizePolicy().hasHeightForWidth()) - self.pause_button.setSizePolicy(sizePolicy) - self.pause_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.pause_button.setStyleSheet("border: 0;\n" -"") - self.pause_button.setText("") - icon1 = QtGui.QIcon() - icon1.addPixmap(QtGui.QPixmap("dexbot/img/pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.pause_button.setIcon(icon1) - self.pause_button.setIconSize(QtCore.QSize(30, 30)) - self.pause_button.setObjectName("pause_button") - self.horizontalLayout.addWidget(self.pause_button) - self.widget_2 = QtWidgets.QWidget(self.widget_frame) + sizePolicy.setHeightForWidth(self.widget_4.sizePolicy().hasHeightForWidth()) + self.widget_4.setSizePolicy(sizePolicy) + self.widget_4.setObjectName("widget_4") + self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.widget_4) + self.verticalLayout_4.setContentsMargins(-1, -1, -1, 5) + self.verticalLayout_4.setObjectName("verticalLayout_4") + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout_4.addItem(spacerItem) + self.widget_2 = QtWidgets.QWidget(self.widget_4) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -82,9 +152,10 @@ def setupUi(self, widget): self.widget_2.setStyleSheet("") self.widget_2.setObjectName("widget_2") self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.widget_2) - self.horizontalLayout_3.setContentsMargins(15, -1, 15, -1) + self.horizontalLayout_3.setContentsMargins(5, -1, 5, 0) self.horizontalLayout_3.setObjectName("horizontalLayout_3") self.horizontalSlider = QtWidgets.QSlider(self.widget_2) + self.horizontalSlider.setEnabled(False) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -96,7 +167,7 @@ def setupUi(self, widget): "}\n" "QSlider::handle:horizontal {\n" "background: #005B78;\n" -"width: 18px;\n" +"width: 15px;\n" "margin: -5px 0;\n" "}\n" "QSlider {\n" @@ -110,70 +181,57 @@ def setupUi(self, widget): self.horizontalSlider.setTickPosition(QtWidgets.QSlider.TicksAbove) self.horizontalSlider.setObjectName("horizontalSlider") self.horizontalLayout_3.addWidget(self.horizontalSlider) - self.horizontalLayout.addWidget(self.widget_2) - self.remove_button = QtWidgets.QPushButton(self.widget_frame) - self.remove_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.remove_button.setObjectName("remove_button") - self.horizontalLayout.addWidget(self.remove_button) - self.gridLayout_6.addLayout(self.horizontalLayout, 4, 0, 1, 1) - spacerItem = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - self.gridLayout_6.addItem(spacerItem, 2, 0, 1, 1) - self.widget_3 = QtWidgets.QWidget(self.widget_frame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + self.verticalLayout_4.addWidget(self.widget_2) + self.gridLayout.addWidget(self.widget_4, 0, 1, 2, 1) + self.widget_5 = QtWidgets.QWidget(self.widget_frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.widget_3.sizePolicy().hasHeightForWidth()) - self.widget_3.setSizePolicy(sizePolicy) - self.widget_3.setObjectName("widget_3") - self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.widget_3) - self.horizontalLayout_4.setContentsMargins(-1, -1, 0, 11) - self.horizontalLayout_4.setObjectName("horizontalLayout_4") - self.botname_label = QtWidgets.QLabel(self.widget_3) - font = QtGui.QFont() - font.setPointSize(12) - font.setBold(True) - font.setWeight(75) - self.botname_label.setFont(font) - self.botname_label.setStyleSheet("color: #005B78;") - self.botname_label.setAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft) - self.botname_label.setObjectName("botname_label") - self.horizontalLayout_4.addWidget(self.botname_label) - self.currency_label = QtWidgets.QLabel(self.widget_3) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(75) - self.currency_label.setFont(font) - self.currency_label.setStyleSheet("color: #005B78;") - self.currency_label.setAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignHCenter) - self.currency_label.setObjectName("currency_label") - self.horizontalLayout_4.addWidget(self.currency_label) - self.profit_label = QtWidgets.QLabel(self.widget_3) - font = QtGui.QFont() - font.setPointSize(12) - font.setBold(True) - font.setWeight(75) - self.profit_label.setFont(font) - self.profit_label.setStyleSheet("color: #00D05A;") - self.profit_label.setAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft) - self.profit_label.setObjectName("profit_label") - self.horizontalLayout_4.addWidget(self.profit_label) - self.strategy_label = QtWidgets.QLabel(self.widget_3) - self.strategy_label.setMaximumSize(QtCore.QSize(16777215, 16777215)) - font = QtGui.QFont() - font.setPointSize(9) - font.setBold(True) - font.setWeight(75) - self.strategy_label.setFont(font) - self.strategy_label.setAutoFillBackground(False) - self.strategy_label.setStyleSheet("color: #005B78;") - self.strategy_label.setAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing) - self.strategy_label.setObjectName("strategy_label") - self.horizontalLayout_4.addWidget(self.strategy_label) - self.edit_button = QtWidgets.QPushButton(self.widget_3) - self.edit_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.edit_button.setObjectName("edit_button") - self.horizontalLayout_4.addWidget(self.edit_button) - self.gridLayout_6.addWidget(self.widget_3, 1, 0, 1, 1) + sizePolicy.setHeightForWidth(self.widget_5.sizePolicy().hasHeightForWidth()) + self.widget_5.setSizePolicy(sizePolicy) + self.widget_5.setMaximumSize(QtCore.QSize(16777215, 16777215)) + self.widget_5.setObjectName("widget_5") + self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.widget_5) + self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.pause_button = QtWidgets.QPushButton(self.widget_5) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.pause_button.sizePolicy().hasHeightForWidth()) + self.pause_button.setSizePolicy(sizePolicy) + self.pause_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.pause_button.setStyleSheet("border: 0;\n" +"") + self.pause_button.setText("") + icon2 = QtGui.QIcon() + icon2.addPixmap(QtGui.QPixmap("dexbot/img/pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + icon2.addPixmap(QtGui.QPixmap("dexbot/img/play.png"), QtGui.QIcon.Normal, QtGui.QIcon.On) + self.pause_button.setIcon(icon2) + self.pause_button.setIconSize(QtCore.QSize(30, 30)) + self.pause_button.setObjectName("pause_button") + self.horizontalLayout_2.addWidget(self.pause_button) + self.play_button = QtWidgets.QPushButton(self.widget_5) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.play_button.sizePolicy().hasHeightForWidth()) + self.play_button.setSizePolicy(sizePolicy) + self.play_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.play_button.setStatusTip("") + self.play_button.setStyleSheet("border: 0;") + self.play_button.setText("") + icon3 = QtGui.QIcon() + icon3.addPixmap(QtGui.QPixmap("dexbot/img/play.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.play_button.setIcon(icon3) + self.play_button.setIconSize(QtCore.QSize(30, 30)) + self.play_button.setObjectName("play_button") + self.horizontalLayout_2.addWidget(self.play_button) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_2.addItem(spacerItem1) + self.gridLayout.addWidget(self.widget_5, 1, 0, 1, 1) + self.gridLayout_6.addLayout(self.gridLayout, 3, 0, 1, 1) + self.gridLayout_2.addWidget(self.widget_frame, 0, 0, 1, 1) self.retranslateUi(widget) QtCore.QMetaObject.connectSlotsByName(widget) @@ -181,10 +239,8 @@ def setupUi(self, widget): def retranslateUi(self, widget): _translate = QtCore.QCoreApplication.translate widget.setWindowTitle(_translate("widget", "widget")) - self.remove_button.setText(_translate("widget", "Remove")) - self.botname_label.setText(_translate("widget", "BOTNAME")) + self.botname_label.setText(_translate("widget", "Botname")) + self.strategy_label.setText(_translate("widget", "SIMPLE STRATEGY")) self.currency_label.setText(_translate("widget", "BTS/USD")) self.profit_label.setText(_translate("widget", "+1.234%")) - self.strategy_label.setText(_translate("widget", "SIMPLE STRATEGY")) - self.edit_button.setText(_translate("widget", "Edit")) diff --git a/dexbot/views/orig/bot_item_widget.ui b/dexbot/views/orig/bot_item_widget.ui index 6d9999299..570328995 100644 --- a/dexbot/views/orig/bot_item_widget.ui +++ b/dexbot/views/orig/bot_item_widget.ui @@ -9,12 +9,12 @@ 0 0 - 500 - 161 + 480 + 138 - + 0 0 @@ -22,331 +22,474 @@ widget - - - - 0 - 0 - 500 - 161 - + + + 0 - - - 0 - 0 - + + 0 - - - 491 - 161 - + + 0 - - - 16777215 - 16777215 - + + 0 - - Qt::LeftToRight - - - false - - - .QFrame { border: 1px solid #005B78; border-radius: 4px; } + + + + + 0 + 0 + + + + + 480 + 137 + + + + + 16777215 + 16777215 + + + + Qt::LeftToRight + + + false + + + .QFrame { border: 1px solid #005B78; border-radius: 4px; } * { background-color: white; } - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - QLayout::SetDefaultConstraint - - - 10 - - - 10 - - - - - - 0 - 0 - - - - PointingHandCursor - - - - - - border: 0; - - - - - - - dexbot/img/play.pngdexbot/img/play.png - - - - 30 - 30 - - - - - - - - - 0 - 0 - - - - PointingHandCursor - - - border: 0; - - - - - - - - dexbot/img/pause.pngdexbot/img/pause.png - - - - 30 - 30 - - - - - - + + + QFrame::StyledPanel + + + QFrame::Raised + + + + - + 0 0 - - - - + - 15 + 0 - 15 + 0 + + + 1 - - - - 0 - 0 - + + + + 12 + 75 + true + - QSlider::groove:horizontal { + color: #005B78; + + + Botname + + + Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + + Qt::TextSelectableByMouse + + + + + + + + 16777215 + 16777215 + + + + + 9 + 75 + true + + + + false + + + color: #005B78; + + + SIMPLE STRATEGY + + + Qt::AlignBottom|Qt::AlignRight|Qt::AlignTrailing + + + Qt::TextSelectableByMouse + + + + + + + + 28 + 16777215 + + + + PointingHandCursor + + + border: 0 + + + + + + + dexbot/img/pen.pngdexbot/img/pen.png + + + + + + + + 28 + 16777215 + + + + PointingHandCursor + + + border: 0; + + + + + + + dexbot/img/bin.pngdexbot/img/bin.png + + + + 20 + 20 + + + + + + + + + + + 0 + + + + + + + + 130 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 11 + 75 + true + + + + color: #005B78; + + + BTS/USD + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + Qt::TextSelectableByMouse + + + + + + + + 10 + 75 + true + + + + color: #00D05A; + + + 1 + + + +1.234% + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + Qt::TextSelectableByMouse + + + + + + + + + + + + + 0 + 0 + + + + + 5 + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 0 + + + + + + + + 5 + + + 5 + + + 0 + + + + + false + + + + 0 + 0 + + + + QSlider::groove:horizontal { height: 2px; background: #005B78; } QSlider::handle:horizontal { background: #005B78; -width: 18px; +width: 15px; margin: -5px 0; } QSlider { border-left: 2px solid #005B78; border-right: 2px solid #005B78; } + + + 100 + + + 50 + + + false + + + Qt::Horizontal + + + QSlider::TicksAbove + + + + + + + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + + 0 - - 100 + + 0 - - 50 + + 0 - - false - - - Qt::Horizontal - - - QSlider::TicksAbove + + 0 - - - - - - - - - PointingHandCursor - - - Remove - - + + + + + 0 + 0 + + + + PointingHandCursor + + + border: 0; + + + + + + + + dexbot/img/pause.png + dexbot/img/play.pngdexbot/img/pause.png + + + + 30 + 30 + + + + + + + + + 0 + 0 + + + + PointingHandCursor + + + + + + border: 0; + + + + + + + dexbot/img/play.pngdexbot/img/play.png + + + + 30 + 30 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 20 - - - - - - - - - 0 - 0 - - - - - 0 - - - 11 - - - - - - 12 - 75 - true - - - - color: #005B78; - - - BOTNAME - - - Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft - - - - - - - - 75 - true - - - - color: #005B78; - - - BTS/USD - - - Qt::AlignBottom|Qt::AlignHCenter - - - - - - - - 12 - 75 - true - - - - color: #00D05A; - - - +1.234% - - - Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft - - - - - - - - 16777215 - 16777215 - - - - - 9 - 75 - true - - - - false - - - color: #005B78; - - - SIMPLE STRATEGY - - - Qt::AlignBottom|Qt::AlignRight|Qt::AlignTrailing - - - - - - - PointingHandCursor - - - Edit - - - - - - - - + + + From fff1a1fe93cd72d1ea81a7fbf173316da7f03605 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 2 Feb 2018 13:47:21 +0200 Subject: [PATCH 0047/1846] Change bot item widget default profit value --- dexbot/views/gen/bot_item_widget.py | 4 ++-- dexbot/views/orig/bot_item_widget.ui | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/views/gen/bot_item_widget.py b/dexbot/views/gen/bot_item_widget.py index f7e729e70..c91cf28e9 100644 --- a/dexbot/views/gen/bot_item_widget.py +++ b/dexbot/views/gen/bot_item_widget.py @@ -205,8 +205,8 @@ def setupUi(self, widget): "") self.pause_button.setText("") icon2 = QtGui.QIcon() - icon2.addPixmap(QtGui.QPixmap("dexbot/img/pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) icon2.addPixmap(QtGui.QPixmap("dexbot/img/play.png"), QtGui.QIcon.Normal, QtGui.QIcon.On) + icon2.addPixmap(QtGui.QPixmap("dexbot/img/pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.pause_button.setIcon(icon2) self.pause_button.setIconSize(QtCore.QSize(30, 30)) self.pause_button.setObjectName("pause_button") @@ -242,5 +242,5 @@ def retranslateUi(self, widget): self.botname_label.setText(_translate("widget", "Botname")) self.strategy_label.setText(_translate("widget", "SIMPLE STRATEGY")) self.currency_label.setText(_translate("widget", "BTS/USD")) - self.profit_label.setText(_translate("widget", "+1.234%")) + self.profit_label.setText(_translate("widget", "+0.000%")) diff --git a/dexbot/views/orig/bot_item_widget.ui b/dexbot/views/orig/bot_item_widget.ui index 570328995..c719ea13c 100644 --- a/dexbot/views/orig/bot_item_widget.ui +++ b/dexbot/views/orig/bot_item_widget.ui @@ -268,7 +268,7 @@ 1 - +1.234% + +0.000% Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter From 9682370edf4174c71793f5521f288a6833e291e5 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 2 Feb 2018 14:07:16 +0200 Subject: [PATCH 0048/1846] Change styling of wallet unlock dialog --- dexbot/views/gen/unlock_wallet_window.py | 7 +++---- dexbot/views/orig/unlock_wallet_window.ui | 23 +++++------------------ 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/dexbot/views/gen/unlock_wallet_window.py b/dexbot/views/gen/unlock_wallet_window.py index bcd85935c..19ed4c3b0 100644 --- a/dexbot/views/gen/unlock_wallet_window.py +++ b/dexbot/views/gen/unlock_wallet_window.py @@ -11,7 +11,7 @@ class Ui_Dialog(object): def setupUi(self, Dialog): Dialog.setObjectName("Dialog") - Dialog.resize(473, 141) + Dialog.resize(473, 126) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -50,6 +50,7 @@ def setupUi(self, Dialog): self.formLayout = QtWidgets.QFormLayout(self.form_wrap) self.formLayout.setLabelAlignment(QtCore.Qt.AlignCenter) self.formLayout.setFormAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignTop) + self.formLayout.setContentsMargins(-1, -1, -1, 0) self.formLayout.setObjectName("formLayout") self.password_label = QtWidgets.QLabel(self.form_wrap) self.password_label.setObjectName("password_label") @@ -70,8 +71,6 @@ def setupUi(self, Dialog): self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.ok_button) self.horizontalLayout_3.addWidget(self.form_wrap) self.verticalLayout.addWidget(self.widget) - spacerItem = QtWidgets.QSpacerItem(20, 25, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout.addItem(spacerItem) self.password_label.setBuddy(self.password_input) self.retranslateUi(Dialog) @@ -79,7 +78,7 @@ def setupUi(self, Dialog): def retranslateUi(self, Dialog): _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "Unlock wallet")) + Dialog.setWindowTitle(_translate("Dialog", "DEXBot - Unlock wallet")) self.helper_text.setText(_translate("Dialog", "Please enter your wallet password before continuing.")) self.password_label.setText(_translate("Dialog", "Wallet password")) self.ok_button.setText(_translate("Dialog", "OK")) diff --git a/dexbot/views/orig/unlock_wallet_window.ui b/dexbot/views/orig/unlock_wallet_window.ui index a11a262b2..f6989a8c3 100644 --- a/dexbot/views/orig/unlock_wallet_window.ui +++ b/dexbot/views/orig/unlock_wallet_window.ui @@ -7,7 +7,7 @@ 0 0 473 - 141 + 126 @@ -17,7 +17,7 @@ - Unlock wallet + DEXBot - Unlock wallet true @@ -75,6 +75,9 @@ Qt::AlignHCenter|Qt::AlignTop + + 0 + @@ -120,22 +123,6 @@ - - - - Qt::Vertical - - - QSizePolicy::Expanding - - - - 20 - 25 - - - - From 6bc57511b16ed586a70f5d4d204cace77087789c Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 5 Feb 2018 13:51:54 +1100 Subject: [PATCH 0049/1846] draft of new approach to config: use whiptail with fallback to plain CLI via click --- dexbot/bot.py | 4 +- dexbot/cli_conf.py | 151 ++++++++++++++++++++++++++++----------------- setup.py | 2 +- 3 files changed, 97 insertions(+), 60 deletions(-) diff --git a/dexbot/bot.py b/dexbot/bot.py index 57441abb3..5c2c3a8d6 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -7,8 +7,8 @@ log = logging.getLogger(__name__) # FIXME: currently static list of bot strategies: ? how to enumerate bots available and deploy new bot strategies. -STRATEGIES={'Echo':('dexbot.strategies.echo','Echo'), - 'Liquidity Walls':('dexbot.strategies.walls','Walls')} +STRATEGIES=[('Echo','dexbot.strategies.echo'), + ("Haywood's Follow Orders",'dexbot.strategies.follow_orders')] class BotInfrastructure(): diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 525e1df9e..56a79309f 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -4,7 +4,7 @@ If systemd is detected it will offer to install a user service unit (under ~/.local/share/systemd This requires a per-user systemd process to be runnng -Requires the 'dialog' tool: so UNIX-like sytems only +Requires the 'whiptail' tool: so UNIX-like sytems only Note there is some common cross-UI configuration stuff: look in basestrategy.py It's expected GUI/web interfaces will be re-implementing code in this file, but they should @@ -14,8 +14,8 @@ """ -import dialog, importlib, os, os.path, sys, collections, re - +import importlib, os, os.path, sys, collections, re, tempfile, shutil +import click, whiptail from dexbot.bot import STRATEGIES @@ -43,54 +43,104 @@ class QuitException(Exception): pass +class LocalWhiptail(whiptail.Whiptail): + + def __init__(self): + super().__init__(backtitle='dexbot configuration') + + def view_text(self, text): + """Whiptail wants a file but we want to provide a text string""" + fd, nam = tempfile.mkstemp() + f = os.fdopen(fd) + f.write(text) + f.close() + self.view_file(nam) + os.unlink(nam) + +class NoWhiptail: + """ + Imitates the interface of whiptail but uses click only + + This is very basic CLI: real state-of-the-1970s stuff, + but it works *everywhere* + """ + + def prompt(self, msg, default='', password=False): + return click.prompt(msg,default=default,hide_input=password) + + def confirm(self, msg, default='yes'): + return click.confirm(msg,default=(default=='yes')) + + def alert(self, msg): + click.echo( + "[" + + click.style("alert", fg="yellow") + + "] " + msg + ) + + def view_text(self, text): + click.echo_via_pager(text) + + def menu(self, msg='', items=(), prefix=' - ', default=0): + click.echo(msg+'\n') + if type(items) is dict: items = list(items.items()) + i = 1 + for k, v in items: + click.echo("{:>2}) {}".format(i, v)) + i += 1 + click.echo("\n") + ret = click.prompt("Your choice:",type=int,default=default+1) + ret = items[ret-1] + return ret[0] + + def radiolist(self, msg='', items=(), prefix=' - '): + d = 0 + default = 0 + for k, v, s in items: + if s == "ON": + default = d + d += 1 + self.menu(msg,items,default=default) + def select_choice(current,choices): """for the radiolist, get us a list with the current value selected""" - return [(tag,text,current == tag) for tag,text in choices] - + return [(tag,text,(current == tag and "ON") or "OFF") for tag,text in choices] def process_config_element(elem,d,config): """ - process an item of configuration metadata display a widget as approrpriate + process an item of configuration metadata display a widget as appropriate d: the Dialog object config: the config dctionary for this bot """ if elem.type == "string": - code, txt = d.inputbox(elem.description,init=config.get(elem.key,elem.default)) - if code != d.OK: raise QuitException() + txt = d.prompt(elem.description,config.get(elem.key,elem.default)) if elem.extra: while not re.match(elem.extra,txt): - d.msgbox("The value is not valid") - code, txt = d.inputbox(elem.description,init=config.get(elem.key,elem.default)) - if code != d.OK: raise QuitException() + d.alert("The value is not valid") + txt = d.prompt(elem.description,config.get(elem.key,elem.default)) config[elem.key] = txt - if elem.type == "int": - code, val = d.rangebox(elem.description,init=config.get(elem.key,elem.default),min=elem.extra[0],max=elem.extra[1]) - if code != d.OK: raise QuitException() - config[elem.key] = val if elem.type == "bool": - code = d.yesno(elem.description) - config[elem.key] = (code == d.OK) - if elem.type == "float": - code, txt = d.inputbox(elem.description,init=config.get(elem.key,str(elem.default))) - if code != d.OK: raise QuitException() + config[elem.key] = d.confirm(elem.description) + if elem.type in ("float", "int"): + txt = d.prompt(elem.description,config.get(elem.key,str(elem.default))) while True: try: - val = float(txt) + if elem.type == "int": + val = int(txt) + else: + val = float(txt) if val < elem.extra[0]: - d.msgbox("The value is too low") + d.alert("The value is too low") elif elem.extra[1] and val > elem.extra[1]: - d.msgbox("the value is too high") + d.alert("the value is too high") else: break except ValueError: - d.msgbox("Not a valid value") - code, txt = d.inputbox(elem.description,init=config.get(elem.key,str(elem.default))) - if code != d.OK: raise QuitException() + d.alert("Not a valid value") + txt = d.prompt(elem.description,config.get(elem.key,str(elem.default))) config[elem.key] = val if elem.type == "choice": - code, tag = d.radiolist(elem.description,choices=select_choice(config.get(elem.key,elem.default),elem.extra)) - if code != d.OK: raise QuitException() - config[elem.key] = tag + config[elem.key] = d.radiolist(elem.description,select_choice(config.get(elem.key,elem.default),elem.extra)) def setup_systemd(d,config): if config.get("systemd_status","install") == "reject": @@ -102,13 +152,12 @@ def setup_systemd(d,config): # so just tell cli.py to quietly restart the daemon config["systemd_status"] = "installed" return - if d.yesno("Do you want to install dexbot as a background (daemon) process?") == d.OK: + if d.confirm("Do you want to install dexbot as a background (daemon) process?"): for i in ["~/.local","~/.local/share","~/.local/share/systemd","~/.local/share/systemd/user"]: j = os.path.expanduser(i) if not os.path.exists(j): os.mkdir(j) - code, passwd = d.passwordbox("The wallet password entered with uptick\nNOTE: this will be saved on disc so the bot can run unattended. This means anyone with access to this computer's file can spend all your money",insecure=True) - if code != d.OK: raise QuitException() + passwd = d.prompt("The wallet password entered with uptick\nNOTE: this will be saved on disc so the bot can run unattended. This means anyone with access to this computer's file can spend all your money",password=True) fd = os.open(SYSTEMD_SERVICE_NAME, os.O_WRONLY|os.O_CREAT, 0o600) # because we hold password be restrictive with open(fd, "w") as fp: fp.write(SYSTEMD_SERVICE_FILE.format(exe=sys.argv[0],passwd=passwd,homedir=os.path.expanduser("~"))) @@ -118,15 +167,10 @@ def setup_systemd(d,config): def configure_bot(d,bot): - if 'module' in bot: - inv_map = {v:k for k,v in STRATEGIES.items()} - strategy = inv_map[(bot['module'],bot['bot'])] - else: - strategy = 'Echo' - code, tag = d.radiolist("Choose a bot strategy", - choices=select_choice(strategy,[(i,i) for i in STRATEGIES])) - if code != d.OK: raise QuitException() - bot['module'], bot['bot'] = STRATEGIES[tag] + strategy = bot.get('module','dexbot.strategies.echo') + bot['module'] = d.radiolist("Choose a bot strategy", + choices=select_choice(strategy,STRATEGIES)) + bot['bot'] = 'Strategy' # its always Strategy now, for backwards compatibilty only # import the bot class but we don't __init__ it here klass = getattr( importlib.import_module(bot["module"]), @@ -138,33 +182,26 @@ def configure_bot(d,bot): for c in configs: process_config_element(c,d,bot) else: - d.msgbox("This bot type does not have configuration information. You will have to check the bot code and add configuration values to config.yml if required") + d.alert("This bot type does not have configuration information. You will have to check the bot code and add configuration values to config.yml if required") return bot def configure_dexbot(config): - d = dialog.Dialog(dialog="dialog",autowidgetsize=True) - d.set_background_title("dexbot configuration") - tag = "" - while not tag: - code, tag = d.radiolist("Choose a Witness node to use", - choices=select_choice(config.get("node"),NODES)) - if code != d.OK: raise QuitException() - if not tag: d.msgbox("You need to choose a node") - config['node'] = tag + if shutil.which("whiptail"): + d = LocalWhiptail() + else: + d = NoWhiptail() # use our own fake whiptail + config['node'] = d.radiolist("Choose a Witness node to use",choices=select_choice(config.get("node"),NODES)) bots = config.get('bots',{}) if len(bots) == 0: - code, txt = d.inputbox("Your name for the bot") - if code != d.OK: raise QuitException() + txt = d.prompt("Your name for the bot") config['bots'] = {txt:configure_bot(d,{})} else: - code, botname = d.menu("Select bot to edit", + botname = d.menu("Select bot to edit", choices=[(i,i) for i in bots]+[('NEW','New bot')]) - if code != d.OK: raise QuitException() if botname == 'NEW': - code, txt = d.inputbox("Your name for the bot") - if code != d.OK: raise QuitException() + txt = d.prompt("Your name for the bot") config['bots'][txt] = configure_bot(d,{}) else: config['bots'][botname] = configure_bot(d,config['bots'][botname]) diff --git a/setup.py b/setup.py index f5c41dc14..da0ac5d2e 100755 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ "sqlalchemy", "appdirs", "pyqt5", - "pythondialog", + "whiptail", "sdnotify" ], include_package_data=True, From cf94a980462c101f739b763f6c080f4b341c78fb Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 5 Feb 2018 10:01:11 +0200 Subject: [PATCH 0050/1846] Add bot creation to GUI --- dexbot/controllers/create_bot_controller.py | 24 ++++++++++++++-- dexbot/views/create_bot.py | 32 ++++++++++++++------- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/dexbot/controllers/create_bot_controller.py b/dexbot/controllers/create_bot_controller.py index 5ad1a91d3..ac687de60 100644 --- a/dexbot/controllers/create_bot_controller.py +++ b/dexbot/controllers/create_bot_controller.py @@ -4,6 +4,7 @@ from bitshares.instance import shared_bitshares_instance from bitshares.asset import Asset from bitshares.account import Account +from bitsharesbase.account import PrivateKey from ruamel.yaml import YAML @@ -44,8 +45,27 @@ def account_exists(self, account): return False def is_account_valid(self, account, private_key): - # Todo: finish this - return True + wallet = self.bitshares.wallet + try: + pubkey = format(PrivateKey(private_key).pubkey, self.bitshares.prefix) + except ValueError: + return False + + accounts = wallet.getAllAccounts(pubkey) + account_names = [account['name'] for account in accounts] + + if account in account_names: + return True + else: + return False + + def add_private_key(self, private_key): + wallet = self.bitshares.wallet + try: + wallet.addPrivateKey(private_key) + except ValueError: + # Private key already added + pass @staticmethod def get_unique_bot_name(): diff --git a/dexbot/views/create_bot.py b/dexbot/views/create_bot.py index 792da387e..24bc03b6e 100644 --- a/dexbot/views/create_bot.py +++ b/dexbot/views/create_bot.py @@ -20,7 +20,7 @@ def __init__(self, controller): self.ui.bot_name_input.setText(self.bot_name) self.ui.save_button.clicked.connect(self.handle_save) - self.ui.cancel_button.clicked.connect(self.handle_cancel) + self.ui.cancel_button.clicked.connect(self.reject) def validate_bot_name(self): bot_name = self.ui.bot_name_input.text() @@ -72,19 +72,29 @@ def handle_save(self): if not self.validate_form(): return - self.bot_name = self.ui.bot_name_input.text() - account = self.ui.account_input.text() - market = '{}:{}'.format(self.ui.base_asset_input, self.ui.quote_asset_input) - strategy = self.ui.strategy_input.currentText() + # 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 + target = { + 'amount': float(ui.amount_input.text()), + 'center_price': float(ui.center_price_input.text()), + 'spread': spread + } + + base_asset = ui.base_asset_input.text() + quote_asset = ui.quote_asset_input.text() + strategy = ui.strategy_input.currentText() bot_module = self.controller.get_strategy_module(strategy) bot_data = { - 'account': account, - 'market': market, + 'account': ui.account_input.text(), + 'market': '{}/{}'.format(base_asset, quote_asset), 'module': bot_module, - 'strategy': strategy + 'strategy': strategy, + 'target': target } + self.bot_name = ui.bot_name_input.text() self.controller.add_bot_config(self.bot_name, bot_data) self.accept() - - def handle_cancel(self): - self.reject() From 35a3391d5e2ddbe8f4182e13129d4d711455f9c8 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 5 Feb 2018 15:04:46 +0200 Subject: [PATCH 0051/1846] Change market format to reversed market --- dexbot/views/create_bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/views/create_bot.py b/dexbot/views/create_bot.py index 24bc03b6e..9bf7c3497 100644 --- a/dexbot/views/create_bot.py +++ b/dexbot/views/create_bot.py @@ -90,7 +90,7 @@ def handle_save(self): bot_module = self.controller.get_strategy_module(strategy) bot_data = { 'account': ui.account_input.text(), - 'market': '{}/{}'.format(base_asset, quote_asset), + 'market': '{}/{}'.format(quote_asset, base_asset), 'module': bot_module, 'strategy': strategy, 'target': target From 2afd60f7e78f5d178a3ddabb4ad91ad1fc5050a6 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 5 Feb 2018 16:50:41 +0200 Subject: [PATCH 0052/1846] Fix gui updating Changed the BotInfrastructure to a subclass of Thread. --- dexbot/bot.py | 8 ++--- dexbot/controllers/main_controller.py | 6 ++-- dexbot/strategies/simple.py | 44 +++++++++++++++++++-------- dexbot/views/bot_item.py | 9 ++++-- dexbot/views/bot_list.py | 18 +++++------ 5 files changed, 54 insertions(+), 31 deletions(-) diff --git a/dexbot/bot.py b/dexbot/bot.py index 7f9ff0649..9ac898613 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -2,7 +2,7 @@ import sys import logging import os.path -from multiprocessing import Process +from threading import Thread from dexbot.basestrategy import BaseStrategy @@ -18,7 +18,7 @@ # GUIs can add a handler to this logger to get a stream of events re the running bots. -class BotInfrastructure(Process): +class BotInfrastructure(Thread): bots = dict() @@ -26,7 +26,7 @@ def __init__( self, config, bitshares_instance=None, - gui_data=None + view=None ): super().__init__() # BitShares instance @@ -60,7 +60,7 @@ def __init__( config=config, name=botname, bitshares_instance=self.bitshares, - gui_data=gui_data + view=view ) markets.add(bot['market']) accounts.add(bot['account']) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 3eefd3cbe..bf123a584 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -13,10 +13,8 @@ def __init__(self, bitshares_instance): set_shared_bitshares_instance(bitshares_instance) self.bot_template = BotInfrastructure - def create_bot(self, botname, config): - gui_data = {'id': botname, 'controller': self} - bot = self.bot_template(config, self.bitshares_instance, gui_data) - bot.daemon = True + def create_bot(self, botname, config, view): + bot = self.bot_template(config, self.bitshares_instance, view) bot.start() self.bots[botname] = bot diff --git a/dexbot/strategies/simple.py b/dexbot/strategies/simple.py index ac7edd681..e6f700fc9 100644 --- a/dexbot/strategies/simple.py +++ b/dexbot/strategies/simple.py @@ -4,7 +4,7 @@ from bitshares.price import Price from dexbot.basestrategy import BaseStrategy -from dexbot.errors import InsufficientFundsError +from dexbot.queue.idle_queue import idle_add class Strategy(BaseStrategy): @@ -29,8 +29,9 @@ def __init__(self, *args, **kwargs): # Tests for actions self.price = self.bot.get("target", {}).get("center_price", 0) - self.cancel_all() - self.clear() + + self.bot_name = kwargs.get('name') + self.view = kwargs.get('view') def error(self, *args, **kwargs): self.disabled = True @@ -50,7 +51,10 @@ def init_strategy(self): # Buy Side if float(self.balance(self.market["base"])) < buy_price * amount: - InsufficientFundsError(Amount(amount=amount * float(buy_price), asset=self.market["base"])) + self.log.critical( + 'Insufficient buy balance, needed {} {}'.format(buy_price * amount, self.market['base']['symbol']) + ) + self.disabled = True else: buy_transaction = self.market.buy( buy_price, @@ -63,8 +67,11 @@ def init_strategy(self): self['buy_order'] = buy_order # Sell Side - if float(self.balance(self.market["quote"])) < sell_price * amount: - InsufficientFundsError(Amount(amount=amount * float(sell_price), asset=self.market["quote"])) + if float(self.balance(self.market["quote"])) < amount: + self.log.critical( + "Insufficient sell balance, needed {} {}".format(amount, self.market['quote']['symbol']) + ) + self.disabled = True else: sell_transaction = self.market.sell( sell_price, @@ -117,7 +124,11 @@ def update_orders(self, new_sell_order, new_buy_order): buy_order_amount = 0 new_buy_amount = buy_order_amount - bought_amount + sold_amount if float(self.balance(self.market["base"])) < new_buy_amount: - InsufficientFundsError(Amount(amount=new_buy_amount, asset=self.market["base"])) + self.log.critical( + 'Insufficient buy balance, needed {} {}'.format(buy_price * new_buy_amount, + self.market['base']['symbol']) + ) + self.disabled = True else: if buy_order: # Cancel the old order @@ -144,7 +155,10 @@ def update_orders(self, new_sell_order, new_buy_order): sell_order_amount = 0 new_sell_amount = sell_order_amount + bought_amount - sold_amount if float(self.balance(self.market["quote"])) < new_sell_amount: - InsufficientFundsError(Amount(amount=new_sell_amount, asset=self.market["quote"])) + self.log.critical( + "Insufficient sell balance, needed {} {}".format(new_sell_amount, self.market["quote"]['symbol']) + ) + self.disabled = True else: if sell_order: # Cancel the old order @@ -204,10 +218,16 @@ def test(self, *args, **kwargs): # Either buy or sell order was changed, update both orders self.update_orders(current_sell_order, current_buy_order) - self.update_gui_profit() + if self.view: + self.update_gui_profit() # GUI updaters def update_gui_profit(self): - # Todo: update gui profit - profit = (self.orders_balance() - self['initial_balance']) / self['initial_balance'] - print(profit) + profit = round((self.orders_balance() - self['initial_balance']) / self['initial_balance'], 3) + idle_add(self.view.set_bot_profit, self.bot_name, profit) + self['profit'] = profit + + def update_gui_slider(self): + # WIP + percentage = '' + idle_add(self.view.update_slider, percentage) diff --git a/dexbot/views/bot_item.py b/dexbot/views/bot_item.py index 58280d703..6d643dbc1 100644 --- a/dexbot/views/bot_item.py +++ b/dexbot/views/bot_item.py @@ -36,7 +36,7 @@ def start_bot(self): self.pause_button.show() self.play_button.hide() - self.controller.create_bot(self.botname, self.config) + self.controller.create_bot(self.botname, self.config, self.view) def pause_bot(self): self.running = False @@ -55,7 +55,12 @@ def set_bot_market(self, value): self.currency_label.setText(value) def set_bot_profit(self, value): - self.bot_profit.setText(value) + if value >= 0: + value = '+' + str(value) + else: + value = '-' + str(value) + value = str(value) + '%' + self.profit_label.setText(value) def remove_widget(self): dialog = ConfirmationDialog('Are you sure you want to remove bot "{}"?'.format(self.botname)) diff --git a/dexbot/views/bot_list.py b/dexbot/views/bot_list.py index 70658984d..24879ef04 100644 --- a/dexbot/views/bot_list.py +++ b/dexbot/views/bot_list.py @@ -39,7 +39,7 @@ 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 + self.bot_widgets[botname] = widget # Todo: Remove the line below this after multi-bot support is added self.ui.add_bot_button.setEnabled(False) @@ -58,17 +58,17 @@ def handle_add_bot(self): def refresh_bot_list(self): pass - def set_bot_name(self, bot_id, value): - self.bot_widgets[bot_id].set_bot_name(value) + def set_bot_name(self, bot_name, value): + self.bot_widgets[bot_name].set_bot_name(value) - def set_bot_account(self, bot_id, value): - self.bot_widgets[bot_id].set_bot_account(value) + def set_bot_account(self, bot_name, value): + self.bot_widgets[bot_name].set_bot_account(value) - def set_bot_profit(self, bot_id, value): - self.bot_widgets[bot_id].set_bot_profit(value) + def set_bot_profit(self, bot_name, value): + self.bot_widgets[bot_name].set_bot_profit(value) - def set_bot_market(self, bot_id, value): - self.bot_widgets[bot_id].set_bot_market(value) + def set_bot_market(self, bot_name, value): + self.bot_widgets[bot_name].set_bot_market(value) def customEvent(self, event): # Process idle_queue_dispatcher events From 3d1d813ea9247d951fa295e42e4de00cb264246e Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 8 Feb 2018 10:51:42 +1100 Subject: [PATCH 0053/1846] major changes to config - use whiptail instead of pythonconfig - have fallback if whiptail not available - auto-discovery of fastest node --- dexbot/bot.py | 4 +- dexbot/cli.py | 40 ++++++------ dexbot/cli_conf.py | 96 ++++++---------------------- dexbot/find_node.py | 60 +++++++++++++++++ dexbot/whiptail.py | 152 ++++++++++++++++++++++++++++++++++++++++++++ setup.py | 1 - 6 files changed, 250 insertions(+), 103 deletions(-) create mode 100644 dexbot/find_node.py create mode 100644 dexbot/whiptail.py diff --git a/dexbot/bot.py b/dexbot/bot.py index 5c2c3a8d6..0285a7931 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -7,8 +7,8 @@ log = logging.getLogger(__name__) # FIXME: currently static list of bot strategies: ? how to enumerate bots available and deploy new bot strategies. -STRATEGIES=[('Echo','dexbot.strategies.echo'), - ("Haywood's Follow Orders",'dexbot.strategies.follow_orders')] +STRATEGIES=[('dexbot.strategies.echo','Echo Test'), + ('dexbot.strategies.follow_orders',"Haywood's Follow Orders")] class BotInfrastructure(): diff --git a/dexbot/cli.py b/dexbot/cli.py index 0d4069656..93ec9ca0e 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -17,7 +17,7 @@ from dexbot.bot import BotInfrastructure -from dexbot.cli_conf import configure_dexbot, QuitException +from dexbot.cli_conf import configure_dexbot log = logging.getLogger(__name__) @@ -81,26 +81,22 @@ def configure(ctx): with open(ctx.obj["configfile"]) as fd: config = yaml.load(fd) else: - config = {} - try: - configure_dexbot(config) - click.clear() - cfg_file = ctx.obj["configfile"] - if not "/" in cfg_file: # use hoke directory by default. - cfg_file = os.path.expanduser("~/"+cfg_file) - with open(cfg_file,"w") as fd: - yaml.dump(config,fd,default_flow_style=False) - click.echo("new configuration saved") - if config['systemd_status'] == 'installed': - # we are already installed - click.echo("restarting dexbot daemon") - os.system("systemctl --user restart dexbot") - if config['systemd_status'] == 'install': - os.system("systemctl --user enable dexbot") - click.echo("starting dexbot daemon") - os.system("systemctl --user start dexbot") - except QuitException: - click.echo("configuration exited: nothing changed") - + config = {} + configure_dexbot(config) + cfg_file = ctx.obj["configfile"] + if not "/" in cfg_file: # save to home directory unless user wants something else + cfg_file = os.path.expanduser("~/"+cfg_file) + with open(cfg_file,"w") as fd: + yaml.dump(config,fd,default_flow_style=False) + click.echo("new configuration saved") + if config['systemd_status'] == 'installed': + # we are already installed + click.echo("restarting dexbot daemon") + os.system("systemctl --user restart dexbot") + if config['systemd_status'] == 'install': + os.system("systemctl --user enable dexbot") + click.echo("starting dexbot daemon") + os.system("systemctl --user start dexbot") + if __name__ == '__main__': main() diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 56a79309f..07a73cb20 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -15,14 +15,9 @@ import importlib, os, os.path, sys, collections, re, tempfile, shutil -import click, whiptail from dexbot.bot import STRATEGIES - - -NODES=[("wss://openledger.hk/ws", "OpenLedger"), - ("wss://dexnode.net/ws", "DEXNode"), - ("wss://node.bitshares.eu/ws", "BitShares.EU")] - +from dexbot.whiptail import get_whiptail +from dexbot.find_node import start_pings, best_node SYSTEMD_SERVICE_NAME=os.path.expanduser("~/.local/share/systemd/user/dexbot.service") @@ -41,66 +36,6 @@ WantedBy=default.target """ -class QuitException(Exception): pass - -class LocalWhiptail(whiptail.Whiptail): - - def __init__(self): - super().__init__(backtitle='dexbot configuration') - - def view_text(self, text): - """Whiptail wants a file but we want to provide a text string""" - fd, nam = tempfile.mkstemp() - f = os.fdopen(fd) - f.write(text) - f.close() - self.view_file(nam) - os.unlink(nam) - -class NoWhiptail: - """ - Imitates the interface of whiptail but uses click only - - This is very basic CLI: real state-of-the-1970s stuff, - but it works *everywhere* - """ - - def prompt(self, msg, default='', password=False): - return click.prompt(msg,default=default,hide_input=password) - - def confirm(self, msg, default='yes'): - return click.confirm(msg,default=(default=='yes')) - - def alert(self, msg): - click.echo( - "[" + - click.style("alert", fg="yellow") + - "] " + msg - ) - - def view_text(self, text): - click.echo_via_pager(text) - - def menu(self, msg='', items=(), prefix=' - ', default=0): - click.echo(msg+'\n') - if type(items) is dict: items = list(items.items()) - i = 1 - for k, v in items: - click.echo("{:>2}) {}".format(i, v)) - i += 1 - click.echo("\n") - ret = click.prompt("Your choice:",type=int,default=default+1) - ret = items[ret-1] - return ret[0] - - def radiolist(self, msg='', items=(), prefix=' - '): - d = 0 - default = 0 - for k, v, s in items: - if s == "ON": - default = d - d += 1 - self.menu(msg,items,default=default) def select_choice(current,choices): """for the radiolist, get us a list with the current value selected""" @@ -168,8 +103,7 @@ def setup_systemd(d,config): def configure_bot(d,bot): strategy = bot.get('module','dexbot.strategies.echo') - bot['module'] = d.radiolist("Choose a bot strategy", - choices=select_choice(strategy,STRATEGIES)) + bot['module'] = d.radiolist("Choose a bot strategy",select_choice(strategy,STRATEGIES)) bot['bot'] = 'Strategy' # its always Strategy now, for backwards compatibilty only # import the bot class but we don't __init__ it here klass = getattr( @@ -188,24 +122,30 @@ def configure_bot(d,bot): def configure_dexbot(config): - if shutil.which("whiptail"): - d = LocalWhiptail() - else: - d = NoWhiptail() # use our own fake whiptail - config['node'] = d.radiolist("Choose a Witness node to use",choices=select_choice(config.get("node"),NODES)) + d = get_whiptail() + if not 'node' in config: + # start our best node search in the background + ping_results = start_pings() bots = config.get('bots',{}) if len(bots) == 0: - txt = d.prompt("Your name for the bot") + txt = d.prompt("Your name for the first bot") config['bots'] = {txt:configure_bot(d,{})} else: - botname = d.menu("Select bot to edit", - choices=[(i,i) for i in bots]+[('NEW','New bot')]) + botname = d.menu("Select bot to edit",[(i,i) for i in bots]+[('NEW','New bot')]) if botname == 'NEW': - txt = d.prompt("Your name for the bot") + txt = d.prompt("Your name for the new bot") config['bots'][txt] = configure_bot(d,{}) else: config['bots'][botname] = configure_bot(d,config['bots'][botname]) + if not 'node' in config: + node = best_node(ping_results) + if node: + config['node'] = node + else: + # search failed, ask the user + config['node'] = d.prompt("Search for best BitShares node failed.\n\nPlease enter wss:// url of chosen node.") setup_systemd(d,config) + d.clear() return config if __name__=='__main__': diff --git a/dexbot/find_node.py b/dexbot/find_node.py new file mode 100644 index 000000000..e146c9e62 --- /dev/null +++ b/dexbot/find_node.py @@ -0,0 +1,60 @@ +""" +Routines for finding the closest node +""" + +# list kindly provided by Cryptick from the DEXBot Telegram channel +ALL_NODES = ["wss://eu.openledger.info/ws", + "wss://bitshares.openledger.info/ws", + "wss://dexnode.net/ws", + "wss://japan.bitshares.apasia.tech/ws", + "wss://bitshares-api.wancloud.io/ws", + "wss://openledger.hk/ws", + "wss://bitshares.apasia.tech/ws", + "wss://uptick.rocks" + "wss://bitshares.crypto.fans/ws", + "wss://kc-us-dex.xeldal.com/ws", + "wss://api.bts.blckchnd.com", + "wss://btsza.co.za:8091/ws", + "wss://bitshares.dacplay.org/ws", + "wss://bit.btsabc.org/ws", + "wss://bts.ai.la/ws", + "wss://ws.gdex.top", + "wss://us.nodes.bitshares.works", + "wss://eu.nodes.bitshares.works", + "wss://sg.nodes.bitshares.works"] + +import re +from urllib.parse import urlsplit +from subprocess import Popen, STDOUT, PIPE +from platform import system + +if system() == 'Windows': + ping_cmd = lambda x: ('ping','-n','5','-w','1500',x) + ping_re = re.compile(r'Average = ([\d.]+)ms') +else: + ping_cmd = lambda x: ('ping','-c5','-n','-w5','-i0.3',x) + ping_re = re.compile(r'min/avg/max/mdev = [\d.]+/([\d.]+)') + +def make_ping_proc(host): + host = urlsplit(host).netloc.split(':')[0] + return Popen(ping_cmd(host),stdout=PIPE, stderr=STDOUT, universal_newlines=True) + +def process_ping_result(host,proc): + out = proc.communicate()[0] + try: + return (float(ping_re.search(out).group(1)),host) + except AttributeError: + return (1000000,host) # hosts that fail are last + +def start_pings(): + return [(i,make_ping_proc(i)) for i in ALL_NODES] + +def best_node(results): + try: + r = sorted([process_ping_result(*i) for i in results]) + return r[0][1] + except: + return None + +if __name__=='__main__': + print(best_node(start_pings())) diff --git a/dexbot/whiptail.py b/dexbot/whiptail.py new file mode 100644 index 000000000..698cac0d2 --- /dev/null +++ b/dexbot/whiptail.py @@ -0,0 +1,152 @@ +from __future__ import print_function +import sys +import shlex, shutil +import itertools +import click +from subprocess import Popen, PIPE +from collections import namedtuple + + +# whiptail.py - Use whiptail to display dialog boxes from shell scripts +# Copyright (C) 2013 Marwan Alsabbagh +# license: BSD, see LICENSE for more details. +# we have to bring this module in to fix up Python 3 problems + +Response = namedtuple('Response', 'returncode value') + + +def flatten(data): + return list(itertools.chain.from_iterable(data)) + +class Whiptail: + + def __init__(self, title='', backtitle='', height=20, width=60, + auto_exit=True): + self.title = title + self.backtitle = backtitle + self.height = height + self.width = width + self.auto_exit = auto_exit + + def run(self, control, msg, extra=(), exit_on=(1, 255)): + cmd = [ + 'whiptail', '--title', self.title, '--backtitle', self.backtitle, + '--' + control, msg, str(self.height), str(self.width) + ] + cmd += list(extra) + p = Popen(cmd, stderr=PIPE) + out, err = p.communicate() + if self.auto_exit and p.returncode in exit_on: + print('User cancelled operation.') + sys.exit(p.returncode) + return Response(p.returncode, str(err,'utf-8','ignore')) + + def prompt(self, msg, default='', password=False): + control = 'passwordbox' if password else 'inputbox' + return self.run(control, msg, [default]).value + + def confirm(self, msg, default='yes'): + defaultno = '--defaultno' if default == 'no' else '' + return self.run('yesno', msg, [defaultno], [255]).returncode == 0 + + def alert(self, msg): + self.run('msgbox', msg) + + def view_file(self, path): + self.run('textbox', path, ['--scrolltext']) + + def calc_height(self, msg): + height_offset = 8 if msg else 7 + return [str(self.height - height_offset)] + + def menu(self, msg='', items=(), prefix=' - '): + if isinstance(items[0], string_types): + items = [(i, '') for i in items] + else: + items = [(k, prefix + v) for k, v in items] + extra = self.calc_height(msg) + flatten(items) + return self.run('menu', msg, extra).value + + def showlist(self, control, msg, items, prefix): + if isinstance(items[0], str): + items = [(i, '', 'OFF') for i in items] + else: + items = [(k, prefix + v, s) for k, v, s in items] + extra = self.calc_height(msg) + flatten(items) + return shlex.split(self.run(control, msg, extra).value) + + def radiolist(self, msg='', items=(), prefix=' - '): + return self.showlist('radiolist', msg, items, prefix)[0] + + def checklist(self, msg='', items=(), prefix=' - '): + return self.showlist('checklist', msg, items, prefix) + + def view_text(self, text): + """Whiptail wants a file but we want to provide a text string""" + fd, nam = tempfile.mkstemp() + f = os.fdopen(fd) + f.write(text) + f.close() + self.view_file(nam) + os.unlink(nam) + + + def clear(self): + # tidy up the screen + click.clear() + +class NoWhiptail: + """ + Imitates the interface of whiptail but uses click only + + This is very basic CLI: real state-of-the-1970s stuff, + but it works *everywhere* + """ + + def prompt(self, msg, default='', password=False): + return click.prompt(msg,default=default,hide_input=password) + + def confirm(self, msg, default='yes'): + return click.confirm(msg,default=(default=='yes')) + + def alert(self, msg): + click.echo( + "[" + + click.style("alert", fg="yellow") + + "] " + msg + ) + + def view_text(self, text): + click.echo_via_pager(text) + + def menu(self, msg='', items=(), prefix=' - ', default=0): + click.echo(msg+'\n') + if type(items) is dict: items = list(items.items()) + i = 1 + for k, v in items: + click.echo("{:>2}) {}".format(i, v)) + i += 1 + click.echo("\n") + ret = click.prompt("Your choice:",type=int,default=default+1) + ret = items[ret-1] + return ret[0] + + def radiolist(self, msg='', items=(), prefix=' - '): + d = 0 + default = 0 + for k, v, s in items: + if s == "ON": + default = d + d += 1 + return self.menu(msg,[(k,v) for k,v,s in items],default=default) + + + def clear(self): + pass # dont tidy the screen + +def get_whiptail(): + if shutil.which("whyptail"): + d = Whiptail() + else: + d = NoWhiptail() # use our own fake whiptail + return d diff --git a/setup.py b/setup.py index da0ac5d2e..242e60479 100755 --- a/setup.py +++ b/setup.py @@ -43,7 +43,6 @@ "sqlalchemy", "appdirs", "pyqt5", - "whiptail", "sdnotify" ], include_package_data=True, From d5cc48d1e82b1d66290f37f5723714b281de7c98 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 8 Feb 2018 10:53:59 +1100 Subject: [PATCH 0054/1846] documentation changes for easyconfig --- docs/configuration.rst | 59 +++++++++++++++++++++++++----------------- docs/setup.rst | 46 +++++++++++++++++++++++++------- 2 files changed, 71 insertions(+), 34 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 388d5e413..7fd521bc7 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -3,39 +3,52 @@ Configuration Questions The configuration consists of a series of questions about the bots you wish to configure. -1. the Node. - You need to set the public node that gives access to the BitShares blockchain. Pick the one closest to you. - - (Please submit a `Github bug report `_ if you think items need to added or removed from this list). - -2. The Bot Name. +1. The Bot Name. - Choose a unique name for your bot, DEXBot doesn't care what you call it. It is used to identify the bot in the logs so should be fairly short. + Choose a unique name for your bot, DEXBot doesn't care what you call it. + It is used to identify the bot in the logs so should be fairly short. -3. The Bot Strategy +2. The Bot Strategy DEXBot provides a number of different bot strategies. They can be quite different in how they behave (i.e. spend *your* money) so it is important you understand the strategy before deploying a bot. a. :doc:`echo` For testing this just logs events on a market, does no trading. - b. :doc:`wall` a basic liquidity bot maintaining a "wall" of buy and sell orders around the market price. + b. :doc:`follow_orders` My (Ian Haywood) main bot, an extension of stakemachine's `wall`, + it has been used to provide liquidity on AUD:BTS. + Does function but by no mean perfect, see caveats in the docs. - Technically the questions that follow are determined by the strategy chosen, and each strategy will have its own questions around - amounts to trade, spreads etc. See the strategy documentations linked above. But two questions are nearly universal among strategies - so are documented here. +3. Strategy-specific questions -3. The Account. + The questions that follow are determined by the strategy chosen, and each strategy will have its own questions around + amounts to trade, spreads etc. See the strategy documentations linked above. But the first two strategy questions + are nearly universal amongst the strategies so are documented here: + + a. The Account. - This is the same account name as the one where you entered the keys into ``uptick`` earlier on: the bot must - always have the private key so it can execute trades. + This is the same account name as the one where you entered the keys into ``uptick`` earlier on: the bot must + always have the private key so it can execute trades. -4. The Market. + b. The Market. - This is the main market the bot trade on. They are specified by the quote asset, a colon (:), and the base asset, for example - the market for BitShares priced in US dollars is called BTS:USD. BitShares always provides a "reverse" market so - there will be a USD:BTS with the same trades, the only difference is the prices will be the inverse (1/x) of BTS:USD. + This is the main market the bot trade on. They are specified by the quote asset, a colon (:), and the base asset, for example + the market for BitShares priced in US dollars is called BTS:USD. BitShares always provides a "reverse" market so + there will be a USD:BTS with the same trades, the only difference is the prices will be the inverse (1/x) of BTS:USD. + +4. the Node. + + DEXBot needs to have a public node (also called "witness") that gives access to the BitShares blockchain. + + The configuration tool will ping a standard list of nodes and use the one with the least latency. If this fails + (most likely because you are not online), the config tool will ask you to enter a value here. + + If think this process is wrong or the list should have servers added/removed (see ``dexbot/find_nodes.py``)) + please file a + `Github bug report `_ . + + If you run your own witness node then you can edit ``config.yml`` to change the node value. 5. Systemd. @@ -43,7 +56,9 @@ The configuration consists of a series of questions about the bots you wish to c as a background service, this will run continuously in the background whenever you are logged in. if you enabled lingering as described, it wil run whenever the computer is turned on. -6. If you select yes above, the final question will be the password you entered to protect the private key with ``uptick``. +6. The Passphrase + + If you select yes above, the final question will be the password you entered to protect the private key with ``uptick``. Entering it here is a security risk: the configuration tool will save the password to a file on the computer. This means anyone with access to the computer can access your private key and spend the money in your account. @@ -61,7 +76,3 @@ If you are not using systemd, the bot can be run manually by:: It will ask for your wallet passphrase (that you have provide when adding your private key to pybitshares using ``uptick addkey``). - -If you want to prevent the password dialog, you can predefine an -environmental variable ``UNLOCK``, if you understand the security -implications. diff --git a/docs/setup.rst b/docs/setup.rst index 0c8a5e6bc..14978dc44 100644 --- a/docs/setup.rst +++ b/docs/setup.rst @@ -11,24 +11,36 @@ To run in the background you need systemd and *lingering* enabled:: On some systems, such as the Raspberry Pi, you need to reboot for this to take effect. -You need to have python3 installed, including the ``pip`` tool, and the development tools for C extensions. -Plus for the configuration you need the ``dialog`` command. +You need to have python3 installed, including the ``pip`` tool, and the development tools for C extensions, and +the OpenSSL libraries. -On Ubuntu/Debian type systems:: +Plus for the easy configuration you need the ``whiptail`` command. - sudo apt-get install dialog python3-pip python3-dev +On Ubuntu:: + sudo add-apt-repository universe + sudo apt-get update + sudo apt-get install libssl-dev python3-pip python3-dev whiptail -On other distros you need to check the documentation for how to install packages, the names should be very similar. +On Debian/Raspian:: + + sudo apt-get install libssl-dev python3-pip python3-dev whiptail + +On CentOS/RedHat:: + + sudo yum install -y epel-release + sudo yum install openssl-devel python34-pip python34-devel newt + +On other distros you need to check the documentation for how to install these packages, the names should be very similar. Installation ------------ :: - pip3 install git+https://github.com/Codaone/DEXBot.git [--user] + pip3 install -If you install using the ``--user`` flag, the binaries of +If you add the ``--user`` flag to this command, the binaries of ``dexbot`` and ``uptick`` are located in ``~/.local/bin``. Otherwise they should be globally reachable. @@ -41,12 +53,26 @@ bot's account into a local wallet. This can be done using uptick addkey -Easy Configuration ------------------- +You can get your private key from the BitShares Web Wallet: click the menu on the top right, +then "Settings", "Accounts", "View keys", then tab "Owner Permissions", click +on the public key, then "Show". + +Look for the private key in Wallet Import Format (WIF), it's a "5" followed +by a long list of letters. Select, copy and paste this into the screen +where uptick asks for the key. + +Check ``uptick`` successfully imported the key with:: + + uptick listaccounts + +Yes, this process is a pain but for security reasons this part probably won't ever be "easy". + +Configuration +------------- ``dexbot`` can be configured using:: - dexbot configure + dexbot configure This will walk you through the configuration process. Read more about this in the :doc:`configuration`. From 058a3f75f8e3876da760cf6ee4e356645815182d Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 8 Feb 2018 15:27:58 +1100 Subject: [PATCH 0055/1846] fix bug in whiptail init --- dexbot/whiptail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/whiptail.py b/dexbot/whiptail.py index 698cac0d2..bae67651f 100644 --- a/dexbot/whiptail.py +++ b/dexbot/whiptail.py @@ -60,7 +60,7 @@ def calc_height(self, msg): return [str(self.height - height_offset)] def menu(self, msg='', items=(), prefix=' - '): - if isinstance(items[0], string_types): + if isinstance(items[0], str): items = [(i, '') for i in items] else: items = [(k, prefix + v) for k, v in items] @@ -145,7 +145,7 @@ def clear(self): pass # dont tidy the screen def get_whiptail(): - if shutil.which("whyptail"): + if shutil.which("whiptail"): d = Whiptail() else: d = NoWhiptail() # use our own fake whiptail From 7805540a044f9e0c5a48d6f204efba1ca59b8643 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 9 Feb 2018 13:45:36 +0200 Subject: [PATCH 0056/1846] Change Storage to use a worker to handle database access --- dexbot/storage.py | 145 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 100 insertions(+), 45 deletions(-) diff --git a/dexbot/storage.py b/dexbot/storage.py index ec9acfc3f..80907d3a7 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -1,6 +1,9 @@ import os import json -import sqlalchemy +import threading +import queue +import uuid +import time from sqlalchemy import create_engine, Table, Column, String, Integer, MetaData from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker @@ -49,56 +52,122 @@ def __init__(self, category): self.category = category def __setitem__(self, key, value): + worker.execute(worker.set_item, self.category, key, value) + + def __getitem__(self, key): + return worker.execute(worker.get_item, self.category, key) + + def __delitem__(self, key): + worker.execute(worker.del_item, self.category, key) + + def __contains__(self, key): + return worker.execute(worker.contains, self.category, key) + + def items(self): + return worker.execute(worker.get_items, self.category) + + def clear(self): + worker.execute(worker.clear, self.category) + + +class DatabaseWorker(threading.Thread): + """ + Thread safe database worker + """ + + def __init__(self): + super().__init__() + + # Obtain engine and session + engine = create_engine('sqlite:///%s' % sqlDataBaseFile, echo=False) + Session = sessionmaker(bind=engine) + self.session = Session() + Base.metadata.create_all(engine) + self.session.commit() + + self.task_queue = queue.Queue() + self.results = {} + + self.daemon = True + self.start() + + def run(self): + for func, args, token in iter(self.task_queue.get, None): + func(*args, token) + + def get_result(self, token): + delay = 0.001 + while True: + if token in self.results: + return_value = self.results[token] + del self.results[token] + return return_value + + time.sleep(delay) + if delay < 5: + delay += delay + + def execute(self, func, *args): + token = str(uuid.uuid4) + self.task_queue.put((func, args, token)) + return self.get_result(token) + + def set_item(self, category, key, value, token): value = json.dumps(value) - e = session.query(Config).filter_by( - category=self.category, + e = self.session.query(Config).filter_by( + category=category, key=key ).first() if e: e.value = value else: - e = Config(self.category, key, value) - session.add(e) - session.commit() - - def __getitem__(self, key): - e = session.query(Config).filter_by( - category=self.category, + e = Config(category, key, value) + self.session.add(e) + self.session.commit() + self.results[token] = None + + def get_item(self, category, key, token): + e = self.session.query(Config).filter_by( + category=category, key=key ).first() if not e: - return None + result = None else: - return json.loads(e.value) + result = json.loads(e.value) + self.results[token] = result - def __delitem__(self, key): - e = session.query(Config).filter_by( - category=self.category, + def del_item(self, category, key, token): + e = self.session.query(Config).filter_by( + category=category, key=key ).first() - session.delete(e) - session.commit() + self.session.delete(e) + self.session.commit() + self.results[token] = None - def __contains__(self, key): - e = session.query(Config).filter_by( - category=self.category, + def contains(self, category, key, token): + e = self.session.query(Config).filter_by( + category=category, key=key ).first() - return bool(e) + self.results[token] = bool(e) - def items(self): - es = session.query(Config).filter_by( - category=self.category + def get_items(self, category, token): + es = self.session.query(Config).filter_by( + category=category ).all() - return [(e.key, e.value) for e in es] + result = [(e.key, e.value) for e in es] + self.results[token] = result - def clear(self): - rows = session.query(Config).filter_by( - category=self.category + def clear(self, category, token): + rows = self.session.query(Config).filter_by( + category=category ) for row in rows: - session.delete(row) - session.commit() + self.session.delete(row) + self.session.commit() + self.results[token] = None # Derive sqlite file directory @@ -108,18 +177,4 @@ def clear(self): # Create directory for sqlite file mkdir_p(data_dir) -# Obtain engine and session -engine = create_engine('sqlite:///%s' % sqlDataBaseFile, echo=False) -Session = sessionmaker(bind=engine) -session = Session() -Base.metadata.create_all(engine) -session.commit() - -if __name__ == "__main__": - storage = Storage("test") - storage["foo"] = "bar" - storage["foo1"] = "bar" - storage["foo3"] = "bar" - print(storage.items()) - print("foo" in storage) - print("bar" in storage) +worker = DatabaseWorker() From 8037a6b433dfe2cf01ee20e8be099875554557c7 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 9 Feb 2018 15:46:38 +0200 Subject: [PATCH 0057/1846] Add bot slider updating code --- dexbot/strategies/simple.py | 40 ++++++++++++---- dexbot/views/bot_item.py | 3 ++ dexbot/views/bot_list.py | 3 ++ dexbot/views/gen/bot_item_widget.py | 57 ++++++++++++++++------- dexbot/views/orig/bot_item_widget.ui | 68 ++++++++++++++++++++++++++-- 5 files changed, 144 insertions(+), 27 deletions(-) diff --git a/dexbot/strategies/simple.py b/dexbot/strategies/simple.py index e6f700fc9..cfbbb20db 100644 --- a/dexbot/strategies/simple.py +++ b/dexbot/strategies/simple.py @@ -27,12 +27,16 @@ def __init__(self, *args, **kwargs): # Counter for blocks self.counter = Counter() - # Tests for actions self.price = self.bot.get("target", {}).get("center_price", 0) - + self.initial_balance = self['initial_balance'] or 0 self.bot_name = kwargs.get('name') self.view = kwargs.get('view') + # Update GUI + if self.view: + self.update_gui_profit() + self.update_gui_slider() + def error(self, *args, **kwargs): self.disabled = True self.cancel_all() @@ -83,7 +87,9 @@ def init_strategy(self): if sell_order: self['sell_order'] = sell_order - self['initial_balance'] = self.orders_balance() + 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): """ @@ -220,14 +226,32 @@ def test(self, *args, **kwargs): if self.view: self.update_gui_profit() + self.update_gui_slider() # GUI updaters def update_gui_profit(self): - profit = round((self.orders_balance() - self['initial_balance']) / self['initial_balance'], 3) - idle_add(self.view.set_bot_profit, self.bot_name, profit) + if self.initial_balance: + 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)) self['profit'] = profit def update_gui_slider(self): - # WIP - percentage = '' - idle_add(self.view.update_slider, percentage) + buy_order = self['buy_order'] + if buy_order: + buy_amount = buy_order['quote']['amount'] + else: + buy_amount = 0 + sell_order = self['sell_order'] + if sell_order: + sell_amount = sell_order['base']['amount'] + else: + sell_amount = 0 + + total = buy_amount + sell_amount + if not total: # Prevent division by zero + percentage = 0 + else: + percentage = (buy_amount / total) * 100 + idle_add(self.view.set_bot_slider, self.bot_name, percentage) diff --git a/dexbot/views/bot_item.py b/dexbot/views/bot_item.py index 6d643dbc1..d81b414b1 100644 --- a/dexbot/views/bot_item.py +++ b/dexbot/views/bot_item.py @@ -62,6 +62,9 @@ def set_bot_profit(self, value): value = str(value) + '%' self.profit_label.setText(value) + def set_bot_slider(self, value): + self.order_slider.setSliderPosition(50) + def remove_widget(self): dialog = ConfirmationDialog('Are you sure you want to remove bot "{}"?'.format(self.botname)) return_value = dialog.exec_() diff --git a/dexbot/views/bot_list.py b/dexbot/views/bot_list.py index 24879ef04..c3925cd6f 100644 --- a/dexbot/views/bot_list.py +++ b/dexbot/views/bot_list.py @@ -70,6 +70,9 @@ def set_bot_profit(self, bot_name, 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/gen/bot_item_widget.py b/dexbot/views/gen/bot_item_widget.py index c91cf28e9..2a73205c2 100644 --- a/dexbot/views/gen/bot_item_widget.py +++ b/dexbot/views/gen/bot_item_widget.py @@ -143,6 +143,30 @@ def setupUi(self, widget): self.verticalLayout_4.setObjectName("verticalLayout_4") spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.verticalLayout_4.addItem(spacerItem) + self.widget_7 = QtWidgets.QWidget(self.widget_4) + self.widget_7.setObjectName("widget_7") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.widget_7) + self.horizontalLayout.setContentsMargins(0, -1, 0, 0) + self.horizontalLayout.setObjectName("horizontalLayout") + self.buy_label = QtWidgets.QLabel(self.widget_7) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.buy_label.setFont(font) + self.buy_label.setStyleSheet("color: #005B78;") + self.buy_label.setObjectName("buy_label") + self.horizontalLayout.addWidget(self.buy_label) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem1) + self.sell_label = QtWidgets.QLabel(self.widget_7) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.sell_label.setFont(font) + self.sell_label.setStyleSheet("color: #005B78;") + self.sell_label.setObjectName("sell_label") + self.horizontalLayout.addWidget(self.sell_label) + self.verticalLayout_4.addWidget(self.widget_7) self.widget_2 = QtWidgets.QWidget(self.widget_4) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) @@ -152,16 +176,16 @@ def setupUi(self, widget): self.widget_2.setStyleSheet("") self.widget_2.setObjectName("widget_2") self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.widget_2) - self.horizontalLayout_3.setContentsMargins(5, -1, 5, 0) + self.horizontalLayout_3.setContentsMargins(5, 0, 5, 0) self.horizontalLayout_3.setObjectName("horizontalLayout_3") - self.horizontalSlider = QtWidgets.QSlider(self.widget_2) - self.horizontalSlider.setEnabled(False) + self.order_slider = QtWidgets.QSlider(self.widget_2) + self.order_slider.setEnabled(False) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.horizontalSlider.sizePolicy().hasHeightForWidth()) - self.horizontalSlider.setSizePolicy(sizePolicy) - self.horizontalSlider.setStyleSheet("QSlider::groove:horizontal {\n" + sizePolicy.setHeightForWidth(self.order_slider.sizePolicy().hasHeightForWidth()) + self.order_slider.setSizePolicy(sizePolicy) + self.order_slider.setStyleSheet("QSlider::groove:horizontal {\n" "height: 2px;\n" "background: #005B78;\n" "}\n" @@ -174,13 +198,13 @@ def setupUi(self, widget): "border-left: 2px solid #005B78;\n" "border-right: 2px solid #005B78;\n" "}") - self.horizontalSlider.setMaximum(100) - self.horizontalSlider.setSliderPosition(50) - self.horizontalSlider.setTracking(False) - self.horizontalSlider.setOrientation(QtCore.Qt.Horizontal) - self.horizontalSlider.setTickPosition(QtWidgets.QSlider.TicksAbove) - self.horizontalSlider.setObjectName("horizontalSlider") - self.horizontalLayout_3.addWidget(self.horizontalSlider) + self.order_slider.setMaximum(100) + self.order_slider.setSliderPosition(50) + self.order_slider.setTracking(False) + self.order_slider.setOrientation(QtCore.Qt.Horizontal) + self.order_slider.setTickPosition(QtWidgets.QSlider.TicksAbove) + self.order_slider.setObjectName("order_slider") + self.horizontalLayout_3.addWidget(self.order_slider) self.verticalLayout_4.addWidget(self.widget_2) self.gridLayout.addWidget(self.widget_4, 0, 1, 2, 1) self.widget_5 = QtWidgets.QWidget(self.widget_frame) @@ -205,7 +229,6 @@ def setupUi(self, widget): "") self.pause_button.setText("") icon2 = QtGui.QIcon() - icon2.addPixmap(QtGui.QPixmap("dexbot/img/play.png"), QtGui.QIcon.Normal, QtGui.QIcon.On) icon2.addPixmap(QtGui.QPixmap("dexbot/img/pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.pause_button.setIcon(icon2) self.pause_button.setIconSize(QtCore.QSize(30, 30)) @@ -227,8 +250,8 @@ def setupUi(self, widget): self.play_button.setIconSize(QtCore.QSize(30, 30)) self.play_button.setObjectName("play_button") self.horizontalLayout_2.addWidget(self.play_button) - spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_2.addItem(spacerItem1) + spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_2.addItem(spacerItem2) self.gridLayout.addWidget(self.widget_5, 1, 0, 1, 1) self.gridLayout_6.addLayout(self.gridLayout, 3, 0, 1, 1) self.gridLayout_2.addWidget(self.widget_frame, 0, 0, 1, 1) @@ -243,4 +266,6 @@ def retranslateUi(self, widget): self.strategy_label.setText(_translate("widget", "SIMPLE STRATEGY")) self.currency_label.setText(_translate("widget", "BTS/USD")) self.profit_label.setText(_translate("widget", "+0.000%")) + self.buy_label.setText(_translate("widget", "Buy")) + self.sell_label.setText(_translate("widget", "Sell")) diff --git a/dexbot/views/orig/bot_item_widget.ui b/dexbot/views/orig/bot_item_widget.ui index c719ea13c..1873b1440 100644 --- a/dexbot/views/orig/bot_item_widget.ui +++ b/dexbot/views/orig/bot_item_widget.ui @@ -308,6 +308,66 @@ + + + + + 0 + + + 0 + + + 0 + + + + + + 75 + true + + + + color: #005B78; + + + Buy + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 75 + true + + + + color: #005B78; + + + Sell + + + + + + @@ -323,6 +383,9 @@ 5 + + 0 + 5 @@ -330,7 +393,7 @@ 0 - + false @@ -425,8 +488,7 @@ border-right: 2px solid #005B78; - dexbot/img/pause.png - dexbot/img/play.pngdexbot/img/pause.png + dexbot/img/pause.pngdexbot/img/pause.png From 12c3bc01e27c8b55130ee82cbc89dab9905edc59 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 12 Feb 2018 13:10:06 +0200 Subject: [PATCH 0058/1846] Fix bot running and stopping --- dexbot/bot.py | 14 +++++++++++--- dexbot/controllers/main_controller.py | 17 ++++++++++++----- setup.py | 6 +++++- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/dexbot/bot.py b/dexbot/bot.py index 9ac898613..020b68b09 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -2,7 +2,7 @@ import sys import logging import os.path -from threading import Thread +import threading from dexbot.basestrategy import BaseStrategy @@ -18,7 +18,7 @@ # GUIs can add a handler to this logger to get a stream of events re the running bots. -class BotInfrastructure(Thread): +class BotInfrastructure(threading.Thread): bots = dict() @@ -29,6 +29,7 @@ def __init__( view=None ): super().__init__() + # BitShares instance self.bitshares = bitshares_instance or shared_bitshares_instance() @@ -120,8 +121,15 @@ def on_account(self, accountupdate): def run(self): self.notify.listen() + def stop(self): + self.notify.websocket.close() + + def remove_bot(self): + for bot in self.bots: + self.bots[bot].purge() + @staticmethod - def remove_bot(config, bot_name): + def remove_offline_bot(config, bot_name): # Initialize the base strategy to get control over the data strategy = BaseStrategy(config, bot_name) strategy.purge() diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index bf123a584..f2dc065e6 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -14,21 +14,28 @@ def __init__(self, bitshares_instance): self.bot_template = BotInfrastructure def create_bot(self, botname, 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 def stop_bot(self, bot_name): - self.bots[bot_name].terminate() + self.bots[bot_name].stop() self.bots.pop(bot_name, None) def remove_bot(self, bot_name): + # Todo: Add some threading here so that the GUI doesn't freeze if bot_name in self.bots: - self.bots[bot_name].terminate() + # Bot currently running + self.bots[bot_name].remove_bot() + self.bots[bot_name].stop() + self.bots.pop(bot_name, None) + else: + # Bot not running + config = self.get_bot_config(bot_name) + self.bot_template.remove_offline_bot(config, bot_name) - # Todo: Add some threading here so that the GUI doesn't freeze - config = self.get_bot_config(bot_name) - self.bot_template.remove_bot(config, bot_name) self.remove_bot_config(bot_name) @staticmethod diff --git a/setup.py b/setup.py index 355c7c6a4..f5ffad76c 100755 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ ], }, install_requires=[ - "bitshares>=0.1.10", + "bitshares==0.1.11.beta", "uptick>=0.1.4", "prettytable", "click", @@ -45,5 +45,9 @@ "pyqt5", "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 40607f7c50b21f7e447ee852c1f40aa4fa50bdf2 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 12 Feb 2018 13:49:49 +0200 Subject: [PATCH 0059/1846] Add profit and slider updating on start up Currently it only fetches the latest value from the database. The values should be fetched directly from the blockchain in a future version. --- dexbot/views/bot_item.py | 11 ++++++++++- dexbot/views/gen/bot_item_widget.py | 2 +- dexbot/views/orig/bot_item_widget.ui | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/dexbot/views/bot_item.py b/dexbot/views/bot_item.py index d81b414b1..b83b1d74f 100644 --- a/dexbot/views/bot_item.py +++ b/dexbot/views/bot_item.py @@ -2,6 +2,7 @@ from dexbot.views.gen.bot_item_widget import Ui_widget from dexbot.views.confirmation import ConfirmationDialog +from dexbot.storage import worker class BotItemWidget(QtWidgets.QWidget, Ui_widget): @@ -31,6 +32,14 @@ def setup_ui_data(self, config): 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() @@ -63,7 +72,7 @@ def set_bot_profit(self, value): self.profit_label.setText(value) def set_bot_slider(self, value): - self.order_slider.setSliderPosition(50) + self.order_slider.setSliderPosition(value) def remove_widget(self): dialog = ConfirmationDialog('Are you sure you want to remove bot "{}"?'.format(self.botname)) diff --git a/dexbot/views/gen/bot_item_widget.py b/dexbot/views/gen/bot_item_widget.py index 2a73205c2..141c0df87 100644 --- a/dexbot/views/gen/bot_item_widget.py +++ b/dexbot/views/gen/bot_item_widget.py @@ -265,7 +265,7 @@ def retranslateUi(self, widget): self.botname_label.setText(_translate("widget", "Botname")) self.strategy_label.setText(_translate("widget", "SIMPLE STRATEGY")) self.currency_label.setText(_translate("widget", "BTS/USD")) - self.profit_label.setText(_translate("widget", "+0.000%")) + self.profit_label.setText(_translate("widget", "+0.0%")) self.buy_label.setText(_translate("widget", "Buy")) self.sell_label.setText(_translate("widget", "Sell")) diff --git a/dexbot/views/orig/bot_item_widget.ui b/dexbot/views/orig/bot_item_widget.ui index 1873b1440..f702771fb 100644 --- a/dexbot/views/orig/bot_item_widget.ui +++ b/dexbot/views/orig/bot_item_widget.ui @@ -268,7 +268,7 @@ 1 - +0.000% + +0.0% Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter From a218ac1199bc4b07862983b1b30e07f78471062b Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 13 Feb 2018 15:17:24 +0200 Subject: [PATCH 0060/1846] Add hooks for pyinstaller --- hooks/hook-Crypto.py | 10 ++++++++++ hooks/rthook-Crypto.py | 15 +++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 hooks/hook-Crypto.py create mode 100644 hooks/rthook-Crypto.py diff --git a/hooks/hook-Crypto.py b/hooks/hook-Crypto.py new file mode 100644 index 000000000..5048d1c78 --- /dev/null +++ b/hooks/hook-Crypto.py @@ -0,0 +1,10 @@ +# Hook for pycryptodome extensions + +hiddenimports = [ + 'Crypto.Cipher._chacha20', + 'Crypto.Cipher._raw_aes', + 'Crypto.Cipher._raw_ecb', + 'Crypto.Hash._SHA256', + 'Crypto.Util._cpuid', + 'Crypto.Util._strxor', +] diff --git a/hooks/rthook-Crypto.py b/hooks/rthook-Crypto.py new file mode 100644 index 000000000..e18378ec6 --- /dev/null +++ b/hooks/rthook-Crypto.py @@ -0,0 +1,15 @@ +# Runtime hook for pycryptodome extensions + +import Crypto.Util._raw_api +import importlib.machinery +import os.path +import sys + +def load_raw_lib(name, cdecl): + for ext in importlib.machinery.EXTENSION_SUFFIXES: + try: + return Crypto.Util._raw_api.load_lib(os.path.join(sys._MEIPASS, name + ext), cdecl) + except OSError: + pass + +Crypto.Util._raw_api.load_pycryptodome_raw_lib = load_raw_lib From 9dac8f46608aeb7e630dc98a15c040e099fc18ca Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 13 Feb 2018 15:26:59 +0200 Subject: [PATCH 0061/1846] Fix simple strategy logic --- dexbot/strategies/simple.py | 56 +++++++++++++++---------------------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/dexbot/strategies/simple.py b/dexbot/strategies/simple.py index cfbbb20db..5678152a5 100644 --- a/dexbot/strategies/simple.py +++ b/dexbot/strategies/simple.py @@ -2,6 +2,7 @@ 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 @@ -28,40 +29,32 @@ def __init__(self, *args, **kwargs): 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.initial_balance = self['initial_balance'] or 0 self.bot_name = kwargs.get('name') self.view = kwargs.get('view') - # Update GUI - if self.view: - self.update_gui_profit() - self.update_gui_slider() - def error(self, *args, **kwargs): self.disabled = True - self.cancel_all() - self.clear() self.log.info(self.execute()) def init_strategy(self): # Target target = self.bot.get("target", {}) - # prices - buy_price = self.price * (1 - (target["spread"] / 2) / 100) - sell_price = self.price * (1 + (target["spread"] / 2) / 100) - amount = target['amount'] / 2 # Buy Side - if float(self.balance(self.market["base"])) < buy_price * amount: + if float(self.balance(self.market["base"])) < self.buy_price * amount: self.log.critical( - 'Insufficient buy balance, needed {} {}'.format(buy_price * amount, self.market['base']['symbol']) + 'Insufficient buy balance, needed {} {}'.format(self.buy_price * amount, self.market['base']['symbol']) ) self.disabled = True else: buy_transaction = self.market.buy( - buy_price, + self.buy_price, Amount(amount=amount, asset=self.market["quote"]), account=self.account, returnOrderId="head" @@ -78,7 +71,7 @@ def init_strategy(self): self.disabled = True else: sell_transaction = self.market.sell( - sell_price, + self.sell_price, Amount(amount=amount, asset=self.market["quote"]), account=self.account, returnOrderId="head" @@ -96,16 +89,11 @@ def update_orders(self, new_sell_order, new_buy_order): Update the orders """ print('Updating orders!') - target = self.bot.get("target", {}) # Stored orders sell_order = self['sell_order'] buy_order = self['buy_order'] - # prices - buy_price = self.price * (1 - (target["spread"] / 2) / 100) - sell_price = self.price * (1 + (target["spread"] / 2) / 100) - sold_amount = 0 if new_sell_order and new_sell_order['base']['amount'] < sell_order['base']['amount']: # Some of the sell order was sold @@ -131,17 +119,17 @@ 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 else: - if buy_order: + if buy_order and Order(buy_order['id']): # Cancel the old 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" @@ -166,12 +154,12 @@ def update_orders(self, new_sell_order, new_buy_order): ) self.disabled = True else: - if sell_order: + if sell_order and Order(sell_order['id']): # Cancel the old 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" @@ -185,14 +173,15 @@ def update_orders(self, new_sell_order, new_buy_order): def orders_balance(self): balance = 0 - for order in [self['buy_order'], self['sell_order']]: - if order: - 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']) - order.invert() - balance += self.get_converted_asset_amount(order['quote']) + 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): + # Fixme: get_order returns false if the order doesn't exists, failing the invert method below + order = self.get_order(order['id']) + order.invert() + balance += order['base']['amount'] return balance @@ -255,3 +244,4 @@ def update_gui_slider(self): else: percentage = (buy_amount / total) * 100 idle_add(self.view.set_bot_slider, self.bot_name, percentage) + self['slider'] = percentage From b201088ca3c202d82d634801fa31a1864545c9bc Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 13 Feb 2018 15:27:40 +0200 Subject: [PATCH 0062/1846] Change config.yml to not include a default bot --- config.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/config.yml b/config.yml index b2041664f..13c47b224 100644 --- a/config.yml +++ b/config.yml @@ -1,8 +1,3 @@ node: "wss://node.testnet.bitshares.eu" -bots: - Echo: - module: "dexbot.strategies.echo" - bot: "Echo" - market: "TEST:PEG.FAKEUSD" - account: "dexbot-test" \ No newline at end of file +bots: {} \ No newline at end of file From 28358a29bf2ba4fdfdcbf53af7b44c1d58494607 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 13 Feb 2018 15:36:28 +0200 Subject: [PATCH 0063/1846] Remove edit bot button Temporally remove the edit button since it doesn't have any functions --- dexbot/views/gen/bot_item_widget.py | 28 +++++++++------------------- dexbot/views/orig/bot_item_widget.ui | 23 ----------------------- 2 files changed, 9 insertions(+), 42 deletions(-) diff --git a/dexbot/views/gen/bot_item_widget.py b/dexbot/views/gen/bot_item_widget.py index 141c0df87..fc53952f1 100644 --- a/dexbot/views/gen/bot_item_widget.py +++ b/dexbot/views/gen/bot_item_widget.py @@ -73,24 +73,14 @@ def setupUi(self, widget): self.strategy_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) self.strategy_label.setObjectName("strategy_label") self.horizontalLayout_4.addWidget(self.strategy_label) - self.edit_button = QtWidgets.QPushButton(self.widget_3) - self.edit_button.setMaximumSize(QtCore.QSize(28, 16777215)) - self.edit_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.edit_button.setStyleSheet("border: 0") - self.edit_button.setText("") - icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap("dexbot/img/pen.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.edit_button.setIcon(icon) - self.edit_button.setObjectName("edit_button") - self.horizontalLayout_4.addWidget(self.edit_button) self.remove_button = QtWidgets.QPushButton(self.widget_3) self.remove_button.setMaximumSize(QtCore.QSize(28, 16777215)) self.remove_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) self.remove_button.setStyleSheet("border: 0;") self.remove_button.setText("") - icon1 = QtGui.QIcon() - icon1.addPixmap(QtGui.QPixmap("dexbot/img/bin.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.remove_button.setIcon(icon1) + icon = QtGui.QIcon() + icon.addPixmap(QtGui.QPixmap("dexbot/img/bin.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.remove_button.setIcon(icon) self.remove_button.setIconSize(QtCore.QSize(20, 20)) self.remove_button.setObjectName("remove_button") self.horizontalLayout_4.addWidget(self.remove_button) @@ -228,9 +218,9 @@ def setupUi(self, widget): self.pause_button.setStyleSheet("border: 0;\n" "") self.pause_button.setText("") - icon2 = QtGui.QIcon() - icon2.addPixmap(QtGui.QPixmap("dexbot/img/pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.pause_button.setIcon(icon2) + icon1 = QtGui.QIcon() + icon1.addPixmap(QtGui.QPixmap("dexbot/img/pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.pause_button.setIcon(icon1) self.pause_button.setIconSize(QtCore.QSize(30, 30)) self.pause_button.setObjectName("pause_button") self.horizontalLayout_2.addWidget(self.pause_button) @@ -244,9 +234,9 @@ def setupUi(self, widget): self.play_button.setStatusTip("") self.play_button.setStyleSheet("border: 0;") self.play_button.setText("") - icon3 = QtGui.QIcon() - icon3.addPixmap(QtGui.QPixmap("dexbot/img/play.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.play_button.setIcon(icon3) + icon2 = QtGui.QIcon() + icon2.addPixmap(QtGui.QPixmap("dexbot/img/play.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.play_button.setIcon(icon2) self.play_button.setIconSize(QtCore.QSize(30, 30)) self.play_button.setObjectName("play_button") self.horizontalLayout_2.addWidget(self.play_button) diff --git a/dexbot/views/orig/bot_item_widget.ui b/dexbot/views/orig/bot_item_widget.ui index f702771fb..0e2c171e0 100644 --- a/dexbot/views/orig/bot_item_widget.ui +++ b/dexbot/views/orig/bot_item_widget.ui @@ -146,29 +146,6 @@ - - - - - 28 - 16777215 - - - - PointingHandCursor - - - border: 0 - - - - - - - dexbot/img/pen.pngdexbot/img/pen.png - - - From 0664834e003cb0902760e9f1ff66a1d8bf97be3c Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 14 Feb 2018 08:12:32 +0200 Subject: [PATCH 0064/1846] Change code structure in bot.py --- dexbot/bot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dexbot/bot.py b/dexbot/bot.py index a78936642..786088eb6 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -73,6 +73,7 @@ def __init__( 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 @@ -88,7 +89,7 @@ def __init__( # Events def on_block(self, data): for botname, bot in self.config["bots"].items(): - if (not botname in self.bots) or self.bots[botname].disabled: + if botname not in self.bots or self.bots[botname].disabled: continue try: self.bots[botname].ontick(data) From d7dd7334d2427571009b17f6b2754dfa109077e4 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 14 Feb 2018 09:56:58 +0200 Subject: [PATCH 0065/1846] Change base asset input field to combobox on create bot window Also added some default assets for the base asset --- dexbot/controllers/create_bot_controller.py | 7 +++ dexbot/views/create_bot.py | 7 +-- dexbot/views/gen/create_bot_window.py | 32 +++++-------- dexbot/views/orig/create_bot_window.ui | 51 ++++++++------------- 4 files changed, 41 insertions(+), 56 deletions(-) diff --git a/dexbot/controllers/create_bot_controller.py b/dexbot/controllers/create_bot_controller.py index ac687de60..7a30e2504 100644 --- a/dexbot/controllers/create_bot_controller.py +++ b/dexbot/controllers/create_bot_controller.py @@ -23,6 +23,13 @@ def strategies(self): def get_strategy_module(self, strategy): return self.strategies[strategy] + @property + def base_assets(self): + assets = [ + 'USD', 'OPEN.BTC', 'CNY', 'BTS', 'BTC' + ] + return assets + @staticmethod def is_bot_name_valid(bot_name): bot_names = MainController.get_bots_data().keys() diff --git a/dexbot/views/create_bot.py b/dexbot/views/create_bot.py index 9bf7c3497..99f54deef 100644 --- a/dexbot/views/create_bot.py +++ b/dexbot/views/create_bot.py @@ -15,6 +15,7 @@ def __init__(self, controller): # 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) self.bot_name = controller.get_unique_bot_name() self.ui.bot_name_input.setText(self.bot_name) @@ -45,7 +46,7 @@ def validate_account(self): def validate_form(self): error_text = '' - base_asset = self.ui.base_asset_input.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() @@ -53,7 +54,7 @@ def validate_form(self): elif not self.validate_asset(base_asset): error_text = 'Field "Base Asset" does not have a valid asset.' elif not self.validate_asset(quote_asset): - error_text = 'Field "Base Quote" does not have a valid asset.' + error_text = 'Field "Quote Asset" does not have a valid asset.' elif not self.validate_market(): error_text = "Market {}/{} doesn't exist.".format(base_asset, quote_asset) elif not self.validate_account_name(): @@ -84,7 +85,7 @@ def handle_save(self): 'spread': spread } - base_asset = ui.base_asset_input.text() + 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) diff --git a/dexbot/views/gen/create_bot_window.py b/dexbot/views/gen/create_bot_window.py index 55fd34401..a19a8cbc4 100644 --- a/dexbot/views/gen/create_bot_window.py +++ b/dexbot/views/gen/create_bot_window.py @@ -185,16 +185,6 @@ def setupUi(self, Dialog): self.base_asset_label.setMaximumSize(QtCore.QSize(110, 16777215)) self.base_asset_label.setObjectName("base_asset_label") self.formLayout_3.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.base_asset_label) - self.base_asset_input = QtWidgets.QLineEdit(self.groupBox) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.base_asset_input.sizePolicy().hasHeightForWidth()) - self.base_asset_input.setSizePolicy(sizePolicy) - self.base_asset_input.setMaximumSize(QtCore.QSize(80, 16777215)) - self.base_asset_input.setInputMethodHints(QtCore.Qt.ImhPreferUppercase) - self.base_asset_input.setObjectName("base_asset_input") - self.formLayout_3.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.base_asset_input) self.quote_asset_label = QtWidgets.QLabel(self.groupBox) self.quote_asset_label.setMinimumSize(QtCore.QSize(110, 0)) self.quote_asset_label.setMaximumSize(QtCore.QSize(110, 16777215)) @@ -209,14 +199,16 @@ def setupUi(self, Dialog): self.quote_asset_input.setMaximumSize(QtCore.QSize(80, 16777215)) self.quote_asset_input.setObjectName("quote_asset_input") self.formLayout_3.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.quote_asset_input) - self.strategy_label.raise_() - self.strategy_input.raise_() - self.bot_name_label.raise_() - self.bot_name_input.raise_() - self.base_asset_label.raise_() - self.base_asset_input.raise_() - self.quote_asset_label.raise_() - self.quote_asset_input.raise_() + self.base_asset_input = QtWidgets.QComboBox(self.groupBox) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.base_asset_input.sizePolicy().hasHeightForWidth()) + self.base_asset_input.setSizePolicy(sizePolicy) + self.base_asset_input.setMinimumSize(QtCore.QSize(105, 0)) + self.base_asset_input.setEditable(True) + self.base_asset_input.setObjectName("base_asset_input") + self.formLayout_3.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.base_asset_input) self.gridLayout.addWidget(self.groupBox, 0, 0, 1, 1) spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.gridLayout.addItem(spacerItem1, 3, 0, 1, 1) @@ -227,15 +219,13 @@ def setupUi(self, Dialog): self.private_key_label.setBuddy(self.private_key_input) self.strategy_label.setBuddy(self.strategy_input) self.bot_name_label.setBuddy(self.bot_name_input) - self.base_asset_label.setBuddy(self.base_asset_input) self.quote_asset_label.setBuddy(self.quote_asset_input) self.retranslateUi(Dialog) self.strategy_input.setCurrentIndex(-1) QtCore.QMetaObject.connectSlotsByName(Dialog) Dialog.setTabOrder(self.strategy_input, self.bot_name_input) - Dialog.setTabOrder(self.bot_name_input, self.base_asset_input) - Dialog.setTabOrder(self.base_asset_input, self.quote_asset_input) + Dialog.setTabOrder(self.bot_name_input, self.quote_asset_input) Dialog.setTabOrder(self.quote_asset_input, self.account_input) Dialog.setTabOrder(self.account_input, self.private_key_input) Dialog.setTabOrder(self.private_key_input, self.amount_input) diff --git a/dexbot/views/orig/create_bot_window.ui b/dexbot/views/orig/create_bot_window.ui index f47ac1294..667f782e5 100644 --- a/dexbot/views/orig/create_bot_window.ui +++ b/dexbot/views/orig/create_bot_window.ui @@ -424,28 +424,6 @@ Base Asset - - base_asset_input - - - - - - - - 0 - 0 - - - - - 80 - 16777215 - - - - Qt::ImhPreferUppercase - @@ -486,16 +464,26 @@ + + + + + 0 + 0 + + + + + 105 + 0 + + + + true + + + - - strategy_label - strategy_input - bot_name_label - bot_name_input - base_asset_label - base_asset_input - quote_asset_label - quote_asset_input @@ -516,7 +504,6 @@ strategy_input bot_name_input - base_asset_input quote_asset_input account_input private_key_input From 9808f8e908b1ca3254b6e8540237c4f01b727b05 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 14 Feb 2018 10:35:45 +0200 Subject: [PATCH 0066/1846] Change the tab order in create bot window --- dexbot/views/gen/create_bot_window.py | 3 ++- dexbot/views/orig/create_bot_window.ui | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dexbot/views/gen/create_bot_window.py b/dexbot/views/gen/create_bot_window.py index a19a8cbc4..891326915 100644 --- a/dexbot/views/gen/create_bot_window.py +++ b/dexbot/views/gen/create_bot_window.py @@ -225,7 +225,8 @@ def setupUi(self, Dialog): self.strategy_input.setCurrentIndex(-1) QtCore.QMetaObject.connectSlotsByName(Dialog) Dialog.setTabOrder(self.strategy_input, self.bot_name_input) - Dialog.setTabOrder(self.bot_name_input, self.quote_asset_input) + Dialog.setTabOrder(self.bot_name_input, self.base_asset_input) + Dialog.setTabOrder(self.base_asset_input, self.quote_asset_input) Dialog.setTabOrder(self.quote_asset_input, self.account_input) Dialog.setTabOrder(self.account_input, self.private_key_input) Dialog.setTabOrder(self.private_key_input, self.amount_input) diff --git a/dexbot/views/orig/create_bot_window.ui b/dexbot/views/orig/create_bot_window.ui index 667f782e5..fa72fded6 100644 --- a/dexbot/views/orig/create_bot_window.ui +++ b/dexbot/views/orig/create_bot_window.ui @@ -504,6 +504,7 @@ strategy_input bot_name_input + base_asset_input quote_asset_input account_input private_key_input From fa2f48f67ac017308c55a127f8057b95d3c9b487 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 14 Feb 2018 10:41:21 +0200 Subject: [PATCH 0067/1846] Fix base asset data fetching --- dexbot/views/create_bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/views/create_bot.py b/dexbot/views/create_bot.py index 99f54deef..597249837 100644 --- a/dexbot/views/create_bot.py +++ b/dexbot/views/create_bot.py @@ -31,7 +31,7 @@ def validate_asset(self, asset): return self.controller.is_asset_valid(asset) def validate_market(self): - base_asset = self.ui.base_asset_input.text() + base_asset = self.ui.base_asset_input.currentText() quote_asset = self.ui.quote_asset_input.text() return base_asset.lower() != quote_asset.lower() From d54267e57714d44f2928af19c3db0b42cfdb555a Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 14 Feb 2018 12:15:50 +0200 Subject: [PATCH 0068/1846] Change code styling in basestrategy Made it pep8 obedient --- dexbot/basestrategy.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 885ed81e5..f2c9d79bf 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -119,10 +119,10 @@ def __init__( 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'], - 'is_disabled':(lambda: self.disabled)}) + self.log = logging.LoggerAdapter(logging.getLogger('dexbot.per_bot'), {'botname': name, + 'account': self.bot['account'], + 'market': self.bot['market'], + 'is_disabled': lambda: self.disabled}) @property def orders(self): From 699d08de5119fb8fbf120a97184331200519c1d4 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 14 Feb 2018 12:17:13 +0200 Subject: [PATCH 0069/1846] Add logging to simple strategy Added logging on order creation --- dexbot/strategies/simple.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/simple.py b/dexbot/strategies/simple.py index 5678152a5..153c13239 100644 --- a/dexbot/strategies/simple.py +++ b/dexbot/strategies/simple.py @@ -43,7 +43,6 @@ def error(self, *args, **kwargs): def init_strategy(self): # Target target = self.bot.get("target", {}) - amount = target['amount'] / 2 # Buy Side @@ -60,6 +59,7 @@ def init_strategy(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)) if buy_order: self['buy_order'] = buy_order @@ -77,6 +77,7 @@ def init_strategy(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)) if sell_order: self['sell_order'] = sell_order @@ -135,6 +136,9 @@ def update_orders(self, new_sell_order, new_buy_order): 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) + ) if buy_order: self['buy_order'] = buy_order else: @@ -165,6 +169,9 @@ def update_orders(self, new_sell_order, new_buy_order): 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) + ) if sell_order: self['sell_order'] = sell_order else: From e65fed7e8b889384622e07b7f696c4321c68065c Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 14 Feb 2018 12:18:29 +0200 Subject: [PATCH 0070/1846] Fix order amount calculation in simple strategy --- dexbot/strategies/simple.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/dexbot/strategies/simple.py b/dexbot/strategies/simple.py index 153c13239..535d245df 100644 --- a/dexbot/strategies/simple.py +++ b/dexbot/strategies/simple.py @@ -89,19 +89,20 @@ def update_orders(self, new_sell_order, new_buy_order): """ Update the orders """ - print('Updating orders!') - # Stored orders sell_order = self['sell_order'] buy_order = self['buy_order'] + sell_price = self.sell_price + 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 - sold_amount = sell_order['quote']['amount'] - new_sell_order['quote']['amount'] + 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['quote']['amount'] + sold_amount = sell_order['base']['amount'] bought_amount = 0 if new_buy_order and new_buy_order['quote']['amount'] < buy_order['quote']['amount']: @@ -120,17 +121,17 @@ 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(self.buy_price * new_buy_amount, + 'Insufficient buy balance, needed {} {}'.format(buy_price * new_buy_amount, self.market['base']['symbol']) ) self.disabled = True else: - if buy_order and Order(buy_order['id']): + 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, + buy_price, Amount(amount=new_buy_amount, asset=self.market["quote"]), account=self.account, returnOrderId="head" @@ -158,12 +159,12 @@ def update_orders(self, new_sell_order, new_buy_order): ) self.disabled = True else: - if sell_order and Order(sell_order['id']): + 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, + sell_price, Amount(amount=new_sell_amount, asset=self.market["quote"]), account=self.account, returnOrderId="head" @@ -185,10 +186,11 @@ def orders_balance(self): if order['base']['symbol'] != self.market['base']['symbol']: # Invert the market for easier calculation if not isinstance(order, Price): - # Fixme: get_order returns false if the order doesn't exists, failing the invert method below order = self.get_order(order['id']) - order.invert() - balance += order['base']['amount'] + if order: + order.invert() + if order: + balance += order['base']['amount'] return balance From 9cfe2a57e8c3670e5229298c016b9be8b53b1fdf Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 14 Feb 2018 12:19:02 +0200 Subject: [PATCH 0071/1846] Fix bot item setting double negative profit --- dexbot/views/bot_item.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dexbot/views/bot_item.py b/dexbot/views/bot_item.py index b83b1d74f..95144f6f2 100644 --- a/dexbot/views/bot_item.py +++ b/dexbot/views/bot_item.py @@ -66,8 +66,7 @@ def set_bot_market(self, value): def set_bot_profit(self, value): if value >= 0: value = '+' + str(value) - else: - value = '-' + str(value) + value = str(value) + '%' self.profit_label.setText(value) From 2cbe246df45e05637bb860c213498bca533836e2 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 14 Feb 2018 12:20:59 +0200 Subject: [PATCH 0072/1846] Change default blockchain node in the config Set the default node to wss://bitshares.openledger.info/ws --- config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.yml b/config.yml index 13c47b224..38d9ef064 100644 --- a/config.yml +++ b/config.yml @@ -1,3 +1,3 @@ -node: "wss://node.testnet.bitshares.eu" +node: wss://bitshares.openledger.info/ws bots: {} \ No newline at end of file From bc89ceb8fa5fa34cb3c3abf88404813b385cf758 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 14 Feb 2018 12:22:37 +0200 Subject: [PATCH 0073/1846] Add app.spec for easier compiling --- .gitignore | 1 + app.spec | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 app.spec diff --git a/.gitignore b/.gitignore index 77a45b664..e68fbd659 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ var/ # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec +!app.spec # Installer logs pip-log.txt diff --git a/app.spec b/app.spec new file mode 100644 index 000000000..d4e6d7c91 --- /dev/null +++ b/app.spec @@ -0,0 +1,30 @@ +# -*- mode: python -*- + +block_cipher = None + + +a = Analysis(['app.py'], + binaries=[], + datas=[('config.yml', '.')], + hiddenimports=[], + 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='DEXBot', + debug=False, + strip=False, + upx=True, + runtime_tmpdir=None, + console=False) From dea58e1a39ee12dd50fb4e8765e8f5e3d370c170 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 14 Feb 2018 12:23:26 +0200 Subject: [PATCH 0074/1846] Change version number to 0.1.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f5ffad76c..6b81ec27b 100755 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages -VERSION = '0.0.6' +VERSION = '0.1.0' setup( name='dexbot', From 98b753f590239bd16e5c0ffa6ba0c59e09f0bf60 Mon Sep 17 00:00:00 2001 From: Nikolay Date: Thu, 15 Feb 2018 09:41:52 +0200 Subject: [PATCH 0075/1846] Add Edit bot window --- dexbot/views/edit_bot.py | 12 + dexbot/views/gen/edit_bot_window.py | 222 +++++++++++++ dexbot/views/orig/edit_bot_window.ui | 457 +++++++++++++++++++++++++++ 3 files changed, 691 insertions(+) create mode 100644 dexbot/views/edit_bot.py create mode 100644 dexbot/views/gen/edit_bot_window.py create mode 100644 dexbot/views/orig/edit_bot_window.ui diff --git a/dexbot/views/edit_bot.py b/dexbot/views/edit_bot.py new file mode 100644 index 000000000..f42ee1144 --- /dev/null +++ b/dexbot/views/edit_bot.py @@ -0,0 +1,12 @@ +from PyQt5 import QtWidgets + +from dexbot.views.notice import NoticeDialog +from dexbot.view.gen.edit_bot_window import Ui_Dialog + +class EditBotView(QtWidgets.QDialog): + def __init__(self, controller): + super().__init__() + self.controller = controller + + self.ui = Ui_Dialog() + self.ui.setupUi(self) diff --git a/dexbot/views/gen/edit_bot_window.py b/dexbot/views/gen/edit_bot_window.py new file mode 100644 index 000000000..7de17c48c --- /dev/null +++ b/dexbot/views/gen/edit_bot_window.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'edit_bot_window.ui' +# +# Created by: PyQt5 UI code generator 5.10 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName("Dialog") + Dialog.resize(400, 430) + self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) + self.verticalLayout.setObjectName("verticalLayout") + self.groupBox = QtWidgets.QGroupBox(Dialog) + self.groupBox.setObjectName("groupBox") + self.formLayout_3 = QtWidgets.QFormLayout(self.groupBox) + self.formLayout_3.setObjectName("formLayout_3") + self.strategy_label = QtWidgets.QLabel(self.groupBox) + self.strategy_label.setMinimumSize(QtCore.QSize(110, 0)) + self.strategy_label.setMaximumSize(QtCore.QSize(110, 16777215)) + self.strategy_label.setObjectName("strategy_label") + self.formLayout_3.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.strategy_label) + self.strategy_input = QtWidgets.QComboBox(self.groupBox) + self.strategy_input.setObjectName("strategy_input") + self.formLayout_3.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.strategy_input) + self.bot_name_label = QtWidgets.QLabel(self.groupBox) + self.bot_name_label.setMinimumSize(QtCore.QSize(110, 0)) + self.bot_name_label.setMaximumSize(QtCore.QSize(110, 16777215)) + self.bot_name_label.setObjectName("bot_name_label") + self.formLayout_3.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.bot_name_label) + self.bot_name_input = QtWidgets.QLineEdit(self.groupBox) + self.bot_name_input.setObjectName("bot_name_input") + self.formLayout_3.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.bot_name_input) + self.base_asset_label = QtWidgets.QLabel(self.groupBox) + self.base_asset_label.setMinimumSize(QtCore.QSize(110, 0)) + self.base_asset_label.setMaximumSize(QtCore.QSize(110, 16777215)) + self.base_asset_label.setObjectName("base_asset_label") + self.formLayout_3.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.base_asset_label) + self.base_asset_input = QtWidgets.QLineEdit(self.groupBox) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.base_asset_input.sizePolicy().hasHeightForWidth()) + self.base_asset_input.setSizePolicy(sizePolicy) + self.base_asset_input.setMaximumSize(QtCore.QSize(80, 16777215)) + self.base_asset_input.setInputMethodHints(QtCore.Qt.ImhPreferUppercase) + self.base_asset_input.setObjectName("base_asset_input") + self.formLayout_3.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.base_asset_input) + self.quote_asset_label = QtWidgets.QLabel(self.groupBox) + self.quote_asset_label.setMinimumSize(QtCore.QSize(110, 0)) + self.quote_asset_label.setMaximumSize(QtCore.QSize(110, 16777215)) + self.quote_asset_label.setObjectName("quote_asset_label") + self.formLayout_3.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.quote_asset_label) + self.quote_asset_input = QtWidgets.QLineEdit(self.groupBox) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.quote_asset_input.sizePolicy().hasHeightForWidth()) + self.quote_asset_input.setSizePolicy(sizePolicy) + self.quote_asset_input.setMaximumSize(QtCore.QSize(80, 16777215)) + self.quote_asset_input.setObjectName("quote_asset_input") + self.formLayout_3.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.quote_asset_input) + self.verticalLayout.addWidget(self.groupBox) + self.groupBox_2 = QtWidgets.QGroupBox(Dialog) + self.groupBox_2.setObjectName("groupBox_2") + self.formLayout_2 = QtWidgets.QFormLayout(self.groupBox_2) + self.formLayout_2.setRowWrapPolicy(QtWidgets.QFormLayout.WrapLongRows) + self.formLayout_2.setObjectName("formLayout_2") + self.account_label = QtWidgets.QLabel(self.groupBox_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.account_label.sizePolicy().hasHeightForWidth()) + self.account_label.setSizePolicy(sizePolicy) + self.account_label.setMinimumSize(QtCore.QSize(110, 0)) + self.account_label.setMaximumSize(QtCore.QSize(110, 16777215)) + self.account_label.setObjectName("account_label") + self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.account_label) + self.account_name = QtWidgets.QLabel(self.groupBox_2) + self.account_name.setText("") + self.account_name.setObjectName("account_name") + self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.account_name) + self.verticalLayout.addWidget(self.groupBox_2) + self.groupBox_3 = QtWidgets.QGroupBox(Dialog) + self.groupBox_3.setObjectName("groupBox_3") + self.formLayout = QtWidgets.QFormLayout(self.groupBox_3) + self.formLayout.setObjectName("formLayout") + self.amount_label = QtWidgets.QLabel(self.groupBox_3) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.amount_label.sizePolicy().hasHeightForWidth()) + self.amount_label.setSizePolicy(sizePolicy) + self.amount_label.setMinimumSize(QtCore.QSize(110, 0)) + self.amount_label.setMaximumSize(QtCore.QSize(110, 16777215)) + self.amount_label.setObjectName("amount_label") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.amount_label) + self.amount_input = QtWidgets.QDoubleSpinBox(self.groupBox_3) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.amount_input.sizePolicy().hasHeightForWidth()) + self.amount_input.setSizePolicy(sizePolicy) + self.amount_input.setMinimumSize(QtCore.QSize(140, 0)) + self.amount_input.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) + self.amount_input.setDecimals(5) + self.amount_input.setMaximum(999999999.999) + self.amount_input.setObjectName("amount_input") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.amount_input) + self.center_price_label = QtWidgets.QLabel(self.groupBox_3) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.center_price_label.sizePolicy().hasHeightForWidth()) + self.center_price_label.setSizePolicy(sizePolicy) + self.center_price_label.setMinimumSize(QtCore.QSize(110, 0)) + self.center_price_label.setMaximumSize(QtCore.QSize(110, 16777215)) + self.center_price_label.setObjectName("center_price_label") + self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.center_price_label) + self.center_price_input = QtWidgets.QDoubleSpinBox(self.groupBox_3) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.center_price_input.sizePolicy().hasHeightForWidth()) + self.center_price_input.setSizePolicy(sizePolicy) + self.center_price_input.setMinimumSize(QtCore.QSize(140, 0)) + self.center_price_input.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) + self.center_price_input.setAccelerated(False) + self.center_price_input.setProperty("showGroupSeparator", False) + self.center_price_input.setDecimals(5) + self.center_price_input.setMinimum(-999999999.999) + self.center_price_input.setMaximum(999999999.999) + self.center_price_input.setObjectName("center_price_input") + self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.center_price_input) + self.spread_label = QtWidgets.QLabel(self.groupBox_3) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.spread_label.sizePolicy().hasHeightForWidth()) + self.spread_label.setSizePolicy(sizePolicy) + self.spread_label.setMinimumSize(QtCore.QSize(110, 0)) + self.spread_label.setMaximumSize(QtCore.QSize(110, 16777215)) + self.spread_label.setObjectName("spread_label") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.spread_label) + self.spread_input = QtWidgets.QDoubleSpinBox(self.groupBox_3) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.spread_input.sizePolicy().hasHeightForWidth()) + self.spread_input.setSizePolicy(sizePolicy) + self.spread_input.setMinimumSize(QtCore.QSize(140, 0)) + self.spread_input.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) + self.spread_input.setMaximum(100000.0) + self.spread_input.setProperty("value", 5.0) + self.spread_input.setObjectName("spread_input") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.spread_input) + self.verticalLayout.addWidget(self.groupBox_3) + spacerItem = QtWidgets.QSpacerItem(20, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem) + self.widget = QtWidgets.QWidget(Dialog) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.widget.sizePolicy().hasHeightForWidth()) + self.widget.setSizePolicy(sizePolicy) + self.widget.setObjectName("widget") + self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.widget) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + spacerItem1 = QtWidgets.QSpacerItem(179, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_2.addItem(spacerItem1) + self.cancel_button = QtWidgets.QPushButton(self.widget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.cancel_button.sizePolicy().hasHeightForWidth()) + self.cancel_button.setSizePolicy(sizePolicy) + self.cancel_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.cancel_button.setObjectName("cancel_button") + self.horizontalLayout_2.addWidget(self.cancel_button) + self.save_button = QtWidgets.QPushButton(self.widget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.save_button.sizePolicy().hasHeightForWidth()) + self.save_button.setSizePolicy(sizePolicy) + self.save_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.save_button.setObjectName("save_button") + self.horizontalLayout_2.addWidget(self.save_button) + self.verticalLayout.addWidget(self.widget) + self.strategy_label.setBuddy(self.strategy_input) + self.bot_name_label.setBuddy(self.bot_name_input) + self.base_asset_label.setBuddy(self.base_asset_input) + self.quote_asset_label.setBuddy(self.quote_asset_input) + self.amount_label.setBuddy(self.amount_input) + self.center_price_label.setBuddy(self.center_price_input) + self.spread_label.setBuddy(self.spread_input) + + self.retranslateUi(Dialog) + self.strategy_input.setCurrentIndex(-1) + QtCore.QMetaObject.connectSlotsByName(Dialog) + + def retranslateUi(self, Dialog): + _translate = QtCore.QCoreApplication.translate + Dialog.setWindowTitle(_translate("Dialog", "DEXBot - Edit Bot")) + self.groupBox.setTitle(_translate("Dialog", "Bot Details")) + self.strategy_label.setText(_translate("Dialog", "Strategy")) + self.bot_name_label.setText(_translate("Dialog", "Bot Name")) + self.base_asset_label.setText(_translate("Dialog", "Base Asset")) + self.quote_asset_label.setText(_translate("Dialog", "Quote Asset")) + self.groupBox_2.setTitle(_translate("Dialog", "Bitshares Account Details")) + self.account_label.setText(_translate("Dialog", "Account")) + self.groupBox_3.setTitle(_translate("Dialog", "Bot Parameters")) + self.amount_label.setText(_translate("Dialog", "Amount")) + self.center_price_label.setText(_translate("Dialog", "Center Price")) + self.spread_label.setText(_translate("Dialog", "Spread")) + self.spread_input.setSuffix(_translate("Dialog", "%")) + self.cancel_button.setText(_translate("Dialog", "Cancel")) + self.save_button.setText(_translate("Dialog", "Save")) + diff --git a/dexbot/views/orig/edit_bot_window.ui b/dexbot/views/orig/edit_bot_window.ui new file mode 100644 index 000000000..a782d4c37 --- /dev/null +++ b/dexbot/views/orig/edit_bot_window.ui @@ -0,0 +1,457 @@ + + + Dialog + + + + 0 + 0 + 400 + 430 + + + + DEXBot - Edit Bot + + + + + + Bot Details + + + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Strategy + + + strategy_input + + + + + + + -1 + + + + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Bot Name + + + bot_name_input + + + + + + + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Base Asset + + + base_asset_input + + + + + + + + 0 + 0 + + + + + 80 + 16777215 + + + + Qt::ImhPreferUppercase + + + + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Quote Asset + + + quote_asset_input + + + + + + + + 0 + 0 + + + + + 80 + 16777215 + + + + + + + + + + + Bitshares Account Details + + + + QFormLayout::WrapLongRows + + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Account + + + + + + + + + + + + + + + + + Bot Parameters + + + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Amount + + + amount_input + + + + + + + + 0 + 0 + + + + + 140 + 0 + + + + + + + 5 + + + 999999999.998999953269958 + + + + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Center Price + + + center_price_input + + + + + + + + 0 + 0 + + + + + 140 + 0 + + + + + + + false + + + false + + + 5 + + + -999999999.998999953269958 + + + 999999999.998999953269958 + + + + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Spread + + + spread_input + + + + + + + + 0 + 0 + + + + + 140 + 0 + + + + + + + % + + + 100000.000000000000000 + + + 5.000000000000000 + + + + + + + + + + Qt::Vertical + + + + 20 + 1 + + + + + + + + + 0 + 0 + + + + + + + Qt::Horizontal + + + + 179 + 20 + + + + + + + + + 0 + 0 + + + + PointingHandCursor + + + Cancel + + + + + + + + 0 + 0 + + + + PointingHandCursor + + + Save + + + + + + + + + + + From 36bc6158ee3aef4fc0c375a0188bf84e60662e4d Mon Sep 17 00:00:00 2001 From: Nikolay Date: Thu, 15 Feb 2018 15:57:08 +0200 Subject: [PATCH 0076/1846] Add data preload for Edit Bot window --- config.yml | 5 ++- dexbot/controllers/create_bot_controller.py | 21 +++++++++++ dexbot/views/bot_item.py | 19 +++++++++- dexbot/views/edit_bot.py | 17 ++++++--- dexbot/views/gen/bot_item_widget.py | 30 ++++++++++----- dexbot/views/gen/edit_bot_window.py | 23 ++++++------ dexbot/views/orig/bot_item_widget.ui | 23 ++++++++++++ dexbot/views/orig/edit_bot_window.ui | 41 ++++++++++----------- 8 files changed, 126 insertions(+), 53 deletions(-) diff --git a/config.yml b/config.yml index 38d9ef064..f627722e3 100644 --- a/config.yml +++ b/config.yml @@ -1,3 +1,4 @@ -node: wss://bitshares.openledger.info/ws +node: wss://node.testnet.bitshares.eu -bots: {} \ No newline at end of file +bots: {Bot 1: {target: {spread: 5.0, center_price: 42.025, amount: 0.01}, strategy: Simple + Strategy, module: dexbot.strategies.simple, market: TESTUSD/TEST, account: nikolay-codaone}} diff --git a/dexbot/controllers/create_bot_controller.py b/dexbot/controllers/create_bot_controller.py index 7a30e2504..6e1ce1d73 100644 --- a/dexbot/controllers/create_bot_controller.py +++ b/dexbot/controllers/create_bot_controller.py @@ -98,3 +98,24 @@ def add_bot_config(botname, bot_data): with open("config.yml", "w") as f: yaml.dump(config, f) + + @staticmethod + def get_bot_current_strategy(bot_data): + strategies = { + bot_data['strategy']: bot_data['module'] + } + return strategies + + @staticmethod + def get_assets(bot_data): + return bot_data['market'].split('/') + + def get_base_asset(self, bot_data): + return self.get_assets(bot_data)[1] + + def get_quote_asset(self, bot_data): + return self.get_assets(bot_data)[0] + + @staticmethod + def get_account(bot_data): + return bot_data['account'] diff --git a/dexbot/views/bot_item.py b/dexbot/views/bot_item.py index 95144f6f2..c365ed4ca 100644 --- a/dexbot/views/bot_item.py +++ b/dexbot/views/bot_item.py @@ -3,17 +3,20 @@ from dexbot.views.gen.bot_item_widget import Ui_widget from dexbot.views.confirmation import ConfirmationDialog from dexbot.storage import worker +from dexbot.controllers.create_bot_controller import CreateBotController +from dexbot.views.edit_bot import EditBotView class BotItemWidget(QtWidgets.QWidget, Ui_widget): - def __init__(self, botname, config, controller, view): + 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 = controller + self.controller = main_ctrl self.view = view self.setupUi(self) @@ -22,6 +25,7 @@ def __init__(self, botname, config, controller, view): self.pause_button.clicked.connect(self.pause_bot) self.play_button.clicked.connect(self.start_bot) self.remove_button.clicked.connect(self.remove_widget) + self.edit_button.clicked.connect(self.handle_edit_bot) self.setup_ui_data(config) @@ -82,3 +86,14 @@ def remove_widget(self): # 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.bitshares_instance) + edit_bot_dialog = EditBotView(controller, self.botname, self.config) + return_value = edit_bot_dialog.exec_() + + # User clicked save + if return_value == 1: + botname = edit_bot_dialog.bot_name + config = self.main_ctrl.get_bot_config(botname) + self.add_bot_widget(botname, config) \ No newline at end of file diff --git a/dexbot/views/edit_bot.py b/dexbot/views/edit_bot.py index f42ee1144..1870d8257 100644 --- a/dexbot/views/edit_bot.py +++ b/dexbot/views/edit_bot.py @@ -1,12 +1,19 @@ from PyQt5 import QtWidgets from dexbot.views.notice import NoticeDialog -from dexbot.view.gen.edit_bot_window import Ui_Dialog +from dexbot.views.gen.edit_bot_window import Ui_Dialog -class EditBotView(QtWidgets.QDialog): - def __init__(self, controller): + +class EditBotView(QtWidgets.QDialog, Ui_Dialog): + def __init__(self, controller, botname, config): super().__init__() self.controller = controller - self.ui = Ui_Dialog() - self.ui.setupUi(self) + self.setupUi(self) + bot_data = config['bots'][botname] + self.strategy_input.addItems(self.controller.get_bot_current_strategy(bot_data)) + self.bot_name_input.setText(botname) + self.base_asset_input.addItem(self.controller.get_base_asset(bot_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)) diff --git a/dexbot/views/gen/bot_item_widget.py b/dexbot/views/gen/bot_item_widget.py index fc53952f1..939447070 100644 --- a/dexbot/views/gen/bot_item_widget.py +++ b/dexbot/views/gen/bot_item_widget.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'dexbot/views/orig/bot_item_widget.ui' # -# Created by: PyQt5 UI code generator 5.9.2 +# Created by: PyQt5 UI code generator 5.10 # # WARNING! All changes made in this file will be lost! @@ -73,14 +73,24 @@ def setupUi(self, widget): self.strategy_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) self.strategy_label.setObjectName("strategy_label") self.horizontalLayout_4.addWidget(self.strategy_label) + self.edit_button = QtWidgets.QPushButton(self.widget_3) + self.edit_button.setMaximumSize(QtCore.QSize(28, 16777215)) + self.edit_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + self.edit_button.setStyleSheet("border: 0;") + self.edit_button.setText("") + icon = QtGui.QIcon() + icon.addPixmap(QtGui.QPixmap("dexbot/img/pen.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.edit_button.setIcon(icon) + self.edit_button.setObjectName("edit_button") + self.horizontalLayout_4.addWidget(self.edit_button) self.remove_button = QtWidgets.QPushButton(self.widget_3) self.remove_button.setMaximumSize(QtCore.QSize(28, 16777215)) self.remove_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) self.remove_button.setStyleSheet("border: 0;") self.remove_button.setText("") - icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap("dexbot/img/bin.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.remove_button.setIcon(icon) + icon1 = QtGui.QIcon() + icon1.addPixmap(QtGui.QPixmap("dexbot/img/bin.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.remove_button.setIcon(icon1) self.remove_button.setIconSize(QtCore.QSize(20, 20)) self.remove_button.setObjectName("remove_button") self.horizontalLayout_4.addWidget(self.remove_button) @@ -218,9 +228,9 @@ def setupUi(self, widget): self.pause_button.setStyleSheet("border: 0;\n" "") self.pause_button.setText("") - icon1 = QtGui.QIcon() - icon1.addPixmap(QtGui.QPixmap("dexbot/img/pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.pause_button.setIcon(icon1) + icon2 = QtGui.QIcon() + icon2.addPixmap(QtGui.QPixmap("dexbot/img/pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.pause_button.setIcon(icon2) self.pause_button.setIconSize(QtCore.QSize(30, 30)) self.pause_button.setObjectName("pause_button") self.horizontalLayout_2.addWidget(self.pause_button) @@ -234,9 +244,9 @@ def setupUi(self, widget): self.play_button.setStatusTip("") self.play_button.setStyleSheet("border: 0;") self.play_button.setText("") - icon2 = QtGui.QIcon() - icon2.addPixmap(QtGui.QPixmap("dexbot/img/play.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.play_button.setIcon(icon2) + icon3 = QtGui.QIcon() + icon3.addPixmap(QtGui.QPixmap("dexbot/img/play.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.play_button.setIcon(icon3) self.play_button.setIconSize(QtCore.QSize(30, 30)) self.play_button.setObjectName("play_button") self.horizontalLayout_2.addWidget(self.play_button) diff --git a/dexbot/views/gen/edit_bot_window.py b/dexbot/views/gen/edit_bot_window.py index 7de17c48c..659c66820 100644 --- a/dexbot/views/gen/edit_bot_window.py +++ b/dexbot/views/gen/edit_bot_window.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'edit_bot_window.ui' +# Form implementation generated from reading ui file 'dexbot/views/orig/edit_bot_window.ui' # # Created by: PyQt5 UI code generator 5.10 # @@ -39,16 +39,6 @@ def setupUi(self, Dialog): self.base_asset_label.setMaximumSize(QtCore.QSize(110, 16777215)) self.base_asset_label.setObjectName("base_asset_label") self.formLayout_3.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.base_asset_label) - self.base_asset_input = QtWidgets.QLineEdit(self.groupBox) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.base_asset_input.sizePolicy().hasHeightForWidth()) - self.base_asset_input.setSizePolicy(sizePolicy) - self.base_asset_input.setMaximumSize(QtCore.QSize(80, 16777215)) - self.base_asset_input.setInputMethodHints(QtCore.Qt.ImhPreferUppercase) - self.base_asset_input.setObjectName("base_asset_input") - self.formLayout_3.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.base_asset_input) self.quote_asset_label = QtWidgets.QLabel(self.groupBox) self.quote_asset_label.setMinimumSize(QtCore.QSize(110, 0)) self.quote_asset_label.setMaximumSize(QtCore.QSize(110, 16777215)) @@ -63,6 +53,16 @@ def setupUi(self, Dialog): self.quote_asset_input.setMaximumSize(QtCore.QSize(80, 16777215)) self.quote_asset_input.setObjectName("quote_asset_input") self.formLayout_3.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.quote_asset_input) + self.base_asset_input = QtWidgets.QComboBox(self.groupBox) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.base_asset_input.sizePolicy().hasHeightForWidth()) + self.base_asset_input.setSizePolicy(sizePolicy) + self.base_asset_input.setMinimumSize(QtCore.QSize(105, 0)) + self.base_asset_input.setEditable(True) + self.base_asset_input.setObjectName("base_asset_input") + self.formLayout_3.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.base_asset_input) self.verticalLayout.addWidget(self.groupBox) self.groupBox_2 = QtWidgets.QGroupBox(Dialog) self.groupBox_2.setObjectName("groupBox_2") @@ -192,7 +192,6 @@ def setupUi(self, Dialog): self.verticalLayout.addWidget(self.widget) self.strategy_label.setBuddy(self.strategy_input) self.bot_name_label.setBuddy(self.bot_name_input) - self.base_asset_label.setBuddy(self.base_asset_input) self.quote_asset_label.setBuddy(self.quote_asset_input) self.amount_label.setBuddy(self.amount_input) self.center_price_label.setBuddy(self.center_price_input) diff --git a/dexbot/views/orig/bot_item_widget.ui b/dexbot/views/orig/bot_item_widget.ui index 0e2c171e0..a7c6b56df 100644 --- a/dexbot/views/orig/bot_item_widget.ui +++ b/dexbot/views/orig/bot_item_widget.ui @@ -146,6 +146,29 @@ + + + + + 28 + 16777215 + + + + PointingHandCursor + + + border: 0; + + + + + + + dexbot/img/pen.pngdexbot/img/pen.png + + + diff --git a/dexbot/views/orig/edit_bot_window.ui b/dexbot/views/orig/edit_bot_window.ui index a782d4c37..83cc835f3 100644 --- a/dexbot/views/orig/edit_bot_window.ui +++ b/dexbot/views/orig/edit_bot_window.ui @@ -91,28 +91,6 @@ Base Asset - - base_asset_input - - - - - - - - 0 - 0 - - - - - 80 - 16777215 - - - - Qt::ImhPreferUppercase - @@ -153,6 +131,25 @@ + + + + + 0 + 0 + + + + + 105 + 0 + + + + true + + + From 2a03eca34cd5bab738799ed4c67d38f3194865d9 Mon Sep 17 00:00:00 2001 From: Nikolay Date: Fri, 16 Feb 2018 15:57:37 +0200 Subject: [PATCH 0077/1846] WIP: Add edit bot feature --- config.yml | 6 +- dexbot/controllers/create_bot_controller.py | 32 ++++++-- dexbot/views/bot_item.py | 26 ++++--- dexbot/views/bot_list.py | 2 +- dexbot/views/edit_bot.py | 82 +++++++++++++++++++++ 5 files changed, 128 insertions(+), 20 deletions(-) diff --git a/config.yml b/config.yml index f627722e3..82b610b9a 100644 --- a/config.yml +++ b/config.yml @@ -1,4 +1,6 @@ node: wss://node.testnet.bitshares.eu -bots: {Bot 1: {target: {spread: 5.0, center_price: 42.025, amount: 0.01}, strategy: Simple - Strategy, module: dexbot.strategies.simple, market: TESTUSD/TEST, account: nikolay-codaone}} +bots: { + Bot 1: { + market: TESTUSD/TEST, account: nikolay-codaone, module: dexbot.strategies.simple, + target: {center_price: 42.025, amount: 0.01, spread: 5.0}, strategy: Simple Strategy}} diff --git a/dexbot/controllers/create_bot_controller.py b/dexbot/controllers/create_bot_controller.py index 6e1ce1d73..2565e125b 100644 --- a/dexbot/controllers/create_bot_controller.py +++ b/dexbot/controllers/create_bot_controller.py @@ -10,8 +10,9 @@ class CreateBotController: - def __init__(self, bitshares_instance): - self.bitshares = bitshares_instance or shared_bitshares_instance() + def __init__(self, main_ctrl): + self.main_ctrl = main_ctrl + self.bitshares = main_ctrl.bitshares_instance or shared_bitshares_instance() @property def strategies(self): @@ -30,12 +31,17 @@ def base_assets(self): ] return assets - @staticmethod - def is_bot_name_valid(bot_name): - bot_names = MainController.get_bots_data().keys() + def remove_bot(self, bot_name): + self.main_ctrl.remove_bot(bot_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: - return False - return True + is_name_changed = False + else: + is_name_changed = True + return is_name_changed def is_asset_valid(self, asset): try: @@ -119,3 +125,15 @@ def get_quote_asset(self, bot_data): @staticmethod def get_account(bot_data): return bot_data['account'] + + @staticmethod + def get_target_amount(bot_data): + return bot_data['target']['amount'] + + @staticmethod + def get_target_center_price(bot_data): + return bot_data['target']['center_price'] + + @staticmethod + def get_target_spread(bot_data): + return bot_data['target']['spread'] diff --git a/dexbot/views/bot_item.py b/dexbot/views/bot_item.py index c365ed4ca..7b01fa875 100644 --- a/dexbot/views/bot_item.py +++ b/dexbot/views/bot_item.py @@ -24,7 +24,7 @@ def __init__(self, botname, config, main_ctrl, view): self.pause_button.clicked.connect(self.pause_bot) self.play_button.clicked.connect(self.start_bot) - self.remove_button.clicked.connect(self.remove_widget) + self.remove_button.clicked.connect(self.remove_widget_dialog) self.edit_button.clicked.connect(self.handle_edit_bot) self.setup_ui_data(config) @@ -77,23 +77,29 @@ def set_bot_profit(self, value): def set_bot_slider(self, value): self.order_slider.setSliderPosition(value) - def remove_widget(self): + 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.controller.remove_bot(self.botname) - self.deleteLater() + 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) - # 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.bitshares_instance) + 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: - botname = edit_bot_dialog.bot_name - config = self.main_ctrl.get_bot_config(botname) - self.add_bot_widget(botname, config) \ No newline at end of file + self.remove_widget() + bot_name = edit_bot_dialog.bot_name + config = self.main_ctrl.get_bot_config(bot_name) + #self.(bot_name, config, self.view) + self.view.add_bot_widget(bot_name, config) diff --git a/dexbot/views/bot_list.py b/dexbot/views/bot_list.py index c3925cd6f..3181c55c3 100644 --- a/dexbot/views/bot_list.py +++ b/dexbot/views/bot_list.py @@ -45,7 +45,7 @@ def add_bot_widget(self, botname, config): self.ui.add_bot_button.setEnabled(False) def handle_add_bot(self): - controller = CreateBotController(self.main_ctrl.bitshares_instance) + controller = CreateBotController(self.main_ctrl) create_bot_dialog = CreateBotView(controller) return_value = create_bot_dialog.exec_() diff --git a/dexbot/views/edit_bot.py b/dexbot/views/edit_bot.py index 1870d8257..f7da21f2a 100644 --- a/dexbot/views/edit_bot.py +++ b/dexbot/views/edit_bot.py @@ -12,8 +12,90 @@ def __init__(self, controller, botname, config): 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)) 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.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) + + 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_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) + # + + 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) + # elif not self.validate_asset(base_asset): + # error_text = 'Field "Base Asset" does not have a valid asset.' + # elif not self.validate_asset(quote_asset): + # error_text = 'Field "Quote Asset" does not have a valid asset.' + # elif not self.validate_market(): + # error_text = "Market {}/{} doesn't exist.".format(base_asset, quote_asset) + # elif not self.validate_account_name(): + # error_text = "Account doesn't exist." + # elif not self.validate_account(): + # error_text = 'Private key is invalid.' + # + if error_text: + dialog = NoticeDialog(error_text) + dialog.exec_() + return False + else: + return True + + def handle_save(self): + if not self.validate_form(): + return + + ui = self + spread = float(ui.spread_input.text()[:-1]) # Remove the percentage character from the end + target = { + 'amount': float(ui.amount_input.text()), + 'center_price': float(ui.center_price_input.text()), + 'spread': spread + } + + 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 = { + 'account': ui.account_name.text(), + 'market': '{}/{}'.format(quote_asset, base_asset), + 'module': bot_module, + 'strategy': strategy, + 'target': target + } + self.bot_name = ui.bot_name_input.text() + self.controller.add_bot_config(self.bot_name, bot_data) + self.accept() From f4bc5ef3ece86c22179c9badcfc9e480d0b881cd Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Sun, 18 Feb 2018 18:39:54 +1100 Subject: [PATCH 0078/1846] make storage slightly more efificent: when saving a value, no need to wait for result --- dexbot/storage.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/dexbot/storage.py b/dexbot/storage.py index 80907d3a7..33d2200cf 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -1,3 +1,4 @@ +import sqlalchemy import os import json import threading @@ -52,13 +53,13 @@ def __init__(self, category): self.category = category def __setitem__(self, key, value): - worker.execute(worker.set_item, self.category, key, value) + worker.execute_noreturn(worker.set_item, self.category, key, value) def __getitem__(self, key): return worker.execute(worker.get_item, self.category, key) def __delitem__(self, key): - worker.execute(worker.del_item, self.category, key) + worker.execute_noreturn(worker.del_item, self.category, key) def __contains__(self, key): return worker.execute(worker.contains, self.category, key) @@ -67,7 +68,7 @@ def items(self): return worker.execute(worker.get_items, self.category) def clear(self): - worker.execute(worker.clear, self.category) + worker.execute_noreturn(worker.clear, self.category) class DatabaseWorker(threading.Thread): @@ -93,7 +94,8 @@ def __init__(self): def run(self): for func, args, token in iter(self.task_queue.get, None): - func(*args, token) + args = args+(token,) + func(*args) def get_result(self, token): delay = 0.001 @@ -112,6 +114,9 @@ def execute(self, func, *args): self.task_queue.put((func, args, 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, token): value = json.dumps(value) e = self.session.query(Config).filter_by( @@ -124,7 +129,6 @@ def set_item(self, category, key, value, token): e = Config(category, key, value) self.session.add(e) self.session.commit() - self.results[token] = None def get_item(self, category, key, token): e = self.session.query(Config).filter_by( @@ -144,7 +148,6 @@ def del_item(self, category, key, token): ).first() self.session.delete(e) self.session.commit() - self.results[token] = None def contains(self, category, key, token): e = self.session.query(Config).filter_by( @@ -167,7 +170,6 @@ def clear(self, category, token): for row in rows: self.session.delete(row) self.session.commit() - self.results[token] = None # Derive sqlite file directory From cb731336e6533e58e1ce84e6b83b2ec073586e25 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Sun, 18 Feb 2018 18:43:37 +1100 Subject: [PATCH 0079/1846] make sure we are tracking changes in uupstream to cli.py: handling excpetions rather than sudden death with sys.exit(70) --- dexbot/cli.py | 25 ++++++++++++++----------- dexbot/ui.py | 32 +++++++++++++++++++++++++------- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index 93ec9ca0e..7cb257dd7 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -2,8 +2,7 @@ import yaml import logging import click -import os.path, os - +import os.path, os, sys from .ui import ( verbose, chain, @@ -18,6 +17,7 @@ from dexbot.bot import BotInfrastructure from dexbot.cli_conf import configure_dexbot +import dexbot.errors as errors log = logging.getLogger(__name__) @@ -61,15 +61,18 @@ def main(ctx, **kwargs): def run(ctx): """ Continuously run the bot """ - bot = BotInfrastructure(ctx.config) - if ctx.obj['systemd']: - try: - import sdnotify # a soft dependency on sdnotify -- don't crash on non-systemd systems - n = sdnotify.SystemdNotifier() - n.notify("READY=1") - except: - warning("sdnotify not available") - bot.run() + try: + bot = BotInfrastructure(ctx.config) + if ctx.obj['systemd']: + try: + import sdnotify # a soft dependency on sdnotify -- don't crash on non-systemd systems + n = sdnotify.SystemdNotifier() + n.notify("READY=1") + except: + warning("sdnotify not available") + bot.run() + except errors.NoBotsAvailable: + sys.exit(70) # 70= "Software error" in /usr/include/sysexts.h @main.command() @click.pass_context diff --git a/dexbot/ui.py b/dexbot/ui.py index 913d425f9..fd6c4a102 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -14,17 +14,35 @@ def verbose(f): @click.pass_context def new_func(ctx, *args, **kwargs): - global log verbosity = [ "critical", "error", "warn", "info", "debug" ][int(min(ctx.obj.get("verbose", 0), 4))] - log.setLevel(getattr(logging, verbosity.upper())) - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + 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') + 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') + 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') + level = getattr(logging, verbosity.upper()) + # use special format for special bots logger ch = logging.StreamHandler() - ch.setLevel(getattr(logging, verbosity.upper())) - ch.setFormatter(formatter) - log.addHandler(ch) - + ch.setFormatter(formatter2) + logging.getLogger("dexbot.per_bot").addHandler(ch) + logging.getLogger("dexbot.per_bot").setLevel(level) + logging.getLogger("dexbot.per_bot").propagate = False # don't double up with root logger + # set the root logger with basic format + ch = logging.StreamHandler() + ch.setFormatter(formatter1) + logging.getLogger("dexbot").setLevel(level) + logging.getLogger("dexbot").addHandler(ch) + # and don't double up on the root logger + logging.getLogger("").handlers = [] + # GrapheneAPI logging if ctx.obj["verbose"] > 4: verbosity = [ From 6a2cde2660170abdaf0806fc3f7481d376f34f2a Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Sun, 18 Feb 2018 20:07:03 +1100 Subject: [PATCH 0080/1846] use threading.Event to wait for results instead of polling --- dexbot/storage.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/dexbot/storage.py b/dexbot/storage.py index 33d2200cf..2e211bfb3 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -89,6 +89,8 @@ def __init__(self): self.task_queue = queue.Queue() self.results = {} + self.lock = threading.Lock() + self.event = threading.Event() self.daemon = True self.start() @@ -98,16 +100,20 @@ def run(self): func(*args) def get_result(self, token): - delay = 0.001 while True: - if token in self.results: - return_value = self.results[token] - del self.results[token] - return return_value - - time.sleep(delay) - if delay < 5: - delay += delay + with self.lock: + if token in self.results: + return_value = self.results[token] + del self.results[token] + return return_value + else: + self.event.clear() + self.event.wait() + + def set_result(self, token, result): + with self.lock: + self.results[token] = result + self.event.set() def execute(self, func, *args): token = str(uuid.uuid4) @@ -139,7 +145,7 @@ def get_item(self, category, key, token): result = None else: result = json.loads(e.value) - self.results[token] = result + self.set_result(token,result) def del_item(self, category, key, token): e = self.session.query(Config).filter_by( @@ -154,14 +160,14 @@ def contains(self, category, key, token): category=category, key=key ).first() - self.results[token] = bool(e) + self.set_result(token,bool(e)) 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.results[token] = result + self.set_result(token,result) def clear(self, category, token): rows = self.session.query(Config).filter_by( From 6c3db2c980c86e5a38e5bf5190cf113d8570de98 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 19 Feb 2018 08:35:31 +1100 Subject: [PATCH 0081/1846] bot now has initisliation split __init___ just sets variables and is very fast init_bots does the heavy lifting and may be very slow this is called from run(), so will be in a background thread on GUI, and won't freeze the GUI --- dexbot/bot.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/dexbot/bot.py b/dexbot/bot.py index 786088eb6..ee7301665 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -34,9 +34,14 @@ def __init__( # BitShares instance self.bitshares = bitshares_instance or shared_bitshares_instance() - self.config = config - + self.view = view + + 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): @@ -60,10 +65,10 @@ def __init__( 'Strategy' ) self.bots[botname] = klass( - config=config, + config=self.config, name=botname, bitshares_instance=self.bitshares, - view=view + view=self.view ) markets.add(bot['market']) accounts.add(bot['account']) @@ -125,6 +130,7 @@ def on_account(self, accountupdate): self.bots[botname].log.exception(".onAccountUpdate()") def run(self): + self.init_bots() self.notify.listen() def stop(self): From 841a38b5cf2c66083bc4f3f5a4c02c7c1f5aa9ad Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 19 Feb 2018 11:16:45 +1100 Subject: [PATCH 0082/1846] use self.config --- dexbot/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/bot.py b/dexbot/bot.py index ee7301665..393545546 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -52,7 +52,7 @@ def init_bots(self): markets = set() # Initialize bots: - for botname, bot in config["bots"].items(): + 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)}) continue From 6aeb2b29a8857636d615a34302f020ebfb6bfa75 Mon Sep 17 00:00:00 2001 From: Nikolay Date: Mon, 19 Feb 2018 09:57:22 +0200 Subject: [PATCH 0083/1846] Add edit bot form validation --- dexbot/controllers/create_bot_controller.py | 8 +-- dexbot/views/bot_item.py | 4 +- dexbot/views/create_bot.py | 22 +++---- dexbot/views/edit_bot.py | 69 ++++++++------------- 4 files changed, 43 insertions(+), 60 deletions(-) diff --git a/dexbot/controllers/create_bot_controller.py b/dexbot/controllers/create_bot_controller.py index 2565e125b..5854413a8 100644 --- a/dexbot/controllers/create_bot_controller.py +++ b/dexbot/controllers/create_bot_controller.py @@ -37,11 +37,11 @@ def remove_bot(self, bot_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: - is_name_changed = False + if bot_name in bot_names and old_bot_name not in bot_names: + is_name_valid = False else: - is_name_changed = True - return is_name_changed + is_name_valid = True + return is_name_valid def is_asset_valid(self, asset): try: diff --git a/dexbot/views/bot_item.py b/dexbot/views/bot_item.py index 7b01fa875..3882d5712 100644 --- a/dexbot/views/bot_item.py +++ b/dexbot/views/bot_item.py @@ -90,7 +90,6 @@ def remove_widget(self): # 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) @@ -98,8 +97,7 @@ def handle_edit_bot(self): # User clicked save if return_value == 1: - self.remove_widget() bot_name = edit_bot_dialog.bot_name config = self.main_ctrl.get_bot_config(bot_name) - #self.(bot_name, config, self.view) + self.remove_widget() self.view.add_bot_widget(bot_name, config) diff --git a/dexbot/views/create_bot.py b/dexbot/views/create_bot.py index 597249837..133ff9ed9 100644 --- a/dexbot/views/create_bot.py +++ b/dexbot/views/create_bot.py @@ -50,17 +50,17 @@ def validate_form(self): 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) - elif not self.validate_asset(base_asset): - error_text = 'Field "Base Asset" does not have a valid asset.' - elif not self.validate_asset(quote_asset): - error_text = 'Field "Quote Asset" does not have a valid asset.' - elif not self.validate_market(): - error_text = "Market {}/{} doesn't exist.".format(base_asset, quote_asset) - elif not self.validate_account_name(): - error_text = "Account doesn't exist." - elif not self.validate_account(): - error_text = 'Private key is invalid.' + error_text += 'Bot name needs to be unique. "{}" is already in use.'.format(bot_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): + 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' + 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 error_text: dialog = NoticeDialog(error_text) diff --git a/dexbot/views/edit_bot.py b/dexbot/views/edit_bot.py index f7da21f2a..3a5152395 100644 --- a/dexbot/views/edit_bot.py +++ b/dexbot/views/edit_bot.py @@ -29,43 +29,29 @@ 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_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) - # + + 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.ui.base_asset_input.currentText() - # quote_asset = self.ui.quote_asset_input.text() + base_asset = self.base_asset_input.currentText() + quote_asset = self.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) - # elif not self.validate_asset(base_asset): - # error_text = 'Field "Base Asset" does not have a valid asset.' - # elif not self.validate_asset(quote_asset): - # error_text = 'Field "Quote Asset" does not have a valid asset.' - # elif not self.validate_market(): - # error_text = "Market {}/{} doesn't exist.".format(base_asset, quote_asset) - # elif not self.validate_account_name(): - # error_text = "Account doesn't exist." - # elif not self.validate_account(): - # error_text = 'Private key is invalid.' - # + bot_name = self.bot_name_input.text() + error_text += 'Bot name needs to be unique. "{}" is already in use.'.format(bot_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): + 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' + if error_text: dialog = NoticeDialog(error_text) dialog.exec_() @@ -77,25 +63,24 @@ def handle_save(self): if not self.validate_form(): return - ui = self - spread = float(ui.spread_input.text()[:-1]) # Remove the percentage character from the end + spread = float(self.spread_input.text()[:-1]) # Remove the percentage character from the end target = { - 'amount': float(ui.amount_input.text()), - 'center_price': float(ui.center_price_input.text()), + 'amount': float(self.amount_input.text()), + 'center_price': float(self.center_price_input.text()), 'spread': spread } - base_asset = ui.base_asset_input.currentText() - quote_asset = ui.quote_asset_input.text() - strategy = ui.strategy_input.currentText() + 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 = { - 'account': ui.account_name.text(), + 'account': self.account_name.text(), 'market': '{}/{}'.format(quote_asset, base_asset), 'module': bot_module, 'strategy': strategy, 'target': target } - self.bot_name = ui.bot_name_input.text() + self.bot_name = self.bot_name_input.text() self.controller.add_bot_config(self.bot_name, bot_data) self.accept() From 4ea192ceab159728a3326a2eb311f8e5c394a0a5 Mon Sep 17 00:00:00 2001 From: Nikolay Date: Mon, 19 Feb 2018 10:08:07 +0200 Subject: [PATCH 0084/1846] Change default blockchain node in the config --- config.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/config.yml b/config.yml index 82b610b9a..38d9ef064 100644 --- a/config.yml +++ b/config.yml @@ -1,6 +1,3 @@ -node: wss://node.testnet.bitshares.eu +node: wss://bitshares.openledger.info/ws -bots: { - Bot 1: { - market: TESTUSD/TEST, account: nikolay-codaone, module: dexbot.strategies.simple, - target: {center_price: 42.025, amount: 0.01, spread: 5.0}, strategy: Simple Strategy}} +bots: {} \ No newline at end of file From 0037e4cb2a8a6ac3bd093ba110fa4c68e6f8d6c7 Mon Sep 17 00:00:00 2001 From: Nikolay Date: Thu, 1 Mar 2018 10:32:55 +0200 Subject: [PATCH 0085/1846] Add dialog window on bot save, change decimal points for create bot and edit bot windows --- dexbot/views/edit_bot.py | 13 ++++++++++++- dexbot/views/gen/create_bot_window.py | 8 ++++---- dexbot/views/gen/edit_bot_window.py | 6 +++--- dexbot/views/orig/create_bot_window.ui | 4 ++-- dexbot/views/orig/edit_bot_window.ui | 4 ++-- 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/dexbot/views/edit_bot.py b/dexbot/views/edit_bot.py index 3a5152395..7b004359c 100644 --- a/dexbot/views/edit_bot.py +++ b/dexbot/views/edit_bot.py @@ -2,7 +2,7 @@ from dexbot.views.notice import NoticeDialog from dexbot.views.gen.edit_bot_window import Ui_Dialog - +from dexbot.views.confirmation import ConfirmationDialog class EditBotView(QtWidgets.QDialog, Ui_Dialog): def __init__(self, controller, botname, config): @@ -59,10 +59,21 @@ def validate_form(self): else: return True + def handle_save_dialog(self): + dialog = ConfirmationDialog('Saving bot will recreate it: cancel all current orders, stop it, start again' + ' and create new orders based on new settings. ' + '\n Are you sure you want to save bot?') + 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 target = { 'amount': float(self.amount_input.text()), diff --git a/dexbot/views/gen/create_bot_window.py b/dexbot/views/gen/create_bot_window.py index 891326915..eecede3c1 100644 --- a/dexbot/views/gen/create_bot_window.py +++ b/dexbot/views/gen/create_bot_window.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'dexbot/views/orig/create_bot_window.ui' +# Form implementation generated from reading ui file 'orig/create_bot_window.ui' # -# Created by: PyQt5 UI code generator 5.9.2 +# Created by: PyQt5 UI code generator 5.10 # # WARNING! All changes made in this file will be lost! @@ -67,7 +67,7 @@ def setupUi(self, Dialog): self.amount_input.setSizePolicy(sizePolicy) self.amount_input.setMinimumSize(QtCore.QSize(140, 0)) self.amount_input.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) - self.amount_input.setDecimals(5) + self.amount_input.setDecimals(8) self.amount_input.setMaximum(999999999.999) self.amount_input.setObjectName("amount_input") self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.amount_input) @@ -91,7 +91,7 @@ def setupUi(self, Dialog): self.center_price_input.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) self.center_price_input.setAccelerated(False) self.center_price_input.setProperty("showGroupSeparator", False) - self.center_price_input.setDecimals(5) + self.center_price_input.setDecimals(8) self.center_price_input.setMinimum(-999999999.999) self.center_price_input.setMaximum(999999999.999) self.center_price_input.setObjectName("center_price_input") diff --git a/dexbot/views/gen/edit_bot_window.py b/dexbot/views/gen/edit_bot_window.py index 659c66820..a7933b253 100644 --- a/dexbot/views/gen/edit_bot_window.py +++ b/dexbot/views/gen/edit_bot_window.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'dexbot/views/orig/edit_bot_window.ui' +# Form implementation generated from reading ui file 'orig/edit_bot_window.ui' # # Created by: PyQt5 UI code generator 5.10 # @@ -106,7 +106,7 @@ def setupUi(self, Dialog): self.amount_input.setSizePolicy(sizePolicy) self.amount_input.setMinimumSize(QtCore.QSize(140, 0)) self.amount_input.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) - self.amount_input.setDecimals(5) + self.amount_input.setDecimals(8) self.amount_input.setMaximum(999999999.999) self.amount_input.setObjectName("amount_input") self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.amount_input) @@ -130,7 +130,7 @@ def setupUi(self, Dialog): self.center_price_input.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) self.center_price_input.setAccelerated(False) self.center_price_input.setProperty("showGroupSeparator", False) - self.center_price_input.setDecimals(5) + self.center_price_input.setDecimals(8) self.center_price_input.setMinimum(-999999999.999) self.center_price_input.setMaximum(999999999.999) self.center_price_input.setObjectName("center_price_input") diff --git a/dexbot/views/orig/create_bot_window.ui b/dexbot/views/orig/create_bot_window.ui index fa72fded6..80c457b81 100644 --- a/dexbot/views/orig/create_bot_window.ui +++ b/dexbot/views/orig/create_bot_window.ui @@ -126,7 +126,7 @@ - 5 + 8 999999999.998999953269958 @@ -185,7 +185,7 @@ false - 5 + 8 -999999999.998999953269958 diff --git a/dexbot/views/orig/edit_bot_window.ui b/dexbot/views/orig/edit_bot_window.ui index 83cc835f3..7454075eb 100644 --- a/dexbot/views/orig/edit_bot_window.ui +++ b/dexbot/views/orig/edit_bot_window.ui @@ -249,7 +249,7 @@ - 5 + 8 999999999.998999953269958 @@ -308,7 +308,7 @@ false - 5 + 8 -999999999.998999953269958 From 66591fb99c8e0663607ee8e93dfc99e2bcf072f8 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 1 Mar 2018 10:34:29 +0200 Subject: [PATCH 0086/1846] Fix pep8 issues in storage.py --- dexbot/storage.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dexbot/storage.py b/dexbot/storage.py index 2e211bfb3..44500692a 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -123,7 +123,7 @@ def execute(self, func, *args): def execute_noreturn(self, func, *args): self.task_queue.put((func, args, None)) - def set_item(self, category, key, value, token): + def set_item(self, category, key, value): value = json.dumps(value) e = self.session.query(Config).filter_by( category=category, @@ -145,9 +145,9 @@ 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, token): + def del_item(self, category, key): e = self.session.query(Config).filter_by( category=category, key=key @@ -160,16 +160,16 @@ def contains(self, category, key, token): category=category, key=key ).first() - self.set_result(token,bool(e)) + self.set_result(token, bool(e)) 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, token): + def clear(self, category): rows = self.session.query(Config).filter_by( category=category ) From aef889b86222103b79bbe756b283ad33f3edfc66 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 1 Mar 2018 14:04:10 +0200 Subject: [PATCH 0087/1846] Change image loading to use qt resources --- dexbot/resources/__init__.py | 0 dexbot/resources/icons.qrc | 8 + dexbot/resources/icons_rc.py | 422 +++++++++++++++++++++++++++ dexbot/{ => resources}/img/bin.png | Bin dexbot/{ => resources}/img/pause.png | Bin dexbot/{ => resources}/img/pen.png | Bin dexbot/{ => resources}/img/play.png | Bin dexbot/views/gen/bot_item_widget.py | 13 +- dexbot/views/orig/bot_item_widget.ui | 20 +- 9 files changed, 448 insertions(+), 15 deletions(-) create mode 100644 dexbot/resources/__init__.py create mode 100644 dexbot/resources/icons.qrc create mode 100644 dexbot/resources/icons_rc.py rename dexbot/{ => resources}/img/bin.png (100%) rename dexbot/{ => resources}/img/pause.png (100%) rename dexbot/{ => resources}/img/pen.png (100%) rename dexbot/{ => resources}/img/play.png (100%) diff --git a/dexbot/resources/__init__.py b/dexbot/resources/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dexbot/resources/icons.qrc b/dexbot/resources/icons.qrc new file mode 100644 index 000000000..66469651f --- /dev/null +++ b/dexbot/resources/icons.qrc @@ -0,0 +1,8 @@ + + + img/bin.png + img/pause.png + img/pen.png + img/play.png + + diff --git a/dexbot/resources/icons_rc.py b/dexbot/resources/icons_rc.py new file mode 100644 index 000000000..c4b5596ad --- /dev/null +++ b/dexbot/resources/icons_rc.py @@ -0,0 +1,422 @@ +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created by: The Resource Compiler for PyQt5 (Qt v5.9.3) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + +qt_resource_data = b"\ +\x00\x00\x07\x5e\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x40\x00\x00\x00\x40\x08\x06\x00\x00\x00\xaa\x69\x71\xde\ +\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ +\x00\x00\x07\x15\x49\x44\x41\x54\x78\x9c\xe5\x9b\x5b\x4f\x13\x4d\ +\x1c\xc6\x9f\xdd\xed\x89\xc5\xed\x81\x02\xa5\x11\x14\x25\x0d\xb5\ +\xa2\x80\x04\x8d\x4d\x54\x0c\x12\xbc\x21\xfa\x0d\x4c\xbc\xf0\x2b\ +\x79\xe9\xb5\xf1\xa2\x88\x87\x60\x02\x09\x21\x31\x72\xa1\x44\x82\ +\xa1\x45\xa5\x6d\x4c\x4c\xcb\x99\x6e\x4f\xb4\xbb\x3b\xf3\x5e\xf4\ +\x6d\x43\x29\xd8\x02\xbb\x5b\x94\x5f\xd2\xc0\x6e\x67\x67\x9e\x7d\ +\x3a\x33\x3b\x3b\xf3\x1f\x06\x3a\x41\x08\xa1\x94\x52\xec\xff\x14\ +\x61\x18\x06\x2c\xcb\x82\x61\x18\x30\x0c\x03\x00\x60\x59\x96\xd1\ +\x5a\x97\xa6\x05\x28\x8a\x42\xb7\xb7\xb7\x11\x8b\xc5\xb0\xb5\xb5\ +\x85\x74\x3a\x8d\x6c\x36\x8b\x4c\x26\x53\xfa\xab\x28\x0a\x2c\x16\ +\x0b\x78\x9e\x47\x43\x43\x03\x78\x9e\x07\xcf\xf3\x10\x04\x01\x2e\ +\x97\x0b\xad\xad\xad\xb0\x58\x2c\x9a\x99\xa1\x49\xa6\x99\x4c\x86\ +\x7e\xff\xfe\x1d\x4b\x4b\x4b\x88\xc7\xe3\x10\x45\x11\xd9\x6c\xf6\ +\x48\x79\x70\x1c\x07\x41\x10\xe0\x70\x38\x70\xf9\xf2\x65\xf4\xf4\ +\xf4\xc0\xe5\x72\x81\xe3\x38\x55\x35\xab\x92\x19\x21\x84\xca\xb2\ +\x8c\xad\xad\x2d\xcc\xcc\xcc\xe0\xdb\xb7\x6f\x90\x24\x49\x8d\xac\ +\xcb\x68\x6d\x6d\xc5\xdd\xbb\x77\xe1\xf3\xf9\x60\xb1\x58\x54\x31\ +\xe3\xc4\x19\x10\x42\x68\x38\x1c\xc6\xfc\xfc\x3c\x16\x17\x17\x91\ +\xcf\xe7\x4f\x9a\x65\x55\x3a\x3a\x3a\xd0\xd7\xd7\x87\xde\xde\x5e\ +\x08\x82\x70\xa2\x7b\x38\xd1\xc5\x9b\x9b\x9b\x74\x72\x72\x12\xe1\ +\x70\x18\xe9\x74\xba\xac\x53\xd3\x1a\xa3\xd1\x08\xa7\xd3\x89\x7b\ +\xf7\xee\xe1\xda\xb5\x6b\x30\x1a\x8d\xc7\xba\x97\x63\x5d\x24\x49\ +\x12\x5d\x5e\x5e\xc6\xc4\xc4\x04\x12\x89\xc4\x71\xb2\x50\x0d\x86\ +\x61\xd0\xdf\xdf\x8f\xd1\xd1\x51\x58\xad\xd6\x23\x77\x96\x47\x36\ +\x40\x14\x45\xfa\xf1\xe3\x47\xcc\xcd\xcd\x61\x77\x77\xf7\xa8\x97\ +\x6b\xc6\xa5\x4b\x97\x30\x34\x34\x04\x8f\xc7\x73\xa4\xbe\xe1\x48\ +\x06\xac\xaf\xaf\xd3\xf1\xf1\x71\xac\xac\xac\xe8\x5a\xdd\x6b\x85\ +\xe7\x79\x8c\x8c\x8c\xe0\xd6\xad\x5b\x35\x9b\x50\x53\xa2\x7c\x3e\ +\x4f\x57\x56\x56\x10\x08\x04\xea\x5e\xe5\xab\xc1\x30\x0c\x6e\xde\ +\xbc\x89\xa1\xa1\x21\xd8\xed\xf6\xaa\x4d\xa2\xaa\x01\x8a\xa2\xd0\ +\x85\x85\x05\x4c\x4c\x4c\x1c\xf9\x59\x5e\x4f\x7c\x3e\x1f\x1e\x3f\ +\x7e\x0c\x9b\xcd\xf6\xc7\x7b\x34\x54\xcb\x68\x6d\x6d\x0d\x93\x93\ +\x93\x7f\xd5\xcd\x03\x40\x30\x18\x44\x6b\x6b\x6b\xd5\x74\xec\x9f\ +\xbe\x14\x45\x91\xbe\x7a\xf5\xea\xd4\x57\xfb\x83\xa0\x94\x62\x76\ +\x76\x16\x9f\x3f\x7f\xa6\x8a\xa2\x1c\xda\x61\x1d\x5a\x03\x76\x76\ +\x76\x68\x20\x10\xc0\xef\xdf\xbf\xb5\x51\xa8\x03\x84\x10\x8c\x8f\ +\x8f\xc3\x64\x32\x81\x10\x42\x0f\xea\x0f\x0e\xac\x01\x92\x24\xd1\ +\x99\x99\x19\x2c\x2d\x2d\x69\xaf\x52\x63\x24\x49\xc2\x87\x0f\x1f\ +\xb0\xb1\xb1\x71\xe0\xf7\x07\x1a\x10\x8f\xc7\x31\x3f\x3f\xaf\xa9\ +\x30\x3d\xd9\xd8\xd8\xc0\xec\xec\x2c\x08\x21\x15\x4d\xa1\xc2\x00\ +\x45\x51\xe8\xfb\xf7\xef\x91\xcb\xe5\xf4\x51\xa7\x13\x5f\xbf\x7e\ +\x45\x24\x12\xa9\x38\x5f\x61\x40\x28\x14\x42\x38\x1c\xd6\x45\x94\ +\x9e\x48\x92\x84\x37\x6f\xde\x60\x7f\x87\x58\x66\x40\x32\x99\xa4\ +\xb3\xb3\xb3\xfa\x2a\xd3\x91\xb5\xb5\x35\x2c\x2e\x2e\x96\x35\x85\ +\x92\x01\x84\x10\x1a\x0c\x06\xff\xea\x5e\xbf\x1a\x8a\xa2\x60\x7e\ +\x7e\x1e\xa9\x54\xaa\x74\xae\x64\x80\x2c\xcb\x58\x58\x58\xd0\x64\ +\x22\xe3\x34\xf1\xe3\xc7\x8f\xb2\x1f\xb9\x64\x40\x3e\x9f\x47\x2c\ +\x16\xab\x8b\x28\x3d\x21\x84\x94\x75\x86\x25\x03\x22\x91\x08\xd2\ +\xe9\x74\x5d\x44\xe9\x4d\x28\x14\x2a\xfd\x6f\x00\x0a\xed\xff\xc5\ +\x8b\x17\x9a\x15\xe8\xf7\xfb\x71\xe1\xc2\x85\xb2\x73\xef\xde\xbd\ +\x43\x32\x99\x2c\x1d\x5b\x2c\x16\xf8\xfd\xfe\xb2\xf1\xbb\x2c\xcb\ +\x98\x9e\x9e\xc6\xf6\xf6\xb6\xaa\x7a\x56\x57\x57\x11\x8b\xc5\xa8\ +\xdb\xed\x66\x0c\x00\x20\x8a\x22\x56\x56\x56\x54\x2d\x64\x2f\x9d\ +\x9d\x9d\xb8\x7e\xfd\x7a\xd9\xb9\xa9\xa9\xa9\x32\x03\x8c\x46\x23\ +\xba\xba\xba\xd0\xd5\xd5\x55\x3a\x97\xcf\xe7\xf1\xe9\xd3\x27\xd5\ +\x0d\x00\x80\x2f\x5f\xbe\x00\xf8\xbf\x09\xfc\xfc\xf9\x13\x8a\xa2\ +\xa8\x5e\xc8\x69\x26\x18\x0c\x22\x97\xcb\x51\x16\xc0\x99\xe8\xfc\ +\xf6\x93\xcd\x66\x91\x48\x24\xc0\x12\x42\xe8\xde\xe7\xe2\x59\x81\ +\x10\x82\x6c\x36\x0b\x56\x96\xe5\xbf\x6e\xb2\x43\x0d\x08\x21\xc8\ +\x64\x32\x60\x73\xb9\x9c\x2e\x8b\x19\xa7\x0d\x45\x51\x0a\x35\x20\ +\x97\xcb\xfd\x73\x6f\x7e\xb5\x50\x6a\x02\xb9\x5c\xee\x9f\x1f\xfe\ +\x1e\x44\xa9\x09\xc8\xb2\x7c\xe6\x1e\x81\x45\x24\x49\x02\x6b\x36\ +\x9b\x61\x34\x1a\xeb\xad\x45\x77\x18\x86\x29\xc4\x1d\x98\x4c\xa6\ +\x33\x69\x00\xcb\xb2\x68\x68\x68\x28\xd4\x00\x93\xc9\x54\x6f\x3d\ +\xba\xc3\xb2\x2c\x1a\x1b\x1b\x0b\x06\x98\xcd\xe6\x7a\xeb\xd1\x1d\ +\x96\x65\x0b\x4d\xc0\x60\x30\x30\x0d\x0d\x0d\xf5\xd6\xa3\x3b\x1c\ +\xc7\x15\x9a\x00\x00\x38\x1c\x8e\x7a\xeb\xd1\x9d\x52\x13\x00\x0a\ +\x6b\xeb\x67\x8d\x96\x96\x96\x42\x40\x05\x50\x30\xc0\x62\xb1\xd4\ +\x5b\x93\xae\xf4\xf7\xf7\xc3\x60\x30\x30\x2c\x00\x18\x0c\x06\x66\ +\x60\x60\x40\xb3\xc2\x76\x77\x77\x91\x4a\xa5\xca\x3e\x84\x90\xb2\ +\x34\x94\x52\x64\xb3\xd9\xb2\x34\xe9\x74\xba\x22\x9d\x1a\x98\x4c\ +\x26\x5c\xbd\x7a\x15\xc0\x9e\xf8\x80\x68\x34\x4a\x9f\x3f\x7f\xae\ +\x7a\x61\x00\xd0\xd4\xd4\x84\xfd\x1d\xed\xea\xea\x2a\x64\x59\x2e\ +\x1d\xb3\x2c\x8b\xa6\xa6\xa6\xb2\x27\x12\xa5\x14\xeb\xeb\xeb\xaa\ +\x0f\xd5\xbb\xbb\xbb\xf1\xf4\xe9\x53\x06\xd8\xb3\x3a\xdc\xd6\xd6\ +\x06\xa7\xd3\x89\xcd\xcd\x4d\x55\x0b\x03\x80\xad\xad\xad\xaa\x69\ +\x08\x21\x87\x2e\x60\xaa\x4d\xf1\xd7\x07\xf6\xcc\x0a\x1b\x0c\x06\ +\x78\x3c\x9e\x52\x9c\xee\xbf\x8a\xdd\x6e\x47\x47\x47\x47\xe9\xb8\ +\x64\x00\xcb\xb2\xe8\xed\xed\x85\xcd\x66\xab\x8b\x30\xbd\xb8\x72\ +\xe5\x0a\x9a\x9b\x9b\x4b\xc7\x7b\x0d\x60\x2e\x5e\xbc\x88\xee\xee\ +\xee\xba\x08\xd3\x03\x8b\xc5\x82\xc1\xc1\x41\x98\x4c\xa6\x52\x35\ +\x2f\x5b\x1c\xe5\x38\x8e\xb9\x73\xe7\x0e\x78\x9e\xd7\x5f\x9d\x0e\ +\xdc\xb8\x71\x03\x6e\xb7\xbb\xec\x5c\xc5\xf2\x78\x31\xfc\xf4\x5f\ +\xc3\x66\xb3\xe1\xe1\xc3\x87\x15\x61\x73\x15\x06\xb0\x2c\xcb\x0c\ +\x0c\x0c\xa0\xad\xad\x4d\x3f\x75\x1a\xc3\x71\x1c\xee\xdf\xbf\x0f\ +\xb3\xd9\x5c\x5b\x8c\x10\xcf\xf3\x78\xf0\xe0\x01\x1a\x1b\x1b\xb5\ +\x57\xa7\x03\x3d\x3d\x3d\x15\x2b\x53\x45\x0e\x8c\x12\xe3\x38\x8e\ +\x51\x14\x85\x26\x93\x49\xbc\x7e\xfd\x5a\x53\x71\x5a\x63\xb5\x5a\ +\x31\x36\x36\x56\x31\x10\x2b\x72\x68\x9c\x20\xc7\x71\x8c\xdf\xef\ +\x67\xfe\x8f\xbb\xd5\x4c\xa0\x96\x38\x1c\x0e\x3c\x7b\xf6\x0c\x82\ +\x20\x30\x87\x85\xcc\xfe\x31\x50\x12\x00\x46\x46\x46\xe0\xf3\xf9\ +\xd4\x57\xa7\x31\x82\x20\x60\x6c\x6c\x0c\x2d\x2d\x2d\x7f\x1c\xd9\ +\x55\x35\xa0\xb1\xb1\x11\xa3\xa3\xa3\x10\x04\x41\x3d\x75\x3a\x70\ +\xfb\xf6\x6d\x78\xbd\xde\xaa\xe9\x6a\x1e\xf7\xa6\x52\x29\x1a\x08\ +\x04\xb0\xbc\xbc\x7c\xaa\xd7\x11\x9c\x4e\x27\x86\x87\x87\x31\x30\ +\x30\xa0\x5e\xb8\x7c\x91\x74\x3a\x4d\xe7\xe6\xe6\x30\x35\x35\xa5\ +\xc9\x6b\xea\x49\x69\x6f\x6f\xc7\xa3\x47\x8f\xd0\xde\xde\x5e\xf3\ +\xce\x91\x63\xbd\xf9\x2c\x2c\x2c\xd0\x97\x2f\x5f\x9e\xaa\x05\x95\ +\xce\xce\x4e\x3c\x79\xf2\x04\x3c\xcf\x6b\xbb\x65\xa6\x48\x38\x1c\ +\xa6\xd3\xd3\xd3\x88\x44\x22\x75\x35\xc2\x6e\xb7\xa3\xaf\xaf\x0f\ +\xc3\xc3\xc3\x65\x63\xfc\x5a\x39\xb6\x01\x84\x10\x2a\x8a\x22\xa2\ +\xd1\x28\xe6\xe6\xe6\x10\x8d\x46\x75\xdd\x46\x73\xee\xdc\x39\x0c\ +\x0e\x0e\xc2\xe7\xf3\xa1\xad\xad\xed\x58\x37\x0f\xa8\xb4\x71\x52\ +\x96\x65\x1a\x0c\x06\xf1\xf6\xed\x5b\x24\x93\x49\xcd\x6a\x04\xc3\ +\x30\x30\x1a\x8d\xe8\xef\xef\xc7\xf0\xf0\x70\xd5\xdd\x20\x35\xe5\ +\xa9\x86\xb0\x22\xb9\x5c\x8e\x2e\x2f\x2f\x23\x1a\x8d\x22\x16\x8b\ +\x21\x1e\x8f\x23\x93\xc9\x9c\x28\x4f\x8e\xe3\xe0\x74\x3a\xe1\x76\ +\xbb\x71\xfe\xfc\x79\x78\xbd\x5e\xb8\x5c\x2e\xd5\x74\x6b\x32\xfd\ +\x23\xcb\x32\x4d\xa5\x52\x48\x24\x12\xf8\xf5\xeb\x17\x42\xa1\x10\ +\xa2\xd1\x68\xd9\x1c\x60\x35\x1c\x0e\x07\xbc\x5e\x2f\x3c\x1e\x0f\ +\x9a\x9b\x9b\x61\xb5\x5a\x61\x36\x9b\x55\xdf\x44\xad\xdb\xfc\x97\ +\x24\x49\x54\x14\x45\x24\x12\x09\xec\xec\xec\x40\x14\x45\xec\xec\ +\xec\x40\x92\x24\x08\x82\x00\x9b\xcd\x06\xab\xd5\x0a\xbb\xdd\x0e\ +\x87\xc3\x71\xe4\xde\xfc\xb8\xfc\x07\x2b\x31\xf0\x27\xb2\xc7\xa5\ +\x82\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x03\x00\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x04\x67\x41\x4d\x41\x00\x00\xb1\x8f\x0b\xfc\x61\x05\x00\x00\x00\ +\x09\x70\x48\x59\x73\x00\x00\x0e\xc3\x00\x00\x0e\xc3\x01\xc7\x6f\ +\xa8\x64\x00\x00\x02\x95\x49\x44\x41\x54\x58\x47\xb5\x97\x49\xc8\ +\x8d\x61\x18\x86\xcf\x82\x4c\x3b\x64\x9e\xe7\xa1\x64\x48\x84\x62\ +\x61\xd8\x2a\x21\x36\x22\x89\xc8\x4a\x21\x2c\x58\xca\x90\x85\xd5\ +\xdf\xaf\x58\xb0\x93\xb0\xb0\x62\x65\x21\x2b\x53\xd9\xd8\x10\x3b\ +\x43\x64\x63\x88\xeb\xaa\xf3\xd4\xd3\xe9\xd3\xf9\xde\x73\x3e\x77\ +\x5d\xf5\xbf\x77\xcf\x73\x7f\xef\x79\x4e\xff\xf7\xbe\xa7\xd5\x83\ +\xc6\xc0\x71\x38\xdf\xc1\x09\x18\x0b\xff\x5d\xcf\xe1\xcf\x3f\x78\ +\x09\x8d\x68\x01\xac\xad\xe0\x00\x54\x3d\x38\x73\x10\xaa\x7a\x17\ +\x42\x2d\xed\x85\xaa\xe0\x26\xd8\x07\x5d\xf5\x0c\xaa\x9a\x9b\xe0\ +\x05\x74\xd5\x62\x58\x02\x73\x1b\xc6\x4c\xb3\x6b\x69\x0d\xdc\x6e\ +\x98\x75\x50\x5b\x3b\xa1\x6a\x84\xfd\xb0\x0b\x6a\x6b\x3d\x54\x85\ +\xf4\xc3\x06\xa8\x2d\xbf\xaf\xdc\x7c\x13\xb6\xc0\xa7\xe4\xc9\x11\ +\x38\xdc\xe1\x7d\x04\x6b\xed\xc9\xfe\x52\xa8\xad\x19\xf0\x03\xa2\ +\x79\x0f\xa8\x47\x10\xde\x4f\x18\x05\x23\x20\xd7\x5a\xa3\xec\xc9\ +\xb5\x33\xa1\xb6\x7c\xdd\x7e\x86\x08\x38\x06\xea\x01\xe4\xd0\x89\ +\x30\xbe\xfd\x77\xf8\xd6\x28\x7b\xc2\xfb\x02\x66\xd6\xd6\x30\x78\ +\x0b\x11\x70\x16\xd4\x1d\x08\xef\x37\x4c\x82\x09\xf0\xab\xed\x89\ +\x35\xca\x9e\xf0\xde\x81\x99\x45\xf2\xbd\x1e\x01\x97\x34\xd0\x2d\ +\x08\xcf\x0d\x4c\x06\xa7\x90\x37\x60\x8d\xb2\x27\xbc\x57\x1a\xa5\ +\x7a\x0c\x11\x30\xa0\x81\xae\x41\x78\x32\x05\x9c\x82\x9b\x09\xcf\ +\x1a\x65\x4f\x78\x66\x15\xeb\x1e\x44\x40\x7c\xaa\xab\x10\x9e\x4c\ +\x05\xa7\x90\x3d\x6b\x54\x9e\xd6\x7d\x8d\x52\x5d\x87\x08\x70\x33\ +\xea\x02\x84\x27\xd3\xc0\x29\x64\xcf\x1a\x95\x3f\xc0\x0d\x8d\x52\ +\xe5\xef\xf0\xa1\x06\x3a\x07\xe1\xc9\x74\x70\x13\xd9\xb3\x46\xd9\ +\x13\xde\x65\x8d\x52\x9d\x82\x08\x78\xa2\x81\xbc\xf5\x84\x27\xfe\ +\x6f\xbb\x89\xec\x9d\x04\x65\x4f\x78\xa7\x35\x4a\xe5\xc5\x22\x02\ +\xe2\x18\x3d\x0a\xe1\xc9\x2c\xf0\xa5\x95\x3d\x6b\x94\x3d\xe1\x1d\ +\xd2\x28\xd5\x76\x88\x80\x37\x1a\x68\x3f\x84\x27\xb3\xc1\x29\x64\ +\xcf\x1a\x65\x4f\x78\x3b\x34\x4a\xb5\x11\x22\xe0\xbd\x06\xda\x0d\ +\xe1\xc9\x1c\x70\x0a\xd9\xb3\x46\xd9\x13\x9e\x59\xc5\x5a\x01\x11\ +\xe0\x21\xa4\xb6\x42\x78\xe2\x45\xc3\x29\x64\xcf\x1a\x95\x0f\x2e\ +\xb3\x8a\xe5\xa7\x8b\x37\xdc\x77\x18\x0e\x9b\xda\xeb\x60\x1e\x58\ +\x97\xbd\xcd\x60\xad\x3d\xae\xcd\xb0\xa6\x58\xe3\xe0\x2b\x44\xc8\ +\x68\xf0\x76\x9b\x1f\x36\x1f\x9c\x42\xf6\xac\xb1\x36\x36\xff\x0d\ +\xcc\x2a\xd6\x48\xf8\x00\x11\xec\x5b\x6f\x59\x5a\x8b\x57\x6d\x37\ +\x91\xbd\xe5\x60\x6d\xac\xcd\x30\xab\x27\xbd\x86\x08\xf2\x61\xfe\ +\x5e\x88\xb5\x2c\xaa\xf0\x5c\x5b\x1b\x6b\x33\x7a\xd6\x53\x88\xa0\ +\x95\xd0\xf9\xd6\xf3\x96\x9b\x1f\x26\xd6\x58\x1b\x6b\x33\x7a\xd6\ +\x5d\x88\xa0\xd5\x30\x24\xad\xa5\xea\x2c\x18\x0a\xab\xd2\xda\x8c\ +\x9e\xb5\x0d\x22\xc8\x8b\x46\xe7\x61\xe4\xd1\x3b\xd8\xe1\x5d\x84\ +\x7c\x71\x31\xa3\x2f\x5d\x81\xfc\x80\x12\xec\x6d\x44\x8e\xff\x0c\ +\x38\x81\x3a\x58\x6b\x4f\x17\xb5\x5a\x7f\x01\x87\x5c\xb4\xb0\x9d\ +\x20\xb3\xed\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x08\xa8\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x40\x00\x00\x00\x40\x08\x06\x00\x00\x00\xaa\x69\x71\xde\ +\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ +\x00\x00\x08\x5f\x49\x44\x41\x54\x78\x9c\xd5\x9b\x6d\x70\x13\xc7\ +\x1d\xc6\x9f\xdd\x3b\x49\xa7\xb3\x25\xd9\x92\xdf\x30\x08\x6c\x03\ +\x71\x5b\x92\x40\x78\xa9\x21\x04\xca\xcb\x94\x4c\x48\x78\x9b\x36\ +\xa4\x03\x99\x94\x78\x86\x42\x26\x49\x93\x29\x7c\xa0\x1f\x98\xa6\ +\x93\xe9\x40\x29\xd3\x26\x13\x20\x43\x42\x4b\x52\xd2\xa6\x49\x61\ +\x0a\x09\x89\x81\x09\x01\x4c\xca\xe0\xa1\xe0\x21\x19\x48\x80\xda\ +\x04\xdb\x60\x5b\xb6\x6c\x59\x92\x4f\x3a\xe9\x6e\xb7\x1f\x4c\x54\ +\x0c\x02\x0b\x49\x77\x36\xbf\x2f\xa7\x7b\xd1\x3e\xff\x7d\xee\x76\ +\x6f\x77\x6f\x97\xc0\x24\x22\xba\xca\x7d\x7a\x08\xed\x5a\x10\xed\ +\xf1\x1e\x74\x68\x7d\xbf\x23\x2c\x8e\x02\x31\x17\x25\x16\x27\x0a\ +\x05\x27\x8a\x2d\x4e\x14\x8b\x2e\xe4\x51\x3b\x04\x2a\x10\xa3\xe3\ +\x32\x54\x20\xc2\x62\xfc\x93\xe0\x59\xbc\xd1\x79\x18\xc7\xc2\xe7\ +\x00\xe8\x29\xff\x37\x47\xcc\xc7\x4b\x9e\xd9\xa8\x76\x3f\x82\x0a\ +\x6b\x11\x28\xa1\x86\xc4\x9a\xd5\x44\x55\x16\xe7\xa7\x94\xcb\xa8\ +\x8f\x5c\x41\x6d\xef\xd7\xf8\x3c\xf4\x35\xfc\x7a\x4f\x66\x89\x12\ +\x01\x0f\x4a\x65\x98\xef\x18\x87\x49\xf2\x68\x54\xc9\x15\x18\x69\ +\xf5\x64\x2d\xee\xac\x24\xc4\x38\xe3\x75\x4a\x23\xd6\xb6\xef\x45\ +\x83\x72\x19\x3e\xad\x07\x0c\x2c\x1b\x49\xf7\xc3\x4e\x6d\x18\x63\ +\x2d\xc2\xb3\xee\x39\xa8\x76\xcf\x40\x9e\x28\x67\x1c\x7f\x46\x09\ +\x68\x4c\xe7\x61\x16\xc5\x6f\x7c\x1f\x61\x4b\xc7\x21\xe8\x5c\xcd\ +\x34\x9e\x94\x79\x40\x2a\xc3\x5b\xde\x6a\x4c\x90\x46\xc2\x46\x44\ +\x50\x9a\x5e\x11\x49\xdb\x80\x8e\x78\x90\xef\xe8\x3a\x86\x4d\x9d\ +\x07\xd1\x1d\xef\x4a\x37\x99\x0c\xa1\x58\xe8\x9a\x84\x17\x0b\x1e\ +\xc5\x9c\x9c\xef\x41\x48\xc3\x84\xb4\x0c\xe8\xd4\x42\x7c\x45\xd3\ +\x5f\xf0\x59\xa8\x1e\x51\x1e\x4b\x27\x89\xac\x32\xdc\x5a\x88\x3f\ +\x0e\x5b\x86\xa7\xf2\xab\x8c\x35\x20\xce\x74\x5e\x1f\xb9\x82\x59\ +\xdf\xbe\x06\x25\xde\x79\xb7\x5a\x86\xb3\xb6\x70\x11\xd6\x17\x2f\ +\x80\x83\x4a\x29\x17\x89\x94\x0d\xd0\x98\xce\xb7\xfb\x8f\x60\x73\ +\xc7\x7e\x5c\x8e\xf9\xd2\x8f\xd2\x40\x2c\x44\xc4\x7c\xd7\x0f\xb1\ +\xb9\xe4\xa7\x18\x2b\x95\x64\xcf\x80\xb0\x1e\xe5\x2f\x5d\xfb\x00\ +\x1f\x76\xd7\x22\xc4\x22\x99\x45\x69\x38\x04\x15\xb6\x12\xec\xf2\ +\xae\xc6\xf4\xdc\xb1\x03\xe6\x8f\x0e\x74\x41\x44\x8f\xf1\xdf\xfb\ +\x6a\xf0\x6e\xd7\x67\xf7\x40\xe6\x01\x80\xa3\x51\x6d\xc5\xca\xe6\ +\xb7\xf1\x55\xa4\x99\x0f\x74\xf5\x80\x06\xec\xe9\x39\x85\x6d\x9d\ +\x07\xa0\xf1\xd4\x5b\x71\x43\x81\xf3\x6a\x0b\xd6\xb4\xee\x46\x58\ +\x8f\xde\xd1\x84\x3b\x3e\x22\xa7\x7a\x1b\xf9\x9c\x86\xdf\xdd\x23\ +\x77\x3e\x19\x04\xf3\x9c\x93\xf1\x69\xd9\x8b\x10\xa9\x98\x34\xaf\ +\x49\x9f\x00\xc6\x18\xaf\x57\x9a\xf8\x4f\x9a\xb6\xde\xc3\x99\x07\ +\x00\x8e\x43\xc1\x53\xd8\xe0\xfb\x04\x51\x3d\x96\xf4\x49\x48\x6a\ +\x40\x98\xa9\xf8\xad\x6f\x1f\x9a\xd4\x36\x63\xe3\x33\x89\xad\x9d\ +\x87\x50\xa7\x34\x26\x3d\x97\xd4\x80\x93\x4a\x03\x3e\x0e\xd4\x01\ +\x18\xb0\x0e\xb9\x27\x68\xd3\xba\xb1\xc1\xb7\x3f\xe9\xb9\x5b\x0c\ +\x08\x69\x11\xbe\xb6\xf5\x7d\xe8\x06\x74\x66\x06\x93\x03\xa1\x33\ +\xd8\xd7\x73\xe6\x96\x3b\x7a\x8b\x01\x6f\x77\x1f\xc7\x97\x91\xcb\ +\xe6\x44\x65\x32\xbf\xba\xfa\x1e\x5a\xe3\x81\x7e\x26\xf4\x33\xa0\ +\x51\xf5\xf1\x9d\xfe\xc3\xe6\x46\x65\x22\x97\x63\x3e\xec\xea\x3e\ +\xd9\xef\x58\xc2\x00\x8d\xe9\xfc\x6f\x81\x3a\x9c\x57\x5b\x4d\x0f\ +\xcc\x2c\x18\x18\xf6\x04\x4e\xa2\x51\xed\x48\x3c\x05\x09\x03\x54\ +\xae\xa1\x26\x78\x06\x3a\xd7\x06\x27\x3a\x93\xf8\x8f\xd2\x88\xd3\ +\xca\xff\x8b\x78\xc2\x80\xb0\x1e\xc5\x09\xa5\x61\x50\x82\x32\x13\ +\x1d\x1a\x6a\x42\x67\x13\xfb\x09\x03\x76\x05\x4e\x00\x06\xdd\x7d\ +\x1b\x11\xf1\x03\xc9\x0b\x0b\x04\x43\xd2\xbf\x5b\xf6\x05\xeb\xa1\ +\xe8\x2a\x07\xae\x1b\xc0\x18\xe3\x9b\x0c\xac\xfc\x5c\x82\x8c\xed\ +\x23\xaa\xb1\xd5\x5b\x0d\xb7\xc5\x63\x98\x4e\xaa\xf8\xb5\x00\x0e\ +\x87\xcf\x03\xb8\x6e\xc0\x39\xf5\x1a\x7c\xea\x35\xc3\x04\x09\x08\ +\x86\x5b\xf2\xb1\xd2\x3d\x0b\x97\x2a\x37\x60\x51\xde\x34\xd8\x88\ +\xd5\x30\xbd\x54\x78\x3f\x50\x07\xc6\x18\xa7\x00\xf0\xf9\x75\x37\ +\xcc\xc0\x2d\xe4\xe0\xc3\x91\xab\xb1\xa7\xec\x65\x2c\x74\x4e\x86\ +\x34\x48\x46\xd4\xf6\x5e\x40\x98\xa9\x10\x01\xe0\x62\xac\xc3\x54\ +\x71\x2b\x11\xf1\xb8\x73\x3c\xa6\xca\xa3\xb1\x2f\x78\x06\xcf\xb7\ +\xed\x41\xd4\xe4\x21\xb6\x18\x8b\xa0\x4d\xeb\x01\x65\x8c\xf1\x96\ +\x98\xdf\x54\xf1\xef\xf0\x88\xb9\xa8\x76\xcf\x44\x7b\xe5\x46\xac\ +\x2e\x78\x14\xf9\x82\xd3\x34\xed\x18\xd7\xd1\xae\xf5\x80\x2a\x3c\ +\x86\x08\x53\x4c\x13\x4e\x86\x53\xb0\xe3\xf5\xd2\x65\xf8\xb8\x7c\ +\x0d\x9e\x71\xcf\x84\x9d\xca\x86\x6b\xc6\xb9\x8e\x0e\x2d\x04\x1a\ +\x66\x51\x28\xcc\xbc\x0f\x1a\xb7\xc3\x4a\x44\x4c\xcf\x19\x83\x2d\ +\xa5\x3f\xc7\x3f\xcb\x5e\x40\xa5\x34\xca\x50\x3d\x8d\xeb\xe8\xd2\ +\x15\xd0\x6e\x5d\x41\x50\x1f\x3a\x83\x1e\x0e\x41\xc2\xe3\x8e\xf1\ +\xa8\x1f\xfb\x0a\x5e\x29\x7e\x12\x5e\x8b\x07\xd4\x80\x6f\xb8\x2a\ +\xd7\xd1\xac\x05\x41\x03\x9a\x82\x10\x8b\x66\x5d\x20\x53\xec\xd4\ +\x8a\xf5\xc5\x8b\xb0\xaf\x7c\x2d\x56\x7a\xe6\x02\xd4\x9e\x65\x05\ +\x06\x55\x0f\x83\x52\x42\x40\x8c\xfd\x4a\x9e\x36\x94\x10\x3c\x64\ +\x1f\x89\xc5\xae\xc9\xc6\xa4\x0f\x02\x51\x22\x16\xd8\x88\x68\x88\ +\x40\x26\xe8\x9c\xe1\x5c\xf4\x2a\xb6\xf9\x0f\xe3\xaf\xdd\xb5\x80\ +\x01\xf5\x94\x85\x5a\x21\x8a\x84\x42\x24\x03\x8e\x8e\x9b\x4a\x4b\ +\xbc\x0b\x7f\xee\xfa\x02\xef\x75\xd5\xe2\xbf\x31\x63\xba\xe7\x04\ +\x14\x22\xb5\x41\x74\x50\x3b\x64\x6a\x33\x44\x24\x1d\x8e\xf7\x5e\ +\xc0\xfc\x2b\xdb\xa0\xc4\xbb\x0c\x99\x63\xf0\x1d\x22\xa1\xc8\x23\ +\x12\xc4\x22\x8b\x03\x1e\xd1\x61\x98\x50\x2a\x68\x5c\xc7\x97\xd1\ +\x66\x6c\xf2\xd5\xe0\x83\xc0\x17\xa6\x68\x4a\xc4\x82\x0a\xab\x07\ +\xa2\x44\xad\xe4\xd9\xe6\x9d\x83\x36\xfc\x7b\x41\x6d\xc3\x46\x5f\ +\x0d\x8e\x84\xcf\xe2\x8a\x89\x4d\x72\x1b\x11\xe1\xb5\xe6\xf7\xf5\ +\x05\xc6\xd9\x86\x99\x26\x7c\x23\x3b\xba\x6a\xb1\xae\xf5\x1f\xf0\ +\x6b\x19\xce\x23\x4a\x03\x1b\xb5\x60\x84\xc5\xd3\x67\xc0\x34\x79\ +\xb4\x69\xc2\x51\x16\xc7\xf1\xde\x8b\x78\xad\xf3\x20\x3e\x0d\x9e\ +\x36\x4d\xf7\x66\xbc\xd2\x48\xe4\x51\x7b\x9f\x01\x13\xed\xa3\x60\ +\x17\xf2\x10\xd1\x03\x86\x8a\x5e\x52\xdb\xf1\xcb\xb6\xdd\x38\x19\ +\xfa\x0a\x01\x3d\x64\xa8\xd6\x40\x54\xe7\x55\x81\x52\xda\xf7\xfe\ +\xb3\x0b\x56\xb2\xcc\x35\xc9\x30\x31\x8d\x33\xfc\xa1\xe3\x20\xc6\ +\x5f\xfc\x35\x0e\x04\x4e\x0c\x7a\xe6\x01\x8a\xe5\xae\xaa\xeb\xbf\ +\xae\xf3\x72\xe1\x3c\x58\x0d\x1a\x9c\xf0\xeb\x21\xbc\xd9\x59\x83\ +\xc8\x10\xe8\x74\x01\xc0\x12\xd7\x14\xe4\x88\x12\x01\x6e\x30\xa0\ +\xc2\x5a\x84\x29\x72\xc5\xe0\x45\x65\x16\x44\xc4\xd3\xf9\xd3\x13\ +\xbb\x09\x03\xec\xc4\x82\xc5\x79\x53\x41\x87\x60\xb3\x38\x9b\x3c\ +\x22\x8f\xc1\x64\x7b\x59\x62\x3f\x61\x00\xa5\x94\x2c\x75\x4e\xc4\ +\xfd\xf6\xf2\xc1\x88\xcb\x14\x44\x08\x58\x9a\x3f\x1d\x23\x2c\xee\ +\xc4\xb1\x7e\x9d\x00\xaf\xc5\x8d\xe7\x3c\xb3\x4d\x0f\xcc\x2c\x64\ +\x6b\x01\x9e\x76\x55\xf5\x9b\x50\xd9\xcf\x00\x4a\x29\xf9\x45\xfe\ +\x0c\xdc\x27\x99\xd7\x2e\x30\x0b\x0a\x8a\xbf\x0f\x5f\x01\xb7\x25\ +\x97\xf4\x3f\x7e\x13\x02\x15\xc8\x1b\xa5\x4b\x21\x53\xc9\xbc\xe8\ +\x4c\x60\xae\x73\x02\x9e\x70\x8d\xbf\x65\xe0\x23\x69\x3f\x78\x46\ +\xce\x7d\x58\x92\x37\x15\x06\x2f\x27\x30\x0d\xaf\xa5\x00\xaf\x16\ +\x2f\x4e\x7a\x2e\x69\x95\x2f\x0b\x36\xf2\xad\xda\xc1\x1b\xa2\x57\ +\x71\x52\xb9\x64\x68\x70\xc6\x23\x60\x4d\xd1\x02\x4c\xb1\x27\x7f\ +\xc5\xdf\x76\x24\xa4\xcc\x56\x48\xb6\x8c\x58\x81\x32\x6b\x91\x61\ +\xa1\x19\x0f\xc1\xaa\xc2\xc7\xb0\xda\xfd\xa3\xdb\xce\x24\xbf\xe3\ +\x50\xd0\x64\xb9\x9c\xec\x18\xb1\x12\x0e\x13\x3f\x58\x64\x93\x95\ +\xee\xd9\x78\xbd\xe4\x49\x48\x82\xf5\xb6\x65\x79\xc0\xb1\xb0\x99\ +\xb9\x95\x58\x57\xbc\x10\xe2\x3d\xd6\x40\x9a\x2a\x8f\xc5\xba\xa2\ +\x05\x90\x04\xcb\x1d\x2b\xb2\x01\x73\x65\xa5\x22\xe9\xd1\x14\x2e\ +\x72\x8e\xf5\xed\xff\x42\x6c\x90\xbf\x22\xa5\xc2\xec\xdc\xfb\xb1\ +\x71\xd8\x53\x18\x2d\x15\x0f\x58\x8b\xa7\x74\x5b\x5d\xa2\x4c\x74\ +\xa6\xf3\x87\xec\xa3\x50\xdd\xfc\x16\x5a\x86\xe0\x5a\x81\x3e\x08\ +\x56\x15\xfc\x18\x9b\x4a\x96\xc2\x95\xe2\x7a\xa2\xbb\x7a\xcf\x31\ +\xce\xf8\xbf\x7b\x2f\xe1\x85\x96\x77\x70\x36\xda\x84\xa1\x34\x91\ +\x32\x97\xca\xf8\x99\x67\x0e\xfe\x54\xb2\x04\x0e\xc1\x9e\x72\xbe\ +\xd2\x7a\xd1\x37\xa8\x3e\xfe\x6a\xfb\x47\x78\xa7\xeb\x18\xee\x66\ +\x2d\xa0\x51\x94\x59\x8b\xb0\x69\xd8\x72\x2c\x72\x4d\x80\x8d\xde\ +\xb9\xcc\xdf\x4c\x5a\x35\xdb\x68\x5b\x11\x09\xeb\x51\x5e\x25\x97\ +\xe3\xb9\x96\x77\x01\x0c\xde\xcc\xb2\xa9\x8e\x07\xb1\xb3\xf4\x19\ +\x54\xda\x4a\xd2\x5a\x39\x96\x71\x53\xcf\xaf\x85\xf9\x66\x5f\x0d\ +\x76\xf7\xd4\xe1\x92\x81\xd3\x6c\x6e\x44\xa2\x32\x1e\xce\xa9\xc4\ +\x9a\xc2\x79\x78\xcc\xf1\x40\x46\xab\x4a\xb3\xd2\xd6\x8d\x33\x8d\ +\x5f\x54\xdb\xb1\x37\x78\x0a\xdb\xfd\x47\xd0\x64\xd4\xf0\x36\xb1\ +\xe0\x09\xe7\x04\xac\xf2\xcc\xc5\xc3\x72\x05\xf2\x05\x39\xe3\x25\ +\xb5\x59\x6f\xec\xab\x2c\xce\x8f\xf6\x7e\x83\x37\x3b\x8f\x62\x6f\ +\xf0\x34\x90\x85\x65\x75\x5e\x6b\x29\x9e\xf7\xcc\xc2\xf2\xfc\x69\ +\xf0\x66\x71\xd9\x2c\x60\x60\x6f\x87\x71\xc6\xfd\x5a\x18\x47\xc3\ +\xdf\x60\x7f\xef\x05\xf4\xc6\xfd\xe8\x61\x11\x04\x74\x05\x3d\x7a\ +\xdf\xb6\x43\x57\xc0\xb9\x8e\x5c\x41\x82\x9b\xca\x70\x09\x76\xe4\ +\x09\x7d\x5b\x97\x90\x8b\x89\xf6\x0a\xcc\x75\x7c\x1f\xe3\x6c\xa5\ +\xb0\xdc\x66\xc5\x47\xa6\x98\xd2\xdd\x63\x8c\xf1\x08\x8f\x21\xcc\ +\x54\x84\xf4\x28\x42\x2c\x8a\xa0\x1e\x45\x37\x8b\x82\x43\x87\x4c\ +\x6d\x70\x51\x09\x0e\x2a\xc1\x21\xf4\x6d\x73\xa9\xcd\xb0\x4c\xdf\ +\xc8\xff\x00\xbb\x7c\x01\xa7\x6a\x3a\xee\x8e\x00\x00\x00\x00\x49\ +\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x01\xae\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x04\x67\x41\x4d\x41\x00\x00\xb1\x8f\x0b\xfc\x61\x05\x00\x00\x00\ +\x09\x70\x48\x59\x73\x00\x00\x0e\xc3\x00\x00\x0e\xc3\x01\xc7\x6f\ +\xa8\x64\x00\x00\x01\x43\x49\x44\x41\x54\x58\x47\x63\x60\xa0\x0f\ +\x50\x01\x5a\xb3\x08\x88\x7f\x02\xf1\x1b\x20\x9e\x0b\xc4\x20\x31\ +\xba\x00\x7b\xa0\x2d\x2f\x80\xf8\x3f\x1a\x7e\x0e\xe4\x2b\xd1\xda\ +\x05\x20\xcb\x5f\x61\xb1\x1c\xe6\x98\x25\xb4\x74\x80\x17\x34\xb8\ +\xd1\x7d\x8e\xcc\xff\x4e\x4b\x07\xb4\xe3\xf1\x39\xcc\x11\x3f\x68\ +\xe9\x00\x26\xa0\xe1\x3d\x04\x1c\x31\x9f\xda\x0e\xf0\x00\x1a\xb8\ +\x0d\x88\x15\x91\x0c\xc6\x15\x12\x2f\x81\x6a\x14\xa8\xe9\x00\x07\ +\xa0\x61\xaf\xa1\x3e\xbe\x8c\x96\xc2\x1b\xd1\x42\x02\x94\x30\x6d\ +\x69\x65\x39\x2c\x7e\xaf\x01\x2d\x40\xce\xeb\x35\x50\x47\xbc\xa5\ +\x87\xe5\x30\x47\xdc\x04\x5a\xa6\x81\xe4\xd3\x02\x20\xdb\x95\xd6\ +\x3e\x47\xcf\x76\x77\x81\x16\x1a\x51\xd3\x52\x98\x59\xa0\x42\x06\ +\x16\xe7\xf8\xf2\x3a\x48\xee\x0c\x10\xb3\x51\xd3\x11\x84\x4a\x38\ +\x64\x07\x81\x12\x1c\x28\xd8\x19\xa9\xe5\x00\x52\x2d\xa7\x6a\x6a\ +\x1f\xb5\x9c\x50\x62\x03\xc9\x53\xbd\x90\x19\x0d\xf6\xd1\x60\xc7\ +\x17\x02\xa3\x09\x8e\x5a\xa5\x2b\x03\x17\xd0\x24\x50\xad\x35\x20\ +\x09\x0e\xe4\x0b\x50\x79\x3d\x60\x96\x83\x1c\x00\x6b\xad\xd0\x35\ +\xc1\x21\xc7\xdf\x2e\x02\x21\x40\xf5\xd4\x8e\x6c\xb9\x20\x90\xf3\ +\x0e\x8f\x03\x68\x6a\x39\xc8\x21\x4e\x38\x2c\xff\x0b\x14\x07\x35\ +\x2c\x6d\xa8\x96\xd4\x71\x18\x84\xdc\x6e\x7f\x0f\x54\x03\x8a\x8e\ +\x5a\x20\xb6\x03\x62\x0e\x5a\x5b\x0e\x32\x7f\x37\x10\x77\x00\xb1\ +\x27\x10\x8b\xd0\xc3\x42\x64\x3b\x00\x3b\x73\x24\x98\x91\x7a\xa9\ +\xd4\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +" + +qt_resource_name = b"\ +\x00\x0a\ +\x06\xd2\x80\x84\ +\x00\x62\ +\x00\x6f\x00\x74\x00\x5f\x00\x77\x00\x69\x00\x64\x00\x67\x00\x65\x00\x74\ +\x00\x03\ +\x00\x00\x70\x37\ +\x00\x69\ +\x00\x6d\x00\x67\ +\x00\x09\ +\x0c\x98\xba\x47\ +\x00\x70\ +\x00\x61\x00\x75\x00\x73\x00\x65\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x07\ +\x09\x01\x57\x87\ +\x00\x62\ +\x00\x69\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x08\ +\x02\x8c\x59\xa7\ +\x00\x70\ +\x00\x6c\x00\x61\x00\x79\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x07\ +\x06\xc1\x57\xa7\ +\x00\x70\ +\x00\x65\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ +" + +qt_resource_struct_v1 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ +\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x04\x00\x00\x00\x03\ +\x00\x00\x00\x52\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x66\ +\x00\x00\x00\x68\x00\x00\x00\x00\x00\x01\x00\x00\x13\x12\ +\x00\x00\x00\x3e\x00\x00\x00\x00\x00\x01\x00\x00\x07\x62\ +\x00\x00\x00\x26\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +" + +qt_resource_struct_v2 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x04\x00\x00\x00\x03\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x52\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x66\ +\x00\x00\x01\x60\xbf\xbf\x27\x4b\ +\x00\x00\x00\x68\x00\x00\x00\x00\x00\x01\x00\x00\x13\x12\ +\x00\x00\x01\x61\x55\xcc\x5d\xc2\ +\x00\x00\x00\x3e\x00\x00\x00\x00\x00\x01\x00\x00\x07\x62\ +\x00\x00\x01\x61\x55\xc7\x8f\x53\ +\x00\x00\x00\x26\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x01\x60\xbf\xc0\xc1\x40\ +" + +qt_version = QtCore.qVersion().split('.') +if qt_version < ['5', '8', '0']: + rcc_version = 1 + qt_resource_struct = qt_resource_struct_v1 +else: + rcc_version = 2 + qt_resource_struct = qt_resource_struct_v2 + +def qInitResources(): + QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources() diff --git a/dexbot/img/bin.png b/dexbot/resources/img/bin.png similarity index 100% rename from dexbot/img/bin.png rename to dexbot/resources/img/bin.png diff --git a/dexbot/img/pause.png b/dexbot/resources/img/pause.png similarity index 100% rename from dexbot/img/pause.png rename to dexbot/resources/img/pause.png diff --git a/dexbot/img/pen.png b/dexbot/resources/img/pen.png similarity index 100% rename from dexbot/img/pen.png rename to dexbot/resources/img/pen.png diff --git a/dexbot/img/play.png b/dexbot/resources/img/play.png similarity index 100% rename from dexbot/img/play.png rename to dexbot/resources/img/play.png diff --git a/dexbot/views/gen/bot_item_widget.py b/dexbot/views/gen/bot_item_widget.py index 939447070..f96fe4e51 100644 --- a/dexbot/views/gen/bot_item_widget.py +++ b/dexbot/views/gen/bot_item_widget.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'dexbot/views/orig/bot_item_widget.ui' +# Form implementation generated from reading ui file 'bot_item_widget.ui' # -# Created by: PyQt5 UI code generator 5.10 +# Created by: PyQt5 UI code generator 5.9.2 # # WARNING! All changes made in this file will be lost! @@ -79,7 +79,7 @@ def setupUi(self, widget): self.edit_button.setStyleSheet("border: 0;") self.edit_button.setText("") icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap("dexbot/img/pen.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + icon.addPixmap(QtGui.QPixmap(":/bot_widget/img/pen.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.edit_button.setIcon(icon) self.edit_button.setObjectName("edit_button") self.horizontalLayout_4.addWidget(self.edit_button) @@ -89,7 +89,7 @@ def setupUi(self, widget): self.remove_button.setStyleSheet("border: 0;") self.remove_button.setText("") icon1 = QtGui.QIcon() - icon1.addPixmap(QtGui.QPixmap("dexbot/img/bin.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + icon1.addPixmap(QtGui.QPixmap(":/bot_widget/img/bin.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.remove_button.setIcon(icon1) self.remove_button.setIconSize(QtCore.QSize(20, 20)) self.remove_button.setObjectName("remove_button") @@ -229,7 +229,7 @@ def setupUi(self, widget): "") self.pause_button.setText("") icon2 = QtGui.QIcon() - icon2.addPixmap(QtGui.QPixmap("dexbot/img/pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + icon2.addPixmap(QtGui.QPixmap(":/bot_widget/img/pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.pause_button.setIcon(icon2) self.pause_button.setIconSize(QtCore.QSize(30, 30)) self.pause_button.setObjectName("pause_button") @@ -245,7 +245,7 @@ def setupUi(self, widget): self.play_button.setStyleSheet("border: 0;") self.play_button.setText("") icon3 = QtGui.QIcon() - icon3.addPixmap(QtGui.QPixmap("dexbot/img/play.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + icon3.addPixmap(QtGui.QPixmap(":/bot_widget/img/play.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.play_button.setIcon(icon3) self.play_button.setIconSize(QtCore.QSize(30, 30)) self.play_button.setObjectName("play_button") @@ -269,3 +269,4 @@ def retranslateUi(self, widget): self.buy_label.setText(_translate("widget", "Buy")) self.sell_label.setText(_translate("widget", "Sell")) +from dexbot.resources import icons_rc diff --git a/dexbot/views/orig/bot_item_widget.ui b/dexbot/views/orig/bot_item_widget.ui index a7c6b56df..4afc52378 100644 --- a/dexbot/views/orig/bot_item_widget.ui +++ b/dexbot/views/orig/bot_item_widget.ui @@ -164,8 +164,8 @@ - - dexbot/img/pen.pngdexbot/img/pen.png + + :/bot_widget/img/pen.png:/bot_widget/img/pen.png @@ -187,8 +187,8 @@ - - dexbot/img/bin.pngdexbot/img/bin.png + + :/bot_widget/img/bin.png:/bot_widget/img/bin.png @@ -487,8 +487,8 @@ border-right: 2px solid #005B78; - - dexbot/img/pause.pngdexbot/img/pause.png + + :/bot_widget/img/pause.png:/bot_widget/img/pause.png @@ -519,8 +519,8 @@ border-right: 2px solid #005B78; - - dexbot/img/play.pngdexbot/img/play.png + + :/bot_widget/img/play.png:/bot_widget/img/play.png @@ -553,6 +553,8 @@ border-right: 2px solid #005B78; - + + + From 3c45c89d98947c86ca3d6ad42e62dc019acae4f2 Mon Sep 17 00:00:00 2001 From: Nikolay Date: Fri, 2 Mar 2018 12:30:47 +0200 Subject: [PATCH 0088/1846] Add canceling orders on bot pause --- dexbot/controllers/main_controller.py | 9 +++++++++ dexbot/views/bot_item.py | 1 + 2 files changed, 10 insertions(+) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index f2dc065e6..b453603d4 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -2,6 +2,8 @@ from ruamel.yaml import YAML from bitshares.instance import set_shared_bitshares_instance +from dexbot.basestrategy import BaseStrategy + class MainController: @@ -86,3 +88,10 @@ def remove_bot_config(bot_name): with open("config.yml", "w") as f: yaml.dump(config, f) + + def pause_bot(self, bot_name): + config = self.get_bot_config(bot_name) + strategy = BaseStrategy(config, bot_name) + strategy.purge() + + diff --git a/dexbot/views/bot_item.py b/dexbot/views/bot_item.py index 95144f6f2..d89a167e7 100644 --- a/dexbot/views/bot_item.py +++ b/dexbot/views/bot_item.py @@ -49,6 +49,7 @@ def start_bot(self): def pause_bot(self): self.running = False + self.main_ctrl.pause_bot(self.botname) self.pause_button.hide() self.play_button.show() From c90f1c2b2412954588bf922562df97899128a3b0 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 5 Mar 2018 09:25:38 +0200 Subject: [PATCH 0089/1846] Remove redundant imports --- dexbot/cli.py | 1 - dexbot/ui.py | 7 ++----- setup.py | 4 ---- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index 03be8f35b..117e07361 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -import yaml import logging import click import sys diff --git a/dexbot/ui.py b/dexbot/ui.py index d3e5c3682..3a0b665e9 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -1,10 +1,7 @@ import os import click import logging -import yaml -from datetime import datetime -from bitshares.price import Price -from prettytable import PrettyTable +from ruamel import yaml from functools import update_wrapper from bitshares import BitShares from bitshares.instance import set_shared_bitshares_instance @@ -96,7 +93,7 @@ def new_func(ctx, *args, **kwargs): def configfile(f): @click.pass_context def new_func(ctx, *args, **kwargs): - ctx.config = yaml.load(open(ctx.obj["configfile"])) + ctx.config = yaml.safe_load(open(ctx.obj["configfile"])) return ctx.invoke(f, *args, **kwargs) return update_wrapper(new_func, f) diff --git a/setup.py b/setup.py index 6b81ec27b..8e3082346 100755 --- a/setup.py +++ b/setup.py @@ -34,12 +34,8 @@ install_requires=[ "bitshares==0.1.11.beta", "uptick>=0.1.4", - "prettytable", "click", "click-datetime", - "colorama", - "tqdm", - "pyyaml", "sqlalchemy", "appdirs", "pyqt5", From 360b2bd53d4aac92b1d4bc644c5c9e8938bbcd23 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 5 Mar 2018 12:42:09 +0200 Subject: [PATCH 0090/1846] Change ui file structure - Removed generated .py file from repo. - Added pyqt-distutils module for easy file generation. --- .gitignore | 2 + dexbot/resources/icons_rc.py | 422 ------------------ dexbot/views/bot_item.py | 10 +- dexbot/views/bot_list.py | 12 +- dexbot/views/confirmation.py | 4 +- dexbot/views/create_bot.py | 6 +- dexbot/views/create_wallet.py | 4 +- dexbot/views/edit_bot.py | 7 +- dexbot/views/gen/__init__.py | 0 dexbot/views/gen/bot_item_widget.py | 272 ----------- dexbot/views/gen/bot_list_window.py | 95 ---- dexbot/views/gen/confirmation_window.py | 49 -- dexbot/views/gen/create_bot_window.py | 256 ----------- dexbot/views/gen/create_wallet_window.py | 101 ----- dexbot/views/gen/edit_bot_window.py | 221 --------- dexbot/views/gen/notice_window.py | 45 -- dexbot/views/gen/unlock_wallet_window.py | 85 ---- dexbot/views/notice.py | 4 +- dexbot/views/ui/__init__.py | 3 + dexbot/views/{orig => ui}/bot_item_widget.ui | 0 dexbot/views/{orig => ui}/bot_list_window.ui | 0 .../views/{orig => ui}/confirmation_window.ui | 0 .../views/{orig => ui}/create_bot_window.ui | 0 .../{orig => ui}/create_wallet_window.ui | 0 dexbot/views/{orig => ui}/edit_bot_window.ui | 0 dexbot/views/{orig => ui}/notice_window.ui | 0 .../{orig => ui}/unlock_wallet_window.ui | 0 dexbot/views/unlock_wallet.py | 6 +- pyuic.json | 11 + setup.py | 20 +- 30 files changed, 62 insertions(+), 1573 deletions(-) delete mode 100644 dexbot/resources/icons_rc.py delete mode 100644 dexbot/views/gen/__init__.py delete mode 100644 dexbot/views/gen/bot_item_widget.py delete mode 100644 dexbot/views/gen/bot_list_window.py delete mode 100644 dexbot/views/gen/confirmation_window.py delete mode 100644 dexbot/views/gen/create_bot_window.py delete mode 100644 dexbot/views/gen/create_wallet_window.py delete mode 100644 dexbot/views/gen/edit_bot_window.py delete mode 100644 dexbot/views/gen/notice_window.py delete mode 100644 dexbot/views/gen/unlock_wallet_window.py create mode 100644 dexbot/views/ui/__init__.py rename dexbot/views/{orig => ui}/bot_item_widget.ui (100%) rename dexbot/views/{orig => ui}/bot_list_window.ui (100%) rename dexbot/views/{orig => ui}/confirmation_window.ui (100%) rename dexbot/views/{orig => ui}/create_bot_window.ui (100%) rename dexbot/views/{orig => ui}/create_wallet_window.ui (100%) rename dexbot/views/{orig => ui}/edit_bot_window.ui (100%) rename dexbot/views/{orig => ui}/notice_window.ui (100%) rename dexbot/views/{orig => ui}/unlock_wallet_window.ui (100%) create mode 100644 pyuic.json diff --git a/.gitignore b/.gitignore index e68fbd659..eaaebbad1 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,5 @@ deprecated *.yml venv/ .idea/ +dexbot/views/ui/*_ui.py +dexbot/resources/*_rc.py diff --git a/dexbot/resources/icons_rc.py b/dexbot/resources/icons_rc.py deleted file mode 100644 index c4b5596ad..000000000 --- a/dexbot/resources/icons_rc.py +++ /dev/null @@ -1,422 +0,0 @@ -# -*- coding: utf-8 -*- - -# Resource object code -# -# Created by: The Resource Compiler for PyQt5 (Qt v5.9.3) -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore - -qt_resource_data = b"\ -\x00\x00\x07\x5e\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x40\x00\x00\x00\x40\x08\x06\x00\x00\x00\xaa\x69\x71\xde\ -\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ -\x00\x00\x07\x15\x49\x44\x41\x54\x78\x9c\xe5\x9b\x5b\x4f\x13\x4d\ -\x1c\xc6\x9f\xdd\xed\x89\xc5\xed\x81\x02\xa5\x11\x14\x25\x0d\xb5\ -\xa2\x80\x04\x8d\x4d\x54\x0c\x12\xbc\x21\xfa\x0d\x4c\xbc\xf0\x2b\ -\x79\xe9\xb5\xf1\xa2\x88\x87\x60\x02\x09\x21\x31\x72\xa1\x44\x82\ -\xa1\x45\xa5\x6d\x4c\x4c\xcb\x99\x6e\x4f\xb4\xbb\x3b\xf3\x5e\xf4\ -\x6d\x43\x29\xd8\x02\xbb\x5b\x94\x5f\xd2\xc0\x6e\x67\x67\x9e\x7d\ -\x3a\x33\x3b\x3b\xf3\x1f\x06\x3a\x41\x08\xa1\x94\x52\xec\xff\x14\ -\x61\x18\x06\x2c\xcb\x82\x61\x18\x30\x0c\x03\x00\x60\x59\x96\xd1\ -\x5a\x97\xa6\x05\x28\x8a\x42\xb7\xb7\xb7\x11\x8b\xc5\xb0\xb5\xb5\ -\x85\x74\x3a\x8d\x6c\x36\x8b\x4c\x26\x53\xfa\xab\x28\x0a\x2c\x16\ -\x0b\x78\x9e\x47\x43\x43\x03\x78\x9e\x07\xcf\xf3\x10\x04\x01\x2e\ -\x97\x0b\xad\xad\xad\xb0\x58\x2c\x9a\x99\xa1\x49\xa6\x99\x4c\x86\ -\x7e\xff\xfe\x1d\x4b\x4b\x4b\x88\xc7\xe3\x10\x45\x11\xd9\x6c\xf6\ -\x48\x79\x70\x1c\x07\x41\x10\xe0\x70\x38\x70\xf9\xf2\x65\xf4\xf4\ -\xf4\xc0\xe5\x72\x81\xe3\x38\x55\x35\xab\x92\x19\x21\x84\xca\xb2\ -\x8c\xad\xad\x2d\xcc\xcc\xcc\xe0\xdb\xb7\x6f\x90\x24\x49\x8d\xac\ -\xcb\x68\x6d\x6d\xc5\xdd\xbb\x77\xe1\xf3\xf9\x60\xb1\x58\x54\x31\ -\xe3\xc4\x19\x10\x42\x68\x38\x1c\xc6\xfc\xfc\x3c\x16\x17\x17\x91\ -\xcf\xe7\x4f\x9a\x65\x55\x3a\x3a\x3a\xd0\xd7\xd7\x87\xde\xde\x5e\ -\x08\x82\x70\xa2\x7b\x38\xd1\xc5\x9b\x9b\x9b\x74\x72\x72\x12\xe1\ -\x70\x18\xe9\x74\xba\xac\x53\xd3\x1a\xa3\xd1\x08\xa7\xd3\x89\x7b\ -\xf7\xee\xe1\xda\xb5\x6b\x30\x1a\x8d\xc7\xba\x97\x63\x5d\x24\x49\ -\x12\x5d\x5e\x5e\xc6\xc4\xc4\x04\x12\x89\xc4\x71\xb2\x50\x0d\x86\ -\x61\xd0\xdf\xdf\x8f\xd1\xd1\x51\x58\xad\xd6\x23\x77\x96\x47\x36\ -\x40\x14\x45\xfa\xf1\xe3\x47\xcc\xcd\xcd\x61\x77\x77\xf7\xa8\x97\ -\x6b\xc6\xa5\x4b\x97\x30\x34\x34\x04\x8f\xc7\x73\xa4\xbe\xe1\x48\ -\x06\xac\xaf\xaf\xd3\xf1\xf1\x71\xac\xac\xac\xe8\x5a\xdd\x6b\x85\ -\xe7\x79\x8c\x8c\x8c\xe0\xd6\xad\x5b\x35\x9b\x50\x53\xa2\x7c\x3e\ -\x4f\x57\x56\x56\x10\x08\x04\xea\x5e\xe5\xab\xc1\x30\x0c\x6e\xde\ -\xbc\x89\xa1\xa1\x21\xd8\xed\xf6\xaa\x4d\xa2\xaa\x01\x8a\xa2\xd0\ -\x85\x85\x05\x4c\x4c\x4c\x1c\xf9\x59\x5e\x4f\x7c\x3e\x1f\x1e\x3f\ -\x7e\x0c\x9b\xcd\xf6\xc7\x7b\x34\x54\xcb\x68\x6d\x6d\x0d\x93\x93\ -\x93\x7f\xd5\xcd\x03\x40\x30\x18\x44\x6b\x6b\x6b\xd5\x74\xec\x9f\ -\xbe\x14\x45\x91\xbe\x7a\xf5\xea\xd4\x57\xfb\x83\xa0\x94\x62\x76\ -\x76\x16\x9f\x3f\x7f\xa6\x8a\xa2\x1c\xda\x61\x1d\x5a\x03\x76\x76\ -\x76\x68\x20\x10\xc0\xef\xdf\xbf\xb5\x51\xa8\x03\x84\x10\x8c\x8f\ -\x8f\xc3\x64\x32\x81\x10\x42\x0f\xea\x0f\x0e\xac\x01\x92\x24\xd1\ -\x99\x99\x19\x2c\x2d\x2d\x69\xaf\x52\x63\x24\x49\xc2\x87\x0f\x1f\ -\xb0\xb1\xb1\x71\xe0\xf7\x07\x1a\x10\x8f\xc7\x31\x3f\x3f\xaf\xa9\ -\x30\x3d\xd9\xd8\xd8\xc0\xec\xec\x2c\x08\x21\x15\x4d\xa1\xc2\x00\ -\x45\x51\xe8\xfb\xf7\xef\x91\xcb\xe5\xf4\x51\xa7\x13\x5f\xbf\x7e\ -\x45\x24\x12\xa9\x38\x5f\x61\x40\x28\x14\x42\x38\x1c\xd6\x45\x94\ -\x9e\x48\x92\x84\x37\x6f\xde\x60\x7f\x87\x58\x66\x40\x32\x99\xa4\ -\xb3\xb3\xb3\xfa\x2a\xd3\x91\xb5\xb5\x35\x2c\x2e\x2e\x96\x35\x85\ -\x92\x01\x84\x10\x1a\x0c\x06\xff\xea\x5e\xbf\x1a\x8a\xa2\x60\x7e\ -\x7e\x1e\xa9\x54\xaa\x74\xae\x64\x80\x2c\xcb\x58\x58\x58\xd0\x64\ -\x22\xe3\x34\xf1\xe3\xc7\x8f\xb2\x1f\xb9\x64\x40\x3e\x9f\x47\x2c\ -\x16\xab\x8b\x28\x3d\x21\x84\x94\x75\x86\x25\x03\x22\x91\x08\xd2\ -\xe9\x74\x5d\x44\xe9\x4d\x28\x14\x2a\xfd\x6f\x00\x0a\xed\xff\xc5\ -\x8b\x17\x9a\x15\xe8\xf7\xfb\x71\xe1\xc2\x85\xb2\x73\xef\xde\xbd\ -\x43\x32\x99\x2c\x1d\x5b\x2c\x16\xf8\xfd\xfe\xb2\xf1\xbb\x2c\xcb\ -\x98\x9e\x9e\xc6\xf6\xf6\xb6\xaa\x7a\x56\x57\x57\x11\x8b\xc5\xa8\ -\xdb\xed\x66\x0c\x00\x20\x8a\x22\x56\x56\x56\x54\x2d\x64\x2f\x9d\ -\x9d\x9d\xb8\x7e\xfd\x7a\xd9\xb9\xa9\xa9\xa9\x32\x03\x8c\x46\x23\ -\xba\xba\xba\xd0\xd5\xd5\x55\x3a\x97\xcf\xe7\xf1\xe9\xd3\x27\xd5\ -\x0d\x00\x80\x2f\x5f\xbe\x00\xf8\xbf\x09\xfc\xfc\xf9\x13\x8a\xa2\ -\xa8\x5e\xc8\x69\x26\x18\x0c\x22\x97\xcb\x51\x16\xc0\x99\xe8\xfc\ -\xf6\x93\xcd\x66\x91\x48\x24\xc0\x12\x42\xe8\xde\xe7\xe2\x59\x81\ -\x10\x82\x6c\x36\x0b\x56\x96\xe5\xbf\x6e\xb2\x43\x0d\x08\x21\xc8\ -\x64\x32\x60\x73\xb9\x9c\x2e\x8b\x19\xa7\x0d\x45\x51\x0a\x35\x20\ -\x97\xcb\xfd\x73\x6f\x7e\xb5\x50\x6a\x02\xb9\x5c\xee\x9f\x1f\xfe\ -\x1e\x44\xa9\x09\xc8\xb2\x7c\xe6\x1e\x81\x45\x24\x49\x02\x6b\x36\ -\x9b\x61\x34\x1a\xeb\xad\x45\x77\x18\x86\x29\xc4\x1d\x98\x4c\xa6\ -\x33\x69\x00\xcb\xb2\x68\x68\x68\x28\xd4\x00\x93\xc9\x54\x6f\x3d\ -\xba\xc3\xb2\x2c\x1a\x1b\x1b\x0b\x06\x98\xcd\xe6\x7a\xeb\xd1\x1d\ -\x96\x65\x0b\x4d\xc0\x60\x30\x30\x0d\x0d\x0d\xf5\xd6\xa3\x3b\x1c\ -\xc7\x15\x9a\x00\x00\x38\x1c\x8e\x7a\xeb\xd1\x9d\x52\x13\x00\x0a\ -\x6b\xeb\x67\x8d\x96\x96\x96\x42\x40\x05\x50\x30\xc0\x62\xb1\xd4\ -\x5b\x93\xae\xf4\xf7\xf7\xc3\x60\x30\x30\x2c\x00\x18\x0c\x06\x66\ -\x60\x60\x40\xb3\xc2\x76\x77\x77\x91\x4a\xa5\xca\x3e\x84\x90\xb2\ -\x34\x94\x52\x64\xb3\xd9\xb2\x34\xe9\x74\xba\x22\x9d\x1a\x98\x4c\ -\x26\x5c\xbd\x7a\x15\xc0\x9e\xf8\x80\x68\x34\x4a\x9f\x3f\x7f\xae\ -\x7a\x61\x00\xd0\xd4\xd4\x84\xfd\x1d\xed\xea\xea\x2a\x64\x59\x2e\ -\x1d\xb3\x2c\x8b\xa6\xa6\xa6\xb2\x27\x12\xa5\x14\xeb\xeb\xeb\xaa\ -\x0f\xd5\xbb\xbb\xbb\xf1\xf4\xe9\x53\x06\xd8\xb3\x3a\xdc\xd6\xd6\ -\x06\xa7\xd3\x89\xcd\xcd\x4d\x55\x0b\x03\x80\xad\xad\xad\xaa\x69\ -\x08\x21\x87\x2e\x60\xaa\x4d\xf1\xd7\x07\xf6\xcc\x0a\x1b\x0c\x06\ -\x78\x3c\x9e\x52\x9c\xee\xbf\x8a\xdd\x6e\x47\x47\x47\x47\xe9\xb8\ -\x64\x00\xcb\xb2\xe8\xed\xed\x85\xcd\x66\xab\x8b\x30\xbd\xb8\x72\ -\xe5\x0a\x9a\x9b\x9b\x4b\xc7\x7b\x0d\x60\x2e\x5e\xbc\x88\xee\xee\ -\xee\xba\x08\xd3\x03\x8b\xc5\x82\xc1\xc1\x41\x98\x4c\xa6\x52\x35\ -\x2f\x5b\x1c\xe5\x38\x8e\xb9\x73\xe7\x0e\x78\x9e\xd7\x5f\x9d\x0e\ -\xdc\xb8\x71\x03\x6e\xb7\xbb\xec\x5c\xc5\xf2\x78\x31\xfc\xf4\x5f\ -\xc3\x66\xb3\xe1\xe1\xc3\x87\x15\x61\x73\x15\x06\xb0\x2c\xcb\x0c\ -\x0c\x0c\xa0\xad\xad\x4d\x3f\x75\x1a\xc3\x71\x1c\xee\xdf\xbf\x0f\ -\xb3\xd9\x5c\x5b\x8c\x10\xcf\xf3\x78\xf0\xe0\x01\x1a\x1b\x1b\xb5\ -\x57\xa7\x03\x3d\x3d\x3d\x15\x2b\x53\x45\x0e\x8c\x12\xe3\x38\x8e\ -\x51\x14\x85\x26\x93\x49\xbc\x7e\xfd\x5a\x53\x71\x5a\x63\xb5\x5a\ -\x31\x36\x36\x56\x31\x10\x2b\x72\x68\x9c\x20\xc7\x71\x8c\xdf\xef\ -\x67\xfe\x8f\xbb\xd5\x4c\xa0\x96\x38\x1c\x0e\x3c\x7b\xf6\x0c\x82\ -\x20\x30\x87\x85\xcc\xfe\x31\x50\x12\x00\x46\x46\x46\xe0\xf3\xf9\ -\xd4\x57\xa7\x31\x82\x20\x60\x6c\x6c\x0c\x2d\x2d\x2d\x7f\x1c\xd9\ -\x55\x35\xa0\xb1\xb1\x11\xa3\xa3\xa3\x10\x04\x41\x3d\x75\x3a\x70\ -\xfb\xf6\x6d\x78\xbd\xde\xaa\xe9\x6a\x1e\xf7\xa6\x52\x29\x1a\x08\ -\x04\xb0\xbc\xbc\x7c\xaa\xd7\x11\x9c\x4e\x27\x86\x87\x87\x31\x30\ -\x30\xa0\x5e\xb8\x7c\x91\x74\x3a\x4d\xe7\xe6\xe6\x30\x35\x35\xa5\ -\xc9\x6b\xea\x49\x69\x6f\x6f\xc7\xa3\x47\x8f\xd0\xde\xde\x5e\xf3\ -\xce\x91\x63\xbd\xf9\x2c\x2c\x2c\xd0\x97\x2f\x5f\x9e\xaa\x05\x95\ -\xce\xce\x4e\x3c\x79\xf2\x04\x3c\xcf\x6b\xbb\x65\xa6\x48\x38\x1c\ -\xa6\xd3\xd3\xd3\x88\x44\x22\x75\x35\xc2\x6e\xb7\xa3\xaf\xaf\x0f\ -\xc3\xc3\xc3\x65\x63\xfc\x5a\x39\xb6\x01\x84\x10\x2a\x8a\x22\xa2\ -\xd1\x28\xe6\xe6\xe6\x10\x8d\x46\x75\xdd\x46\x73\xee\xdc\x39\x0c\ -\x0e\x0e\xc2\xe7\xf3\xa1\xad\xad\xed\x58\x37\x0f\xa8\xb4\x71\x52\ -\x96\x65\x1a\x0c\x06\xf1\xf6\xed\x5b\x24\x93\x49\xcd\x6a\x04\xc3\ -\x30\x30\x1a\x8d\xe8\xef\xef\xc7\xf0\xf0\x70\xd5\xdd\x20\x35\xe5\ -\xa9\x86\xb0\x22\xb9\x5c\x8e\x2e\x2f\x2f\x23\x1a\x8d\x22\x16\x8b\ -\x21\x1e\x8f\x23\x93\xc9\x9c\x28\x4f\x8e\xe3\xe0\x74\x3a\xe1\x76\ -\xbb\x71\xfe\xfc\x79\x78\xbd\x5e\xb8\x5c\x2e\xd5\x74\x6b\x32\xfd\ -\x23\xcb\x32\x4d\xa5\x52\x48\x24\x12\xf8\xf5\xeb\x17\x42\xa1\x10\ -\xa2\xd1\x68\xd9\x1c\x60\x35\x1c\x0e\x07\xbc\x5e\x2f\x3c\x1e\x0f\ -\x9a\x9b\x9b\x61\xb5\x5a\x61\x36\x9b\x55\xdf\x44\xad\xdb\xfc\x97\ -\x24\x49\x54\x14\x45\x24\x12\x09\xec\xec\xec\x40\x14\x45\xec\xec\ -\xec\x40\x92\x24\x08\x82\x00\x9b\xcd\x06\xab\xd5\x0a\xbb\xdd\x0e\ -\x87\xc3\x71\xe4\xde\xfc\xb8\xfc\x07\x2b\x31\xf0\x27\xb2\xc7\xa5\ -\x82\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x03\x00\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ -\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x04\x67\x41\x4d\x41\x00\x00\xb1\x8f\x0b\xfc\x61\x05\x00\x00\x00\ -\x09\x70\x48\x59\x73\x00\x00\x0e\xc3\x00\x00\x0e\xc3\x01\xc7\x6f\ -\xa8\x64\x00\x00\x02\x95\x49\x44\x41\x54\x58\x47\xb5\x97\x49\xc8\ -\x8d\x61\x18\x86\xcf\x82\x4c\x3b\x64\x9e\xe7\xa1\x64\x48\x84\x62\ -\x61\xd8\x2a\x21\x36\x22\x89\xc8\x4a\x21\x2c\x58\xca\x90\x85\xd5\ -\xdf\xaf\x58\xb0\x93\xb0\xb0\x62\x65\x21\x2b\x53\xd9\xd8\x10\x3b\ -\x43\x64\x63\x88\xeb\xaa\xf3\xd4\xd3\xe9\xd3\xf9\xde\x73\x3e\x77\ -\x5d\xf5\xbf\x77\xcf\x73\x7f\xef\x79\x4e\xff\xf7\xbe\xa7\xd5\x83\ -\xc6\xc0\x71\x38\xdf\xc1\x09\x18\x0b\xff\x5d\xcf\xe1\xcf\x3f\x78\ -\x09\x8d\x68\x01\xac\xad\xe0\x00\x54\x3d\x38\x73\x10\xaa\x7a\x17\ -\x42\x2d\xed\x85\xaa\xe0\x26\xd8\x07\x5d\xf5\x0c\xaa\x9a\x9b\xe0\ -\x05\x74\xd5\x62\x58\x02\x73\x1b\xc6\x4c\xb3\x6b\x69\x0d\xdc\x6e\ -\x98\x75\x50\x5b\x3b\xa1\x6a\x84\xfd\xb0\x0b\x6a\x6b\x3d\x54\x85\ -\xf4\xc3\x06\xa8\x2d\xbf\xaf\xdc\x7c\x13\xb6\xc0\xa7\xe4\xc9\x11\ -\x38\xdc\xe1\x7d\x04\x6b\xed\xc9\xfe\x52\xa8\xad\x19\xf0\x03\xa2\ -\x79\x0f\xa8\x47\x10\xde\x4f\x18\x05\x23\x20\xd7\x5a\xa3\xec\xc9\ -\xb5\x33\xa1\xb6\x7c\xdd\x7e\x86\x08\x38\x06\xea\x01\xe4\xd0\x89\ -\x30\xbe\xfd\x77\xf8\xd6\x28\x7b\xc2\xfb\x02\x66\xd6\xd6\x30\x78\ -\x0b\x11\x70\x16\xd4\x1d\x08\xef\x37\x4c\x82\x09\xf0\xab\xed\x89\ -\x35\xca\x9e\xf0\xde\x81\x99\x45\xf2\xbd\x1e\x01\x97\x34\xd0\x2d\ -\x08\xcf\x0d\x4c\x06\xa7\x90\x37\x60\x8d\xb2\x27\xbc\x57\x1a\xa5\ -\x7a\x0c\x11\x30\xa0\x81\xae\x41\x78\x32\x05\x9c\x82\x9b\x09\xcf\ -\x1a\x65\x4f\x78\x66\x15\xeb\x1e\x44\x40\x7c\xaa\xab\x10\x9e\x4c\ -\x05\xa7\x90\x3d\x6b\x54\x9e\xd6\x7d\x8d\x52\x5d\x87\x08\x70\x33\ -\xea\x02\x84\x27\xd3\xc0\x29\x64\xcf\x1a\x95\x3f\xc0\x0d\x8d\x52\ -\xe5\xef\xf0\xa1\x06\x3a\x07\xe1\xc9\x74\x70\x13\xd9\xb3\x46\xd9\ -\x13\xde\x65\x8d\x52\x9d\x82\x08\x78\xa2\x81\xbc\xf5\x84\x27\xfe\ -\x6f\xbb\x89\xec\x9d\x04\x65\x4f\x78\xa7\x35\x4a\xe5\xc5\x22\x02\ -\xe2\x18\x3d\x0a\xe1\xc9\x2c\xf0\xa5\x95\x3d\x6b\x94\x3d\xe1\x1d\ -\xd2\x28\xd5\x76\x88\x80\x37\x1a\x68\x3f\x84\x27\xb3\xc1\x29\x64\ -\xcf\x1a\x65\x4f\x78\x3b\x34\x4a\xb5\x11\x22\xe0\xbd\x06\xda\x0d\ -\xe1\xc9\x1c\x70\x0a\xd9\xb3\x46\xd9\x13\x9e\x59\xc5\x5a\x01\x11\ -\xe0\x21\xa4\xb6\x42\x78\xe2\x45\xc3\x29\x64\xcf\x1a\x95\x0f\x2e\ -\xb3\x8a\xe5\xa7\x8b\x37\xdc\x77\x18\x0e\x9b\xda\xeb\x60\x1e\x58\ -\x97\xbd\xcd\x60\xad\x3d\xae\xcd\xb0\xa6\x58\xe3\xe0\x2b\x44\xc8\ -\x68\xf0\x76\x9b\x1f\x36\x1f\x9c\x42\xf6\xac\xb1\x36\x36\xff\x0d\ -\xcc\x2a\xd6\x48\xf8\x00\x11\xec\x5b\x6f\x59\x5a\x8b\x57\x6d\x37\ -\x91\xbd\xe5\x60\x6d\xac\xcd\x30\xab\x27\xbd\x86\x08\xf2\x61\xfe\ -\x5e\x88\xb5\x2c\xaa\xf0\x5c\x5b\x1b\x6b\x33\x7a\xd6\x53\x88\xa0\ -\x95\xd0\xf9\xd6\xf3\x96\x9b\x1f\x26\xd6\x58\x1b\x6b\x33\x7a\xd6\ -\x5d\x88\xa0\xd5\x30\x24\xad\xa5\xea\x2c\x18\x0a\xab\xd2\xda\x8c\ -\x9e\xb5\x0d\x22\xc8\x8b\x46\xe7\x61\xe4\xd1\x3b\xd8\xe1\x5d\x84\ -\x7c\x71\x31\xa3\x2f\x5d\x81\xfc\x80\x12\xec\x6d\x44\x8e\xff\x0c\ -\x38\x81\x3a\x58\x6b\x4f\x17\xb5\x5a\x7f\x01\x87\x5c\xb4\xb0\x9d\ -\x20\xb3\xed\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x08\xa8\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x40\x00\x00\x00\x40\x08\x06\x00\x00\x00\xaa\x69\x71\xde\ -\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ -\x00\x00\x08\x5f\x49\x44\x41\x54\x78\x9c\xd5\x9b\x6d\x70\x13\xc7\ -\x1d\xc6\x9f\xdd\x3b\x49\xa7\xb3\x25\xd9\x92\xdf\x30\x08\x6c\x03\ -\x71\x5b\x92\x40\x78\xa9\x21\x04\xca\xcb\x94\x4c\x48\x78\x9b\x36\ -\xa4\x03\x99\x94\x78\x86\x42\x26\x49\x93\x29\x7c\xa0\x1f\x98\xa6\ -\x93\xe9\x40\x29\xd3\x26\x13\x20\x43\x42\x4b\x52\xd2\xa6\x49\x61\ -\x0a\x09\x89\x81\x09\x01\x4c\xca\xe0\xa1\xe0\x21\x19\x48\x80\xda\ -\x04\xdb\x60\x5b\xb6\x6c\x59\x92\x4f\x3a\xe9\x6e\xb7\x1f\x4c\x54\ -\x0c\x02\x0b\x49\x77\x36\xbf\x2f\xa7\x7b\xd1\x3e\xff\x7d\xee\x76\ -\x6f\x77\x6f\x97\xc0\x24\x22\xba\xca\x7d\x7a\x08\xed\x5a\x10\xed\ -\xf1\x1e\x74\x68\x7d\xbf\x23\x2c\x8e\x02\x31\x17\x25\x16\x27\x0a\ -\x05\x27\x8a\x2d\x4e\x14\x8b\x2e\xe4\x51\x3b\x04\x2a\x10\xa3\xe3\ -\x32\x54\x20\xc2\x62\xfc\x93\xe0\x59\xbc\xd1\x79\x18\xc7\xc2\xe7\ -\x00\xe8\x29\xff\x37\x47\xcc\xc7\x4b\x9e\xd9\xa8\x76\x3f\x82\x0a\ -\x6b\x11\x28\xa1\x86\xc4\x9a\xd5\x44\x55\x16\xe7\xa7\x94\xcb\xa8\ -\x8f\x5c\x41\x6d\xef\xd7\xf8\x3c\xf4\x35\xfc\x7a\x4f\x66\x89\x12\ -\x01\x0f\x4a\x65\x98\xef\x18\x87\x49\xf2\x68\x54\xc9\x15\x18\x69\ -\xf5\x64\x2d\xee\xac\x24\xc4\x38\xe3\x75\x4a\x23\xd6\xb6\xef\x45\ -\x83\x72\x19\x3e\xad\x07\x0c\x2c\x1b\x49\xf7\xc3\x4e\x6d\x18\x63\ -\x2d\xc2\xb3\xee\x39\xa8\x76\xcf\x40\x9e\x28\x67\x1c\x7f\x46\x09\ -\x68\x4c\xe7\x61\x16\xc5\x6f\x7c\x1f\x61\x4b\xc7\x21\xe8\x5c\xcd\ -\x34\x9e\x94\x79\x40\x2a\xc3\x5b\xde\x6a\x4c\x90\x46\xc2\x46\x44\ -\x50\x9a\x5e\x11\x49\xdb\x80\x8e\x78\x90\xef\xe8\x3a\x86\x4d\x9d\ -\x07\xd1\x1d\xef\x4a\x37\x99\x0c\xa1\x58\xe8\x9a\x84\x17\x0b\x1e\ -\xc5\x9c\x9c\xef\x41\x48\xc3\x84\xb4\x0c\xe8\xd4\x42\x7c\x45\xd3\ -\x5f\xf0\x59\xa8\x1e\x51\x1e\x4b\x27\x89\xac\x32\xdc\x5a\x88\x3f\ -\x0e\x5b\x86\xa7\xf2\xab\x8c\x35\x20\xce\x74\x5e\x1f\xb9\x82\x59\ -\xdf\xbe\x06\x25\xde\x79\xb7\x5a\x86\xb3\xb6\x70\x11\xd6\x17\x2f\ -\x80\x83\x4a\x29\x17\x89\x94\x0d\xd0\x98\xce\xb7\xfb\x8f\x60\x73\ -\xc7\x7e\x5c\x8e\xf9\xd2\x8f\xd2\x40\x2c\x44\xc4\x7c\xd7\x0f\xb1\ -\xb9\xe4\xa7\x18\x2b\x95\x64\xcf\x80\xb0\x1e\xe5\x2f\x5d\xfb\x00\ -\x1f\x76\xd7\x22\xc4\x22\x99\x45\x69\x38\x04\x15\xb6\x12\xec\xf2\ -\xae\xc6\xf4\xdc\xb1\x03\xe6\x8f\x0e\x74\x41\x44\x8f\xf1\xdf\xfb\ -\x6a\xf0\x6e\xd7\x67\xf7\x40\xe6\x01\x80\xa3\x51\x6d\xc5\xca\xe6\ -\xb7\xf1\x55\xa4\x99\x0f\x74\xf5\x80\x06\xec\xe9\x39\x85\x6d\x9d\ -\x07\xa0\xf1\xd4\x5b\x71\x43\x81\xf3\x6a\x0b\xd6\xb4\xee\x46\x58\ -\x8f\xde\xd1\x84\x3b\x3e\x22\xa7\x7a\x1b\xf9\x9c\x86\xdf\xdd\x23\ -\x77\x3e\x19\x04\xf3\x9c\x93\xf1\x69\xd9\x8b\x10\xa9\x98\x34\xaf\ -\x49\x9f\x00\xc6\x18\xaf\x57\x9a\xf8\x4f\x9a\xb6\xde\xc3\x99\x07\ -\x00\x8e\x43\xc1\x53\xd8\xe0\xfb\x04\x51\x3d\x96\xf4\x49\x48\x6a\ -\x40\x98\xa9\xf8\xad\x6f\x1f\x9a\xd4\x36\x63\xe3\x33\x89\xad\x9d\ -\x87\x50\xa7\x34\x26\x3d\x97\xd4\x80\x93\x4a\x03\x3e\x0e\xd4\x01\ -\x18\xb0\x0e\xb9\x27\x68\xd3\xba\xb1\xc1\xb7\x3f\xe9\xb9\x5b\x0c\ -\x08\x69\x11\xbe\xb6\xf5\x7d\xe8\x06\x74\x66\x06\x93\x03\xa1\x33\ -\xd8\xd7\x73\xe6\x96\x3b\x7a\x8b\x01\x6f\x77\x1f\xc7\x97\x91\xcb\ -\xe6\x44\x65\x32\xbf\xba\xfa\x1e\x5a\xe3\x81\x7e\x26\xf4\x33\xa0\ -\x51\xf5\xf1\x9d\xfe\xc3\xe6\x46\x65\x22\x97\x63\x3e\xec\xea\x3e\ -\xd9\xef\x58\xc2\x00\x8d\xe9\xfc\x6f\x81\x3a\x9c\x57\x5b\x4d\x0f\ -\xcc\x2c\x18\x18\xf6\x04\x4e\xa2\x51\xed\x48\x3c\x05\x09\x03\x54\ -\xae\xa1\x26\x78\x06\x3a\xd7\x06\x27\x3a\x93\xf8\x8f\xd2\x88\xd3\ -\xca\xff\x8b\x78\xc2\x80\xb0\x1e\xc5\x09\xa5\x61\x50\x82\x32\x13\ -\x1d\x1a\x6a\x42\x67\x13\xfb\x09\x03\x76\x05\x4e\x00\x06\xdd\x7d\ -\x1b\x11\xf1\x03\xc9\x0b\x0b\x04\x43\xd2\xbf\x5b\xf6\x05\xeb\xa1\ -\xe8\x2a\x07\xae\x1b\xc0\x18\xe3\x9b\x0c\xac\xfc\x5c\x82\x8c\xed\ -\x23\xaa\xb1\xd5\x5b\x0d\xb7\xc5\x63\x98\x4e\xaa\xf8\xb5\x00\x0e\ -\x87\xcf\x03\xb8\x6e\xc0\x39\xf5\x1a\x7c\xea\x35\xc3\x04\x09\x08\ -\x86\x5b\xf2\xb1\xd2\x3d\x0b\x97\x2a\x37\x60\x51\xde\x34\xd8\x88\ -\xd5\x30\xbd\x54\x78\x3f\x50\x07\xc6\x18\xa7\x00\xf0\xf9\x75\x37\ -\xcc\xc0\x2d\xe4\xe0\xc3\x91\xab\xb1\xa7\xec\x65\x2c\x74\x4e\x86\ -\x34\x48\x46\xd4\xf6\x5e\x40\x98\xa9\x10\x01\xe0\x62\xac\xc3\x54\ -\x71\x2b\x11\xf1\xb8\x73\x3c\xa6\xca\xa3\xb1\x2f\x78\x06\xcf\xb7\ -\xed\x41\xd4\xe4\x21\xb6\x18\x8b\xa0\x4d\xeb\x01\x65\x8c\xf1\x96\ -\x98\xdf\x54\xf1\xef\xf0\x88\xb9\xa8\x76\xcf\x44\x7b\xe5\x46\xac\ -\x2e\x78\x14\xf9\x82\xd3\x34\xed\x18\xd7\xd1\xae\xf5\x80\x2a\x3c\ -\x86\x08\x53\x4c\x13\x4e\x86\x53\xb0\xe3\xf5\xd2\x65\xf8\xb8\x7c\ -\x0d\x9e\x71\xcf\x84\x9d\xca\x86\x6b\xc6\xb9\x8e\x0e\x2d\x04\x1a\ -\x66\x51\x28\xcc\xbc\x0f\x1a\xb7\xc3\x4a\x44\x4c\xcf\x19\x83\x2d\ -\xa5\x3f\xc7\x3f\xcb\x5e\x40\xa5\x34\xca\x50\x3d\x8d\xeb\xe8\xd2\ -\x15\xd0\x6e\x5d\x41\x50\x1f\x3a\x83\x1e\x0e\x41\xc2\xe3\x8e\xf1\ -\xa8\x1f\xfb\x0a\x5e\x29\x7e\x12\x5e\x8b\x07\xd4\x80\x6f\xb8\x2a\ -\xd7\xd1\xac\x05\x41\x03\x9a\x82\x10\x8b\x66\x5d\x20\x53\xec\xd4\ -\x8a\xf5\xc5\x8b\xb0\xaf\x7c\x2d\x56\x7a\xe6\x02\xd4\x9e\x65\x05\ -\x06\x55\x0f\x83\x52\x42\x40\x8c\xfd\x4a\x9e\x36\x94\x10\x3c\x64\ -\x1f\x89\xc5\xae\xc9\xc6\xa4\x0f\x02\x51\x22\x16\xd8\x88\x68\x88\ -\x40\x26\xe8\x9c\xe1\x5c\xf4\x2a\xb6\xf9\x0f\xe3\xaf\xdd\xb5\x80\ -\x01\xf5\x94\x85\x5a\x21\x8a\x84\x42\x24\x03\x8e\x8e\x9b\x4a\x4b\ -\xbc\x0b\x7f\xee\xfa\x02\xef\x75\xd5\xe2\xbf\x31\x63\xba\xe7\x04\ -\x14\x22\xb5\x41\x74\x50\x3b\x64\x6a\x33\x44\x24\x1d\x8e\xf7\x5e\ -\xc0\xfc\x2b\xdb\xa0\xc4\xbb\x0c\x99\x63\xf0\x1d\x22\xa1\xc8\x23\ -\x12\xc4\x22\x8b\x03\x1e\xd1\x61\x98\x50\x2a\x68\x5c\xc7\x97\xd1\ -\x66\x6c\xf2\xd5\xe0\x83\xc0\x17\xa6\x68\x4a\xc4\x82\x0a\xab\x07\ -\xa2\x44\xad\xe4\xd9\xe6\x9d\x83\x36\xfc\x7b\x41\x6d\xc3\x46\x5f\ -\x0d\x8e\x84\xcf\xe2\x8a\x89\x4d\x72\x1b\x11\xe1\xb5\xe6\xf7\xf5\ -\x05\xc6\xd9\x86\x99\x26\x7c\x23\x3b\xba\x6a\xb1\xae\xf5\x1f\xf0\ -\x6b\x19\xce\x23\x4a\x03\x1b\xb5\x60\x84\xc5\xd3\x67\xc0\x34\x79\ -\xb4\x69\xc2\x51\x16\xc7\xf1\xde\x8b\x78\xad\xf3\x20\x3e\x0d\x9e\ -\x36\x4d\xf7\x66\xbc\xd2\x48\xe4\x51\x7b\x9f\x01\x13\xed\xa3\x60\ -\x17\xf2\x10\xd1\x03\x86\x8a\x5e\x52\xdb\xf1\xcb\xb6\xdd\x38\x19\ -\xfa\x0a\x01\x3d\x64\xa8\xd6\x40\x54\xe7\x55\x81\x52\xda\xf7\xfe\ -\xb3\x0b\x56\xb2\xcc\x35\xc9\x30\x31\x8d\x33\xfc\xa1\xe3\x20\xc6\ -\x5f\xfc\x35\x0e\x04\x4e\x0c\x7a\xe6\x01\x8a\xe5\xae\xaa\xeb\xbf\ -\xae\xf3\x72\xe1\x3c\x58\x0d\x1a\x9c\xf0\xeb\x21\xbc\xd9\x59\x83\ -\xc8\x10\xe8\x74\x01\xc0\x12\xd7\x14\xe4\x88\x12\x01\x6e\x30\xa0\ -\xc2\x5a\x84\x29\x72\xc5\xe0\x45\x65\x16\x44\xc4\xd3\xf9\xd3\x13\ -\xbb\x09\x03\xec\xc4\x82\xc5\x79\x53\x41\x87\x60\xb3\x38\x9b\x3c\ -\x22\x8f\xc1\x64\x7b\x59\x62\x3f\x61\x00\xa5\x94\x2c\x75\x4e\xc4\ -\xfd\xf6\xf2\xc1\x88\xcb\x14\x44\x08\x58\x9a\x3f\x1d\x23\x2c\xee\ -\xc4\xb1\x7e\x9d\x00\xaf\xc5\x8d\xe7\x3c\xb3\x4d\x0f\xcc\x2c\x64\ -\x6b\x01\x9e\x76\x55\xf5\x9b\x50\xd9\xcf\x00\x4a\x29\xf9\x45\xfe\ -\x0c\xdc\x27\x99\xd7\x2e\x30\x0b\x0a\x8a\xbf\x0f\x5f\x01\xb7\x25\ -\x97\xf4\x3f\x7e\x13\x02\x15\xc8\x1b\xa5\x4b\x21\x53\xc9\xbc\xe8\ -\x4c\x60\xae\x73\x02\x9e\x70\x8d\xbf\x65\xe0\x23\x69\x3f\x78\x46\ -\xce\x7d\x58\x92\x37\x15\x06\x2f\x27\x30\x0d\xaf\xa5\x00\xaf\x16\ -\x2f\x4e\x7a\x2e\x69\x95\x2f\x0b\x36\xf2\xad\xda\xc1\x1b\xa2\x57\ -\x71\x52\xb9\x64\x68\x70\xc6\x23\x60\x4d\xd1\x02\x4c\xb1\x27\x7f\ -\xc5\xdf\x76\x24\xa4\xcc\x56\x48\xb6\x8c\x58\x81\x32\x6b\x91\x61\ -\xa1\x19\x0f\xc1\xaa\xc2\xc7\xb0\xda\xfd\xa3\xdb\xce\x24\xbf\xe3\ -\x50\xd0\x64\xb9\x9c\xec\x18\xb1\x12\x0e\x13\x3f\x58\x64\x93\x95\ -\xee\xd9\x78\xbd\xe4\x49\x48\x82\xf5\xb6\x65\x79\xc0\xb1\xb0\x99\ -\xb9\x95\x58\x57\xbc\x10\xe2\x3d\xd6\x40\x9a\x2a\x8f\xc5\xba\xa2\ -\x05\x90\x04\xcb\x1d\x2b\xb2\x01\x73\x65\xa5\x22\xe9\xd1\x14\x2e\ -\x72\x8e\xf5\xed\xff\x42\x6c\x90\xbf\x22\xa5\xc2\xec\xdc\xfb\xb1\ -\x71\xd8\x53\x18\x2d\x15\x0f\x58\x8b\xa7\x74\x5b\x5d\xa2\x4c\x74\ -\xa6\xf3\x87\xec\xa3\x50\xdd\xfc\x16\x5a\x86\xe0\x5a\x81\x3e\x08\ -\x56\x15\xfc\x18\x9b\x4a\x96\xc2\x95\xe2\x7a\xa2\xbb\x7a\xcf\x31\ -\xce\xf8\xbf\x7b\x2f\xe1\x85\x96\x77\x70\x36\xda\x84\xa1\x34\x91\ -\x32\x97\xca\xf8\x99\x67\x0e\xfe\x54\xb2\x04\x0e\xc1\x9e\x72\xbe\ -\xd2\x7a\xd1\x37\xa8\x3e\xfe\x6a\xfb\x47\x78\xa7\xeb\x18\xee\x66\ -\x2d\xa0\x51\x94\x59\x8b\xb0\x69\xd8\x72\x2c\x72\x4d\x80\x8d\xde\ -\xb9\xcc\xdf\x4c\x5a\x35\xdb\x68\x5b\x11\x09\xeb\x51\x5e\x25\x97\ -\xe3\xb9\x96\x77\x01\x0c\xde\xcc\xb2\xa9\x8e\x07\xb1\xb3\xf4\x19\ -\x54\xda\x4a\xd2\x5a\x39\x96\x71\x53\xcf\xaf\x85\xf9\x66\x5f\x0d\ -\x76\xf7\xd4\xe1\x92\x81\xd3\x6c\x6e\x44\xa2\x32\x1e\xce\xa9\xc4\ -\x9a\xc2\x79\x78\xcc\xf1\x40\x46\xab\x4a\xb3\xd2\xd6\x8d\x33\x8d\ -\x5f\x54\xdb\xb1\x37\x78\x0a\xdb\xfd\x47\xd0\x64\xd4\xf0\x36\xb1\ -\xe0\x09\xe7\x04\xac\xf2\xcc\xc5\xc3\x72\x05\xf2\x05\x39\xe3\x25\ -\xb5\x59\x6f\xec\xab\x2c\xce\x8f\xf6\x7e\x83\x37\x3b\x8f\x62\x6f\ -\xf0\x34\x90\x85\x65\x75\x5e\x6b\x29\x9e\xf7\xcc\xc2\xf2\xfc\x69\ -\xf0\x66\x71\xd9\x2c\x60\x60\x6f\x87\x71\xc6\xfd\x5a\x18\x47\xc3\ -\xdf\x60\x7f\xef\x05\xf4\xc6\xfd\xe8\x61\x11\x04\x74\x05\x3d\x7a\ -\xdf\xb6\x43\x57\xc0\xb9\x8e\x5c\x41\x82\x9b\xca\x70\x09\x76\xe4\ -\x09\x7d\x5b\x97\x90\x8b\x89\xf6\x0a\xcc\x75\x7c\x1f\xe3\x6c\xa5\ -\xb0\xdc\x66\xc5\x47\xa6\x98\xd2\xdd\x63\x8c\xf1\x08\x8f\x21\xcc\ -\x54\x84\xf4\x28\x42\x2c\x8a\xa0\x1e\x45\x37\x8b\x82\x43\x87\x4c\ -\x6d\x70\x51\x09\x0e\x2a\xc1\x21\xf4\x6d\x73\xa9\xcd\xb0\x4c\xdf\ -\xc8\xff\x00\xbb\x7c\x01\xa7\x6a\x3a\xee\x8e\x00\x00\x00\x00\x49\ -\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x01\xae\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ -\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x04\x67\x41\x4d\x41\x00\x00\xb1\x8f\x0b\xfc\x61\x05\x00\x00\x00\ -\x09\x70\x48\x59\x73\x00\x00\x0e\xc3\x00\x00\x0e\xc3\x01\xc7\x6f\ -\xa8\x64\x00\x00\x01\x43\x49\x44\x41\x54\x58\x47\x63\x60\xa0\x0f\ -\x50\x01\x5a\xb3\x08\x88\x7f\x02\xf1\x1b\x20\x9e\x0b\xc4\x20\x31\ -\xba\x00\x7b\xa0\x2d\x2f\x80\xf8\x3f\x1a\x7e\x0e\xe4\x2b\xd1\xda\ -\x05\x20\xcb\x5f\x61\xb1\x1c\xe6\x98\x25\xb4\x74\x80\x17\x34\xb8\ -\xd1\x7d\x8e\xcc\xff\x4e\x4b\x07\xb4\xe3\xf1\x39\xcc\x11\x3f\x68\ -\xe9\x00\x26\xa0\xe1\x3d\x04\x1c\x31\x9f\xda\x0e\xf0\x00\x1a\xb8\ -\x0d\x88\x15\x91\x0c\xc6\x15\x12\x2f\x81\x6a\x14\xa8\xe9\x00\x07\ -\xa0\x61\xaf\xa1\x3e\xbe\x8c\x96\xc2\x1b\xd1\x42\x02\x94\x30\x6d\ -\x69\x65\x39\x2c\x7e\xaf\x01\x2d\x40\xce\xeb\x35\x50\x47\xbc\xa5\ -\x87\xe5\x30\x47\xdc\x04\x5a\xa6\x81\xe4\xd3\x02\x20\xdb\x95\xd6\ -\x3e\x47\xcf\x76\x77\x81\x16\x1a\x51\xd3\x52\x98\x59\xa0\x42\x06\ -\x16\xe7\xf8\xf2\x3a\x48\xee\x0c\x10\xb3\x51\xd3\x11\x84\x4a\x38\ -\x64\x07\x81\x12\x1c\x28\xd8\x19\xa9\xe5\x00\x52\x2d\xa7\x6a\x6a\ -\x1f\xb5\x9c\x50\x62\x03\xc9\x53\xbd\x90\x19\x0d\xf6\xd1\x60\xc7\ -\x17\x02\xa3\x09\x8e\x5a\xa5\x2b\x03\x17\xd0\x24\x50\xad\x35\x20\ -\x09\x0e\xe4\x0b\x50\x79\x3d\x60\x96\x83\x1c\x00\x6b\xad\xd0\x35\ -\xc1\x21\xc7\xdf\x2e\x02\x21\x40\xf5\xd4\x8e\x6c\xb9\x20\x90\xf3\ -\x0e\x8f\x03\x68\x6a\x39\xc8\x21\x4e\x38\x2c\xff\x0b\x14\x07\x35\ -\x2c\x6d\xa8\x96\xd4\x71\x18\x84\xdc\x6e\x7f\x0f\x54\x03\x8a\x8e\ -\x5a\x20\xb6\x03\x62\x0e\x5a\x5b\x0e\x32\x7f\x37\x10\x77\x00\xb1\ -\x27\x10\x8b\xd0\xc3\x42\x64\x3b\x00\x3b\x73\x24\x98\x91\x7a\xa9\ -\xd4\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -" - -qt_resource_name = b"\ -\x00\x0a\ -\x06\xd2\x80\x84\ -\x00\x62\ -\x00\x6f\x00\x74\x00\x5f\x00\x77\x00\x69\x00\x64\x00\x67\x00\x65\x00\x74\ -\x00\x03\ -\x00\x00\x70\x37\ -\x00\x69\ -\x00\x6d\x00\x67\ -\x00\x09\ -\x0c\x98\xba\x47\ -\x00\x70\ -\x00\x61\x00\x75\x00\x73\x00\x65\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x07\ -\x09\x01\x57\x87\ -\x00\x62\ -\x00\x69\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x08\ -\x02\x8c\x59\xa7\ -\x00\x70\ -\x00\x6c\x00\x61\x00\x79\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x07\ -\x06\xc1\x57\xa7\ -\x00\x70\ -\x00\x65\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ -" - -qt_resource_struct_v1 = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ -\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x04\x00\x00\x00\x03\ -\x00\x00\x00\x52\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x66\ -\x00\x00\x00\x68\x00\x00\x00\x00\x00\x01\x00\x00\x13\x12\ -\x00\x00\x00\x3e\x00\x00\x00\x00\x00\x01\x00\x00\x07\x62\ -\x00\x00\x00\x26\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -" - -qt_resource_struct_v2 = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x04\x00\x00\x00\x03\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x52\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x66\ -\x00\x00\x01\x60\xbf\xbf\x27\x4b\ -\x00\x00\x00\x68\x00\x00\x00\x00\x00\x01\x00\x00\x13\x12\ -\x00\x00\x01\x61\x55\xcc\x5d\xc2\ -\x00\x00\x00\x3e\x00\x00\x00\x00\x00\x01\x00\x00\x07\x62\ -\x00\x00\x01\x61\x55\xc7\x8f\x53\ -\x00\x00\x00\x26\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x60\xbf\xc0\xc1\x40\ -" - -qt_version = QtCore.qVersion().split('.') -if qt_version < ['5', '8', '0']: - rcc_version = 1 - qt_resource_struct = qt_resource_struct_v1 -else: - rcc_version = 2 - qt_resource_struct = qt_resource_struct_v2 - -def qInitResources(): - QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) - -def qCleanupResources(): - QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) - -qInitResources() diff --git a/dexbot/views/bot_item.py b/dexbot/views/bot_item.py index 3882d5712..7816f14ad 100644 --- a/dexbot/views/bot_item.py +++ b/dexbot/views/bot_item.py @@ -1,10 +1,10 @@ -from PyQt5 import QtWidgets - -from dexbot.views.gen.bot_item_widget import Ui_widget -from dexbot.views.confirmation import ConfirmationDialog +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 dexbot.views.edit_bot import EditBotView + +from PyQt5 import QtWidgets class BotItemWidget(QtWidgets.QWidget, Ui_widget): diff --git a/dexbot/views/bot_list.py b/dexbot/views/bot_list.py index 3181c55c3..1fb51c1ed 100644 --- a/dexbot/views/bot_list.py +++ b/dexbot/views/bot_list.py @@ -1,10 +1,10 @@ -from PyQt5 import QtGui, QtWidgets, QtCore - -from dexbot.queue.queue_dispatcher import ThreadDispatcher -from dexbot.views.gen.bot_list_window import Ui_MainWindow -from dexbot.views.create_bot import CreateBotView +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.views.bot_item import BotItemWidget +from dexbot.queue.queue_dispatcher import ThreadDispatcher + +from PyQt5 import QtWidgets class MainView(QtWidgets.QMainWindow): diff --git a/dexbot/views/confirmation.py b/dexbot/views/confirmation.py index 55d292aed..f6af5d81c 100644 --- a/dexbot/views/confirmation.py +++ b/dexbot/views/confirmation.py @@ -1,6 +1,6 @@ -from PyQt5 import QtWidgets +from .ui.confirmation_window_ui import Ui_Dialog -from dexbot.views.gen.confirmation_window import Ui_Dialog +from PyQt5 import QtWidgets class ConfirmationDialog(QtWidgets.QDialog): diff --git a/dexbot/views/create_bot.py b/dexbot/views/create_bot.py index 133ff9ed9..2c88541f4 100644 --- a/dexbot/views/create_bot.py +++ b/dexbot/views/create_bot.py @@ -1,7 +1,7 @@ -from PyQt5 import QtWidgets +from .notice import NoticeDialog +from .ui.create_bot_window_ui import Ui_Dialog -from dexbot.views.notice import NoticeDialog -from dexbot.views.gen.create_bot_window import Ui_Dialog +from PyQt5 import QtWidgets class CreateBotView(QtWidgets.QDialog): diff --git a/dexbot/views/create_wallet.py b/dexbot/views/create_wallet.py index c5444e4fa..e8ef06025 100644 --- a/dexbot/views/create_wallet.py +++ b/dexbot/views/create_wallet.py @@ -1,5 +1,5 @@ -from dexbot.views.gen.create_wallet_window import Ui_Dialog -from dexbot.views.notice import NoticeDialog +from .ui.create_wallet_window_ui import Ui_Dialog +from .notice import NoticeDialog from PyQt5 import QtWidgets diff --git a/dexbot/views/edit_bot.py b/dexbot/views/edit_bot.py index 7b004359c..1a2b49320 100644 --- a/dexbot/views/edit_bot.py +++ b/dexbot/views/edit_bot.py @@ -1,8 +1,9 @@ +from .ui.edit_bot_window_ui import Ui_Dialog +from .confirmation import ConfirmationDialog +from .notice import NoticeDialog + from PyQt5 import QtWidgets -from dexbot.views.notice import NoticeDialog -from dexbot.views.gen.edit_bot_window import Ui_Dialog -from dexbot.views.confirmation import ConfirmationDialog class EditBotView(QtWidgets.QDialog, Ui_Dialog): def __init__(self, controller, botname, config): diff --git a/dexbot/views/gen/__init__.py b/dexbot/views/gen/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/dexbot/views/gen/bot_item_widget.py b/dexbot/views/gen/bot_item_widget.py deleted file mode 100644 index f96fe4e51..000000000 --- a/dexbot/views/gen/bot_item_widget.py +++ /dev/null @@ -1,272 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'bot_item_widget.ui' -# -# Created by: PyQt5 UI code generator 5.9.2 -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore, QtGui, QtWidgets - -class Ui_widget(object): - def setupUi(self, widget): - widget.setObjectName("widget") - widget.setEnabled(True) - widget.resize(480, 138) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(widget.sizePolicy().hasHeightForWidth()) - widget.setSizePolicy(sizePolicy) - self.gridLayout_2 = QtWidgets.QGridLayout(widget) - self.gridLayout_2.setContentsMargins(0, 0, 0, 0) - self.gridLayout_2.setObjectName("gridLayout_2") - self.widget_frame = QtWidgets.QFrame(widget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.widget_frame.sizePolicy().hasHeightForWidth()) - self.widget_frame.setSizePolicy(sizePolicy) - self.widget_frame.setMinimumSize(QtCore.QSize(480, 137)) - self.widget_frame.setMaximumSize(QtCore.QSize(16777215, 16777215)) - self.widget_frame.setLayoutDirection(QtCore.Qt.LeftToRight) - self.widget_frame.setAutoFillBackground(False) - self.widget_frame.setStyleSheet(".QFrame { border: 1px solid #005B78; border-radius: 4px; }\n" -"* { background-color: white; }\n" -"") - self.widget_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.widget_frame.setFrameShadow(QtWidgets.QFrame.Raised) - self.widget_frame.setObjectName("widget_frame") - self.gridLayout_6 = QtWidgets.QGridLayout(self.widget_frame) - self.gridLayout_6.setObjectName("gridLayout_6") - self.widget_3 = QtWidgets.QWidget(self.widget_frame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.widget_3.sizePolicy().hasHeightForWidth()) - self.widget_3.setSizePolicy(sizePolicy) - self.widget_3.setObjectName("widget_3") - self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.widget_3) - self.horizontalLayout_4.setContentsMargins(0, -1, 0, 1) - self.horizontalLayout_4.setObjectName("horizontalLayout_4") - self.botname_label = QtWidgets.QLabel(self.widget_3) - font = QtGui.QFont() - font.setPointSize(12) - font.setBold(True) - font.setWeight(75) - self.botname_label.setFont(font) - self.botname_label.setStyleSheet("color: #005B78;") - self.botname_label.setAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft) - self.botname_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) - self.botname_label.setObjectName("botname_label") - self.horizontalLayout_4.addWidget(self.botname_label) - self.strategy_label = QtWidgets.QLabel(self.widget_3) - self.strategy_label.setMaximumSize(QtCore.QSize(16777215, 16777215)) - font = QtGui.QFont() - font.setPointSize(9) - font.setBold(True) - font.setWeight(75) - self.strategy_label.setFont(font) - self.strategy_label.setAutoFillBackground(False) - self.strategy_label.setStyleSheet("color: #005B78;") - self.strategy_label.setAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing) - self.strategy_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) - self.strategy_label.setObjectName("strategy_label") - self.horizontalLayout_4.addWidget(self.strategy_label) - self.edit_button = QtWidgets.QPushButton(self.widget_3) - self.edit_button.setMaximumSize(QtCore.QSize(28, 16777215)) - self.edit_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.edit_button.setStyleSheet("border: 0;") - self.edit_button.setText("") - icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(":/bot_widget/img/pen.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.edit_button.setIcon(icon) - self.edit_button.setObjectName("edit_button") - self.horizontalLayout_4.addWidget(self.edit_button) - self.remove_button = QtWidgets.QPushButton(self.widget_3) - self.remove_button.setMaximumSize(QtCore.QSize(28, 16777215)) - self.remove_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.remove_button.setStyleSheet("border: 0;") - self.remove_button.setText("") - icon1 = QtGui.QIcon() - icon1.addPixmap(QtGui.QPixmap(":/bot_widget/img/bin.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.remove_button.setIcon(icon1) - self.remove_button.setIconSize(QtCore.QSize(20, 20)) - self.remove_button.setObjectName("remove_button") - self.horizontalLayout_4.addWidget(self.remove_button) - self.gridLayout_6.addWidget(self.widget_3, 1, 0, 1, 1) - self.gridLayout = QtWidgets.QGridLayout() - self.gridLayout.setContentsMargins(0, -1, -1, -1) - self.gridLayout.setObjectName("gridLayout") - self.gridLayout_3 = QtWidgets.QGridLayout() - self.gridLayout_3.setObjectName("gridLayout_3") - self.widget_6 = QtWidgets.QWidget(self.widget_frame) - self.widget_6.setMinimumSize(QtCore.QSize(130, 0)) - self.widget_6.setObjectName("widget_6") - self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.widget_6) - self.verticalLayout_5.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_5.setObjectName("verticalLayout_5") - self.currency_label = QtWidgets.QLabel(self.widget_6) - font = QtGui.QFont() - font.setPointSize(11) - font.setBold(True) - font.setWeight(75) - self.currency_label.setFont(font) - self.currency_label.setStyleSheet("color: #005B78;") - self.currency_label.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) - self.currency_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) - self.currency_label.setObjectName("currency_label") - self.verticalLayout_5.addWidget(self.currency_label) - self.profit_label = QtWidgets.QLabel(self.widget_6) - font = QtGui.QFont() - font.setPointSize(10) - font.setBold(True) - font.setWeight(75) - self.profit_label.setFont(font) - self.profit_label.setStyleSheet("color: #00D05A;") - self.profit_label.setLineWidth(1) - self.profit_label.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) - self.profit_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) - self.profit_label.setObjectName("profit_label") - self.verticalLayout_5.addWidget(self.profit_label) - self.gridLayout_3.addWidget(self.widget_6, 0, 0, 1, 1) - self.gridLayout.addLayout(self.gridLayout_3, 0, 0, 1, 1) - self.widget_4 = QtWidgets.QWidget(self.widget_frame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.widget_4.sizePolicy().hasHeightForWidth()) - self.widget_4.setSizePolicy(sizePolicy) - self.widget_4.setObjectName("widget_4") - self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.widget_4) - self.verticalLayout_4.setContentsMargins(-1, -1, -1, 5) - self.verticalLayout_4.setObjectName("verticalLayout_4") - spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout_4.addItem(spacerItem) - self.widget_7 = QtWidgets.QWidget(self.widget_4) - self.widget_7.setObjectName("widget_7") - self.horizontalLayout = QtWidgets.QHBoxLayout(self.widget_7) - self.horizontalLayout.setContentsMargins(0, -1, 0, 0) - self.horizontalLayout.setObjectName("horizontalLayout") - self.buy_label = QtWidgets.QLabel(self.widget_7) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(75) - self.buy_label.setFont(font) - self.buy_label.setStyleSheet("color: #005B78;") - self.buy_label.setObjectName("buy_label") - self.horizontalLayout.addWidget(self.buy_label) - spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout.addItem(spacerItem1) - self.sell_label = QtWidgets.QLabel(self.widget_7) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(75) - self.sell_label.setFont(font) - self.sell_label.setStyleSheet("color: #005B78;") - self.sell_label.setObjectName("sell_label") - self.horizontalLayout.addWidget(self.sell_label) - self.verticalLayout_4.addWidget(self.widget_7) - self.widget_2 = QtWidgets.QWidget(self.widget_4) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.widget_2.sizePolicy().hasHeightForWidth()) - self.widget_2.setSizePolicy(sizePolicy) - self.widget_2.setStyleSheet("") - self.widget_2.setObjectName("widget_2") - self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.widget_2) - self.horizontalLayout_3.setContentsMargins(5, 0, 5, 0) - self.horizontalLayout_3.setObjectName("horizontalLayout_3") - self.order_slider = QtWidgets.QSlider(self.widget_2) - self.order_slider.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.order_slider.sizePolicy().hasHeightForWidth()) - self.order_slider.setSizePolicy(sizePolicy) - self.order_slider.setStyleSheet("QSlider::groove:horizontal {\n" -"height: 2px;\n" -"background: #005B78;\n" -"}\n" -"QSlider::handle:horizontal {\n" -"background: #005B78;\n" -"width: 15px;\n" -"margin: -5px 0;\n" -"}\n" -"QSlider {\n" -"border-left: 2px solid #005B78;\n" -"border-right: 2px solid #005B78;\n" -"}") - self.order_slider.setMaximum(100) - self.order_slider.setSliderPosition(50) - self.order_slider.setTracking(False) - self.order_slider.setOrientation(QtCore.Qt.Horizontal) - self.order_slider.setTickPosition(QtWidgets.QSlider.TicksAbove) - self.order_slider.setObjectName("order_slider") - self.horizontalLayout_3.addWidget(self.order_slider) - self.verticalLayout_4.addWidget(self.widget_2) - self.gridLayout.addWidget(self.widget_4, 0, 1, 2, 1) - self.widget_5 = QtWidgets.QWidget(self.widget_frame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.widget_5.sizePolicy().hasHeightForWidth()) - self.widget_5.setSizePolicy(sizePolicy) - self.widget_5.setMaximumSize(QtCore.QSize(16777215, 16777215)) - self.widget_5.setObjectName("widget_5") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.widget_5) - self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.pause_button = QtWidgets.QPushButton(self.widget_5) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.pause_button.sizePolicy().hasHeightForWidth()) - self.pause_button.setSizePolicy(sizePolicy) - self.pause_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.pause_button.setStyleSheet("border: 0;\n" -"") - self.pause_button.setText("") - icon2 = QtGui.QIcon() - icon2.addPixmap(QtGui.QPixmap(":/bot_widget/img/pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.pause_button.setIcon(icon2) - self.pause_button.setIconSize(QtCore.QSize(30, 30)) - self.pause_button.setObjectName("pause_button") - self.horizontalLayout_2.addWidget(self.pause_button) - self.play_button = QtWidgets.QPushButton(self.widget_5) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.play_button.sizePolicy().hasHeightForWidth()) - self.play_button.setSizePolicy(sizePolicy) - self.play_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.play_button.setStatusTip("") - self.play_button.setStyleSheet("border: 0;") - self.play_button.setText("") - icon3 = QtGui.QIcon() - icon3.addPixmap(QtGui.QPixmap(":/bot_widget/img/play.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.play_button.setIcon(icon3) - self.play_button.setIconSize(QtCore.QSize(30, 30)) - self.play_button.setObjectName("play_button") - self.horizontalLayout_2.addWidget(self.play_button) - spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_2.addItem(spacerItem2) - self.gridLayout.addWidget(self.widget_5, 1, 0, 1, 1) - self.gridLayout_6.addLayout(self.gridLayout, 3, 0, 1, 1) - self.gridLayout_2.addWidget(self.widget_frame, 0, 0, 1, 1) - - self.retranslateUi(widget) - QtCore.QMetaObject.connectSlotsByName(widget) - - def retranslateUi(self, widget): - _translate = QtCore.QCoreApplication.translate - widget.setWindowTitle(_translate("widget", "widget")) - self.botname_label.setText(_translate("widget", "Botname")) - self.strategy_label.setText(_translate("widget", "SIMPLE STRATEGY")) - self.currency_label.setText(_translate("widget", "BTS/USD")) - self.profit_label.setText(_translate("widget", "+0.0%")) - self.buy_label.setText(_translate("widget", "Buy")) - self.sell_label.setText(_translate("widget", "Sell")) - -from dexbot.resources import icons_rc diff --git a/dexbot/views/gen/bot_list_window.py b/dexbot/views/gen/bot_list_window.py deleted file mode 100644 index 97229c00b..000000000 --- a/dexbot/views/gen/bot_list_window.py +++ /dev/null @@ -1,95 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'dexbot/views/orig/bot_list_window.ui' -# -# Created by: PyQt5 UI code generator 5.9.2 -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore, QtGui, QtWidgets - -class Ui_MainWindow(object): - def setupUi(self, MainWindow): - MainWindow.setObjectName("MainWindow") - MainWindow.resize(814, 513) - MainWindow.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - MainWindow.setStyleSheet("background-color: #EDEDED") - self.centralwidget = QtWidgets.QWidget(MainWindow) - self.centralwidget.setAutoFillBackground(False) - self.centralwidget.setStyleSheet("") - self.centralwidget.setObjectName("centralwidget") - self.gridLayout_2 = QtWidgets.QGridLayout(self.centralwidget) - self.gridLayout_2.setObjectName("gridLayout_2") - self.scrollArea = QtWidgets.QScrollArea(self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(1) - sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth()) - self.scrollArea.setSizePolicy(sizePolicy) - self.scrollArea.setAutoFillBackground(False) - self.scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame) - self.scrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.scrollArea.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents) - self.scrollArea.setWidgetResizable(True) - self.scrollArea.setAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignTop) - self.scrollArea.setObjectName("scrollArea") - self.scrollAreaWidgetContents = QtWidgets.QWidget() - self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(389, 0, 18, 18)) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.scrollAreaWidgetContents.sizePolicy().hasHeightForWidth()) - self.scrollAreaWidgetContents.setSizePolicy(sizePolicy) - self.scrollAreaWidgetContents.setToolTipDuration(-7) - self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") - self.verticalLayout = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents) - self.verticalLayout.setObjectName("verticalLayout") - self.scrollArea.setWidget(self.scrollAreaWidgetContents) - self.gridLayout_2.addWidget(self.scrollArea, 3, 0, 1, 1) - self.widget = QtWidgets.QWidget(self.centralwidget) - self.widget.setEnabled(True) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.widget.sizePolicy().hasHeightForWidth()) - self.widget.setSizePolicy(sizePolicy) - self.widget.setMinimumSize(QtCore.QSize(0, 100)) - self.widget.setObjectName("widget") - self.gridLayout_4 = QtWidgets.QGridLayout(self.widget) - self.gridLayout_4.setObjectName("gridLayout_4") - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.label = QtWidgets.QLabel(self.widget) - self.label.setObjectName("label") - self.horizontalLayout.addWidget(self.label) - self.gridLayout_4.addLayout(self.horizontalLayout, 0, 0, 1, 1) - self.horizontalLayout_3 = QtWidgets.QHBoxLayout() - self.horizontalLayout_3.setObjectName("horizontalLayout_3") - self.add_bot_button = QtWidgets.QPushButton(self.widget) - self.add_bot_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.add_bot_button.setToolTipDuration(-1) - self.add_bot_button.setObjectName("add_bot_button") - self.horizontalLayout_3.addWidget(self.add_bot_button) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_3.addItem(spacerItem) - self.gridLayout_4.addLayout(self.horizontalLayout_3, 1, 0, 1, 1) - self.gridLayout_2.addWidget(self.widget, 0, 0, 1, 1) - self.line = QtWidgets.QFrame(self.centralwidget) - self.line.setFrameShape(QtWidgets.QFrame.HLine) - self.line.setFrameShadow(QtWidgets.QFrame.Sunken) - self.line.setObjectName("line") - self.gridLayout_2.addWidget(self.line, 2, 0, 1, 1) - self.scrollArea.raise_() - self.widget.raise_() - self.line.raise_() - MainWindow.setCentralWidget(self.centralwidget) - - self.retranslateUi(MainWindow) - QtCore.QMetaObject.connectSlotsByName(MainWindow) - - def retranslateUi(self, MainWindow): - _translate = QtCore.QCoreApplication.translate - MainWindow.setWindowTitle(_translate("MainWindow", "DEXBot")) - self.label.setText(_translate("MainWindow", "DEXBot")) - self.add_bot_button.setText(_translate("MainWindow", "Add bot")) - diff --git a/dexbot/views/gen/confirmation_window.py b/dexbot/views/gen/confirmation_window.py deleted file mode 100644 index 03a67893e..000000000 --- a/dexbot/views/gen/confirmation_window.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'dexbot/views/orig/confirmation_window.ui' -# -# Created by: PyQt5 UI code generator 5.9.2 -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore, QtGui, QtWidgets - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName("Dialog") - Dialog.resize(569, 107) - self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) - self.verticalLayout.setObjectName("verticalLayout") - self.confirmation_label = QtWidgets.QLabel(Dialog) - self.confirmation_label.setText("") - self.confirmation_label.setAlignment(QtCore.Qt.AlignCenter) - self.confirmation_label.setObjectName("confirmation_label") - self.verticalLayout.addWidget(self.confirmation_label) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout.addItem(spacerItem) - self.cancel_button = QtWidgets.QPushButton(Dialog) - self.cancel_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.cancel_button.setObjectName("cancel_button") - self.horizontalLayout.addWidget(self.cancel_button) - self.ok_button = QtWidgets.QPushButton(Dialog) - self.ok_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.ok_button.setObjectName("ok_button") - self.horizontalLayout.addWidget(self.ok_button) - spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout.addItem(spacerItem1) - self.verticalLayout.addLayout(self.horizontalLayout) - - self.retranslateUi(Dialog) - self.ok_button.clicked.connect(Dialog.accept) - self.cancel_button.clicked.connect(Dialog.reject) - QtCore.QMetaObject.connectSlotsByName(Dialog) - Dialog.setTabOrder(self.ok_button, self.cancel_button) - - def retranslateUi(self, Dialog): - _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "DEXBot - Confirmation")) - self.cancel_button.setText(_translate("Dialog", "Cancel")) - self.ok_button.setText(_translate("Dialog", "OK")) - diff --git a/dexbot/views/gen/create_bot_window.py b/dexbot/views/gen/create_bot_window.py deleted file mode 100644 index eecede3c1..000000000 --- a/dexbot/views/gen/create_bot_window.py +++ /dev/null @@ -1,256 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'orig/create_bot_window.ui' -# -# Created by: PyQt5 UI code generator 5.10 -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore, QtGui, QtWidgets - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName("Dialog") - Dialog.resize(418, 474) - Dialog.setModal(True) - self.gridLayout = QtWidgets.QGridLayout(Dialog) - self.gridLayout.setObjectName("gridLayout") - self.widget = QtWidgets.QWidget(Dialog) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.widget.sizePolicy().hasHeightForWidth()) - self.widget.setSizePolicy(sizePolicy) - self.widget.setObjectName("widget") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.widget) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - spacerItem = QtWidgets.QSpacerItem(179, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_2.addItem(spacerItem) - self.cancel_button = QtWidgets.QPushButton(self.widget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.cancel_button.sizePolicy().hasHeightForWidth()) - self.cancel_button.setSizePolicy(sizePolicy) - self.cancel_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.cancel_button.setObjectName("cancel_button") - self.horizontalLayout_2.addWidget(self.cancel_button) - self.save_button = QtWidgets.QPushButton(self.widget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.save_button.sizePolicy().hasHeightForWidth()) - self.save_button.setSizePolicy(sizePolicy) - self.save_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.save_button.setObjectName("save_button") - self.horizontalLayout_2.addWidget(self.save_button) - self.gridLayout.addWidget(self.widget, 4, 0, 1, 1) - self.groupBox_3 = QtWidgets.QGroupBox(Dialog) - self.groupBox_3.setObjectName("groupBox_3") - self.formLayout = QtWidgets.QFormLayout(self.groupBox_3) - self.formLayout.setObjectName("formLayout") - self.amount_label = QtWidgets.QLabel(self.groupBox_3) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.amount_label.sizePolicy().hasHeightForWidth()) - self.amount_label.setSizePolicy(sizePolicy) - self.amount_label.setMinimumSize(QtCore.QSize(110, 0)) - self.amount_label.setMaximumSize(QtCore.QSize(110, 16777215)) - self.amount_label.setObjectName("amount_label") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.amount_label) - self.amount_input = QtWidgets.QDoubleSpinBox(self.groupBox_3) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.amount_input.sizePolicy().hasHeightForWidth()) - self.amount_input.setSizePolicy(sizePolicy) - self.amount_input.setMinimumSize(QtCore.QSize(140, 0)) - self.amount_input.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) - self.amount_input.setDecimals(8) - self.amount_input.setMaximum(999999999.999) - self.amount_input.setObjectName("amount_input") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.amount_input) - self.center_price_label = QtWidgets.QLabel(self.groupBox_3) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.center_price_label.sizePolicy().hasHeightForWidth()) - self.center_price_label.setSizePolicy(sizePolicy) - self.center_price_label.setMinimumSize(QtCore.QSize(110, 0)) - self.center_price_label.setMaximumSize(QtCore.QSize(110, 16777215)) - self.center_price_label.setObjectName("center_price_label") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.center_price_label) - self.center_price_input = QtWidgets.QDoubleSpinBox(self.groupBox_3) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.center_price_input.sizePolicy().hasHeightForWidth()) - self.center_price_input.setSizePolicy(sizePolicy) - self.center_price_input.setMinimumSize(QtCore.QSize(140, 0)) - self.center_price_input.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) - self.center_price_input.setAccelerated(False) - self.center_price_input.setProperty("showGroupSeparator", False) - self.center_price_input.setDecimals(8) - self.center_price_input.setMinimum(-999999999.999) - self.center_price_input.setMaximum(999999999.999) - self.center_price_input.setObjectName("center_price_input") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.center_price_input) - self.spread_label = QtWidgets.QLabel(self.groupBox_3) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.spread_label.sizePolicy().hasHeightForWidth()) - self.spread_label.setSizePolicy(sizePolicy) - self.spread_label.setMinimumSize(QtCore.QSize(110, 0)) - self.spread_label.setMaximumSize(QtCore.QSize(110, 16777215)) - self.spread_label.setObjectName("spread_label") - self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.spread_label) - self.spread_input = QtWidgets.QDoubleSpinBox(self.groupBox_3) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.spread_input.sizePolicy().hasHeightForWidth()) - self.spread_input.setSizePolicy(sizePolicy) - self.spread_input.setMinimumSize(QtCore.QSize(140, 0)) - self.spread_input.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) - self.spread_input.setMaximum(100000.0) - self.spread_input.setProperty("value", 5.0) - self.spread_input.setObjectName("spread_input") - self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.spread_input) - self.gridLayout.addWidget(self.groupBox_3, 2, 0, 1, 1) - self.groupBox_2 = QtWidgets.QGroupBox(Dialog) - self.groupBox_2.setObjectName("groupBox_2") - self.formLayout_2 = QtWidgets.QFormLayout(self.groupBox_2) - self.formLayout_2.setRowWrapPolicy(QtWidgets.QFormLayout.WrapLongRows) - self.formLayout_2.setObjectName("formLayout_2") - self.account_label = QtWidgets.QLabel(self.groupBox_2) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.account_label.sizePolicy().hasHeightForWidth()) - self.account_label.setSizePolicy(sizePolicy) - self.account_label.setMinimumSize(QtCore.QSize(110, 0)) - self.account_label.setMaximumSize(QtCore.QSize(110, 16777215)) - self.account_label.setObjectName("account_label") - self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.account_label) - self.account_input = QtWidgets.QLineEdit(self.groupBox_2) - self.account_input.setObjectName("account_input") - self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.account_input) - self.private_key_label = QtWidgets.QLabel(self.groupBox_2) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.private_key_label.sizePolicy().hasHeightForWidth()) - self.private_key_label.setSizePolicy(sizePolicy) - self.private_key_label.setMinimumSize(QtCore.QSize(110, 0)) - self.private_key_label.setMaximumSize(QtCore.QSize(110, 16777215)) - self.private_key_label.setScaledContents(False) - self.private_key_label.setWordWrap(True) - self.private_key_label.setObjectName("private_key_label") - self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.private_key_label) - self.private_key_input = QtWidgets.QLineEdit(self.groupBox_2) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.private_key_input.sizePolicy().hasHeightForWidth()) - self.private_key_input.setSizePolicy(sizePolicy) - self.private_key_input.setEchoMode(QtWidgets.QLineEdit.Password) - self.private_key_input.setClearButtonEnabled(False) - self.private_key_input.setObjectName("private_key_input") - self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.private_key_input) - self.gridLayout.addWidget(self.groupBox_2, 1, 0, 1, 1) - self.groupBox = QtWidgets.QGroupBox(Dialog) - self.groupBox.setObjectName("groupBox") - self.formLayout_3 = QtWidgets.QFormLayout(self.groupBox) - self.formLayout_3.setObjectName("formLayout_3") - self.strategy_label = QtWidgets.QLabel(self.groupBox) - self.strategy_label.setMinimumSize(QtCore.QSize(110, 0)) - self.strategy_label.setMaximumSize(QtCore.QSize(110, 16777215)) - self.strategy_label.setObjectName("strategy_label") - self.formLayout_3.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.strategy_label) - self.strategy_input = QtWidgets.QComboBox(self.groupBox) - self.strategy_input.setObjectName("strategy_input") - self.formLayout_3.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.strategy_input) - self.bot_name_label = QtWidgets.QLabel(self.groupBox) - self.bot_name_label.setMinimumSize(QtCore.QSize(110, 0)) - self.bot_name_label.setMaximumSize(QtCore.QSize(110, 16777215)) - self.bot_name_label.setObjectName("bot_name_label") - self.formLayout_3.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.bot_name_label) - self.bot_name_input = QtWidgets.QLineEdit(self.groupBox) - self.bot_name_input.setObjectName("bot_name_input") - self.formLayout_3.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.bot_name_input) - self.base_asset_label = QtWidgets.QLabel(self.groupBox) - self.base_asset_label.setMinimumSize(QtCore.QSize(110, 0)) - self.base_asset_label.setMaximumSize(QtCore.QSize(110, 16777215)) - self.base_asset_label.setObjectName("base_asset_label") - self.formLayout_3.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.base_asset_label) - self.quote_asset_label = QtWidgets.QLabel(self.groupBox) - self.quote_asset_label.setMinimumSize(QtCore.QSize(110, 0)) - self.quote_asset_label.setMaximumSize(QtCore.QSize(110, 16777215)) - self.quote_asset_label.setObjectName("quote_asset_label") - self.formLayout_3.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.quote_asset_label) - self.quote_asset_input = QtWidgets.QLineEdit(self.groupBox) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.quote_asset_input.sizePolicy().hasHeightForWidth()) - self.quote_asset_input.setSizePolicy(sizePolicy) - self.quote_asset_input.setMaximumSize(QtCore.QSize(80, 16777215)) - self.quote_asset_input.setObjectName("quote_asset_input") - self.formLayout_3.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.quote_asset_input) - self.base_asset_input = QtWidgets.QComboBox(self.groupBox) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.base_asset_input.sizePolicy().hasHeightForWidth()) - self.base_asset_input.setSizePolicy(sizePolicy) - self.base_asset_input.setMinimumSize(QtCore.QSize(105, 0)) - self.base_asset_input.setEditable(True) - self.base_asset_input.setObjectName("base_asset_input") - self.formLayout_3.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.base_asset_input) - self.gridLayout.addWidget(self.groupBox, 0, 0, 1, 1) - spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.gridLayout.addItem(spacerItem1, 3, 0, 1, 1) - self.amount_label.setBuddy(self.amount_input) - self.center_price_label.setBuddy(self.center_price_input) - self.spread_label.setBuddy(self.spread_input) - self.account_label.setBuddy(self.account_input) - self.private_key_label.setBuddy(self.private_key_input) - self.strategy_label.setBuddy(self.strategy_input) - self.bot_name_label.setBuddy(self.bot_name_input) - self.quote_asset_label.setBuddy(self.quote_asset_input) - - self.retranslateUi(Dialog) - self.strategy_input.setCurrentIndex(-1) - QtCore.QMetaObject.connectSlotsByName(Dialog) - Dialog.setTabOrder(self.strategy_input, self.bot_name_input) - Dialog.setTabOrder(self.bot_name_input, self.base_asset_input) - Dialog.setTabOrder(self.base_asset_input, self.quote_asset_input) - Dialog.setTabOrder(self.quote_asset_input, self.account_input) - Dialog.setTabOrder(self.account_input, self.private_key_input) - Dialog.setTabOrder(self.private_key_input, self.amount_input) - Dialog.setTabOrder(self.amount_input, self.center_price_input) - Dialog.setTabOrder(self.center_price_input, self.spread_input) - Dialog.setTabOrder(self.spread_input, self.save_button) - Dialog.setTabOrder(self.save_button, self.cancel_button) - - def retranslateUi(self, Dialog): - _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "DEXBot - Create Bot")) - self.cancel_button.setText(_translate("Dialog", "Cancel")) - self.save_button.setText(_translate("Dialog", "Save")) - self.groupBox_3.setTitle(_translate("Dialog", "Bot Parameters")) - self.amount_label.setText(_translate("Dialog", "Amount")) - self.center_price_label.setText(_translate("Dialog", "Center Price")) - self.spread_label.setText(_translate("Dialog", "Spread")) - self.spread_input.setSuffix(_translate("Dialog", "%")) - self.groupBox_2.setTitle(_translate("Dialog", "Bitshares Account Details")) - self.account_label.setText(_translate("Dialog", "Account")) - self.private_key_label.setText(_translate("Dialog", "Private Active Key")) - self.groupBox.setTitle(_translate("Dialog", "Bot Details")) - self.strategy_label.setText(_translate("Dialog", "Strategy")) - self.bot_name_label.setText(_translate("Dialog", "Bot Name")) - self.base_asset_label.setText(_translate("Dialog", "Base Asset")) - self.quote_asset_label.setText(_translate("Dialog", "Quote Asset")) - diff --git a/dexbot/views/gen/create_wallet_window.py b/dexbot/views/gen/create_wallet_window.py deleted file mode 100644 index 8cba44efd..000000000 --- a/dexbot/views/gen/create_wallet_window.py +++ /dev/null @@ -1,101 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'dexbot/views/orig/create_wallet_window.ui' -# -# Created by: PyQt5 UI code generator 5.9.2 -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore, QtGui, QtWidgets - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName("Dialog") - Dialog.resize(530, 196) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(Dialog.sizePolicy().hasHeightForWidth()) - Dialog.setSizePolicy(sizePolicy) - Dialog.setModal(True) - self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) - self.verticalLayout.setObjectName("verticalLayout") - self.helper_text = QtWidgets.QLabel(Dialog) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Maximum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.helper_text.sizePolicy().hasHeightForWidth()) - self.helper_text.setSizePolicy(sizePolicy) - self.helper_text.setAlignment(QtCore.Qt.AlignCenter) - self.helper_text.setObjectName("helper_text") - self.verticalLayout.addWidget(self.helper_text) - self.label = QtWidgets.QLabel(Dialog) - self.label.setAlignment(QtCore.Qt.AlignCenter) - self.label.setObjectName("label") - self.verticalLayout.addWidget(self.label) - self.widget = QtWidgets.QWidget(Dialog) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.widget.sizePolicy().hasHeightForWidth()) - self.widget.setSizePolicy(sizePolicy) - self.widget.setMaximumSize(QtCore.QSize(16777215, 126)) - self.widget.setObjectName("widget") - self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.widget) - self.horizontalLayout_3.setContentsMargins(-1, 5, -1, 5) - self.horizontalLayout_3.setObjectName("horizontalLayout_3") - self.form_wrap = QtWidgets.QWidget(self.widget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.form_wrap.sizePolicy().hasHeightForWidth()) - self.form_wrap.setSizePolicy(sizePolicy) - self.form_wrap.setObjectName("form_wrap") - self.formLayout = QtWidgets.QFormLayout(self.form_wrap) - self.formLayout.setLabelAlignment(QtCore.Qt.AlignCenter) - self.formLayout.setFormAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignTop) - self.formLayout.setObjectName("formLayout") - self.password_label = QtWidgets.QLabel(self.form_wrap) - self.password_label.setObjectName("password_label") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.password_label) - self.password_input = QtWidgets.QLineEdit(self.form_wrap) - self.password_input.setMinimumSize(QtCore.QSize(200, 0)) - self.password_input.setEchoMode(QtWidgets.QLineEdit.Password) - self.password_input.setObjectName("password_input") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.password_input) - self.ok_button = QtWidgets.QPushButton(self.form_wrap) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.ok_button.sizePolicy().hasHeightForWidth()) - self.ok_button.setSizePolicy(sizePolicy) - self.ok_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.ok_button.setObjectName("ok_button") - self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.ok_button) - self.confirm_password_label = QtWidgets.QLabel(self.form_wrap) - self.confirm_password_label.setObjectName("confirm_password_label") - self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.confirm_password_label) - self.confirm_password_input = QtWidgets.QLineEdit(self.form_wrap) - self.confirm_password_input.setMinimumSize(QtCore.QSize(200, 0)) - self.confirm_password_input.setEchoMode(QtWidgets.QLineEdit.Password) - self.confirm_password_input.setObjectName("confirm_password_input") - self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.confirm_password_input) - self.horizontalLayout_3.addWidget(self.form_wrap) - self.verticalLayout.addWidget(self.widget) - spacerItem = QtWidgets.QSpacerItem(20, 25, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout.addItem(spacerItem) - self.password_label.setBuddy(self.password_input) - self.confirm_password_label.setBuddy(self.confirm_password_input) - - self.retranslateUi(Dialog) - QtCore.QMetaObject.connectSlotsByName(Dialog) - - def retranslateUi(self, Dialog): - _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "Create wallet")) - self.helper_text.setText(_translate("Dialog", "Before you can start using DEXBot, you need to create a wallet.")) - self.label.setText(_translate("Dialog", "Wallet password is used to encrypt your BitShares account private keys.")) - self.password_label.setText(_translate("Dialog", "Wallet password")) - self.ok_button.setText(_translate("Dialog", "OK")) - self.confirm_password_label.setText(_translate("Dialog", "Confirm password")) - diff --git a/dexbot/views/gen/edit_bot_window.py b/dexbot/views/gen/edit_bot_window.py deleted file mode 100644 index a7933b253..000000000 --- a/dexbot/views/gen/edit_bot_window.py +++ /dev/null @@ -1,221 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'orig/edit_bot_window.ui' -# -# Created by: PyQt5 UI code generator 5.10 -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore, QtGui, QtWidgets - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName("Dialog") - Dialog.resize(400, 430) - self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) - self.verticalLayout.setObjectName("verticalLayout") - self.groupBox = QtWidgets.QGroupBox(Dialog) - self.groupBox.setObjectName("groupBox") - self.formLayout_3 = QtWidgets.QFormLayout(self.groupBox) - self.formLayout_3.setObjectName("formLayout_3") - self.strategy_label = QtWidgets.QLabel(self.groupBox) - self.strategy_label.setMinimumSize(QtCore.QSize(110, 0)) - self.strategy_label.setMaximumSize(QtCore.QSize(110, 16777215)) - self.strategy_label.setObjectName("strategy_label") - self.formLayout_3.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.strategy_label) - self.strategy_input = QtWidgets.QComboBox(self.groupBox) - self.strategy_input.setObjectName("strategy_input") - self.formLayout_3.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.strategy_input) - self.bot_name_label = QtWidgets.QLabel(self.groupBox) - self.bot_name_label.setMinimumSize(QtCore.QSize(110, 0)) - self.bot_name_label.setMaximumSize(QtCore.QSize(110, 16777215)) - self.bot_name_label.setObjectName("bot_name_label") - self.formLayout_3.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.bot_name_label) - self.bot_name_input = QtWidgets.QLineEdit(self.groupBox) - self.bot_name_input.setObjectName("bot_name_input") - self.formLayout_3.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.bot_name_input) - self.base_asset_label = QtWidgets.QLabel(self.groupBox) - self.base_asset_label.setMinimumSize(QtCore.QSize(110, 0)) - self.base_asset_label.setMaximumSize(QtCore.QSize(110, 16777215)) - self.base_asset_label.setObjectName("base_asset_label") - self.formLayout_3.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.base_asset_label) - self.quote_asset_label = QtWidgets.QLabel(self.groupBox) - self.quote_asset_label.setMinimumSize(QtCore.QSize(110, 0)) - self.quote_asset_label.setMaximumSize(QtCore.QSize(110, 16777215)) - self.quote_asset_label.setObjectName("quote_asset_label") - self.formLayout_3.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.quote_asset_label) - self.quote_asset_input = QtWidgets.QLineEdit(self.groupBox) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.quote_asset_input.sizePolicy().hasHeightForWidth()) - self.quote_asset_input.setSizePolicy(sizePolicy) - self.quote_asset_input.setMaximumSize(QtCore.QSize(80, 16777215)) - self.quote_asset_input.setObjectName("quote_asset_input") - self.formLayout_3.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.quote_asset_input) - self.base_asset_input = QtWidgets.QComboBox(self.groupBox) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.base_asset_input.sizePolicy().hasHeightForWidth()) - self.base_asset_input.setSizePolicy(sizePolicy) - self.base_asset_input.setMinimumSize(QtCore.QSize(105, 0)) - self.base_asset_input.setEditable(True) - self.base_asset_input.setObjectName("base_asset_input") - self.formLayout_3.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.base_asset_input) - self.verticalLayout.addWidget(self.groupBox) - self.groupBox_2 = QtWidgets.QGroupBox(Dialog) - self.groupBox_2.setObjectName("groupBox_2") - self.formLayout_2 = QtWidgets.QFormLayout(self.groupBox_2) - self.formLayout_2.setRowWrapPolicy(QtWidgets.QFormLayout.WrapLongRows) - self.formLayout_2.setObjectName("formLayout_2") - self.account_label = QtWidgets.QLabel(self.groupBox_2) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.account_label.sizePolicy().hasHeightForWidth()) - self.account_label.setSizePolicy(sizePolicy) - self.account_label.setMinimumSize(QtCore.QSize(110, 0)) - self.account_label.setMaximumSize(QtCore.QSize(110, 16777215)) - self.account_label.setObjectName("account_label") - self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.account_label) - self.account_name = QtWidgets.QLabel(self.groupBox_2) - self.account_name.setText("") - self.account_name.setObjectName("account_name") - self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.account_name) - self.verticalLayout.addWidget(self.groupBox_2) - self.groupBox_3 = QtWidgets.QGroupBox(Dialog) - self.groupBox_3.setObjectName("groupBox_3") - self.formLayout = QtWidgets.QFormLayout(self.groupBox_3) - self.formLayout.setObjectName("formLayout") - self.amount_label = QtWidgets.QLabel(self.groupBox_3) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.amount_label.sizePolicy().hasHeightForWidth()) - self.amount_label.setSizePolicy(sizePolicy) - self.amount_label.setMinimumSize(QtCore.QSize(110, 0)) - self.amount_label.setMaximumSize(QtCore.QSize(110, 16777215)) - self.amount_label.setObjectName("amount_label") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.amount_label) - self.amount_input = QtWidgets.QDoubleSpinBox(self.groupBox_3) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.amount_input.sizePolicy().hasHeightForWidth()) - self.amount_input.setSizePolicy(sizePolicy) - self.amount_input.setMinimumSize(QtCore.QSize(140, 0)) - self.amount_input.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) - self.amount_input.setDecimals(8) - self.amount_input.setMaximum(999999999.999) - self.amount_input.setObjectName("amount_input") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.amount_input) - self.center_price_label = QtWidgets.QLabel(self.groupBox_3) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.center_price_label.sizePolicy().hasHeightForWidth()) - self.center_price_label.setSizePolicy(sizePolicy) - self.center_price_label.setMinimumSize(QtCore.QSize(110, 0)) - self.center_price_label.setMaximumSize(QtCore.QSize(110, 16777215)) - self.center_price_label.setObjectName("center_price_label") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.center_price_label) - self.center_price_input = QtWidgets.QDoubleSpinBox(self.groupBox_3) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.center_price_input.sizePolicy().hasHeightForWidth()) - self.center_price_input.setSizePolicy(sizePolicy) - self.center_price_input.setMinimumSize(QtCore.QSize(140, 0)) - self.center_price_input.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) - self.center_price_input.setAccelerated(False) - self.center_price_input.setProperty("showGroupSeparator", False) - self.center_price_input.setDecimals(8) - self.center_price_input.setMinimum(-999999999.999) - self.center_price_input.setMaximum(999999999.999) - self.center_price_input.setObjectName("center_price_input") - self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.center_price_input) - self.spread_label = QtWidgets.QLabel(self.groupBox_3) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.spread_label.sizePolicy().hasHeightForWidth()) - self.spread_label.setSizePolicy(sizePolicy) - self.spread_label.setMinimumSize(QtCore.QSize(110, 0)) - self.spread_label.setMaximumSize(QtCore.QSize(110, 16777215)) - self.spread_label.setObjectName("spread_label") - self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.spread_label) - self.spread_input = QtWidgets.QDoubleSpinBox(self.groupBox_3) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.spread_input.sizePolicy().hasHeightForWidth()) - self.spread_input.setSizePolicy(sizePolicy) - self.spread_input.setMinimumSize(QtCore.QSize(140, 0)) - self.spread_input.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) - self.spread_input.setMaximum(100000.0) - self.spread_input.setProperty("value", 5.0) - self.spread_input.setObjectName("spread_input") - self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.spread_input) - self.verticalLayout.addWidget(self.groupBox_3) - spacerItem = QtWidgets.QSpacerItem(20, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout.addItem(spacerItem) - self.widget = QtWidgets.QWidget(Dialog) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.widget.sizePolicy().hasHeightForWidth()) - self.widget.setSizePolicy(sizePolicy) - self.widget.setObjectName("widget") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.widget) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - spacerItem1 = QtWidgets.QSpacerItem(179, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_2.addItem(spacerItem1) - self.cancel_button = QtWidgets.QPushButton(self.widget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.cancel_button.sizePolicy().hasHeightForWidth()) - self.cancel_button.setSizePolicy(sizePolicy) - self.cancel_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.cancel_button.setObjectName("cancel_button") - self.horizontalLayout_2.addWidget(self.cancel_button) - self.save_button = QtWidgets.QPushButton(self.widget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.save_button.sizePolicy().hasHeightForWidth()) - self.save_button.setSizePolicy(sizePolicy) - self.save_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.save_button.setObjectName("save_button") - self.horizontalLayout_2.addWidget(self.save_button) - self.verticalLayout.addWidget(self.widget) - self.strategy_label.setBuddy(self.strategy_input) - self.bot_name_label.setBuddy(self.bot_name_input) - self.quote_asset_label.setBuddy(self.quote_asset_input) - self.amount_label.setBuddy(self.amount_input) - self.center_price_label.setBuddy(self.center_price_input) - self.spread_label.setBuddy(self.spread_input) - - self.retranslateUi(Dialog) - self.strategy_input.setCurrentIndex(-1) - QtCore.QMetaObject.connectSlotsByName(Dialog) - - def retranslateUi(self, Dialog): - _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "DEXBot - Edit Bot")) - self.groupBox.setTitle(_translate("Dialog", "Bot Details")) - self.strategy_label.setText(_translate("Dialog", "Strategy")) - self.bot_name_label.setText(_translate("Dialog", "Bot Name")) - self.base_asset_label.setText(_translate("Dialog", "Base Asset")) - self.quote_asset_label.setText(_translate("Dialog", "Quote Asset")) - self.groupBox_2.setTitle(_translate("Dialog", "Bitshares Account Details")) - self.account_label.setText(_translate("Dialog", "Account")) - self.groupBox_3.setTitle(_translate("Dialog", "Bot Parameters")) - self.amount_label.setText(_translate("Dialog", "Amount")) - self.center_price_label.setText(_translate("Dialog", "Center Price")) - self.spread_label.setText(_translate("Dialog", "Spread")) - self.spread_input.setSuffix(_translate("Dialog", "%")) - self.cancel_button.setText(_translate("Dialog", "Cancel")) - self.save_button.setText(_translate("Dialog", "Save")) - diff --git a/dexbot/views/gen/notice_window.py b/dexbot/views/gen/notice_window.py deleted file mode 100644 index 2d10c0a95..000000000 --- a/dexbot/views/gen/notice_window.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'dexbot/views/orig/notice_window.ui' -# -# Created by: PyQt5 UI code generator 5.9.2 -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore, QtGui, QtWidgets - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName("Dialog") - Dialog.resize(600, 107) - self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) - self.verticalLayout.setObjectName("verticalLayout") - self.notice_label = QtWidgets.QLabel(Dialog) - self.notice_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.notice_label.setText("") - self.notice_label.setAlignment(QtCore.Qt.AlignCenter) - self.notice_label.setObjectName("notice_label") - self.verticalLayout.addWidget(self.notice_label) - self.widget = QtWidgets.QWidget(Dialog) - self.widget.setObjectName("widget") - self.horizontalLayout = QtWidgets.QHBoxLayout(self.widget) - self.horizontalLayout.setObjectName("horizontalLayout") - self.ok_button = QtWidgets.QPushButton(self.widget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.ok_button.sizePolicy().hasHeightForWidth()) - self.ok_button.setSizePolicy(sizePolicy) - self.ok_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.ok_button.setObjectName("ok_button") - self.horizontalLayout.addWidget(self.ok_button) - self.verticalLayout.addWidget(self.widget) - - self.retranslateUi(Dialog) - QtCore.QMetaObject.connectSlotsByName(Dialog) - - def retranslateUi(self, Dialog): - _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "Notice")) - self.ok_button.setText(_translate("Dialog", "OK")) - diff --git a/dexbot/views/gen/unlock_wallet_window.py b/dexbot/views/gen/unlock_wallet_window.py deleted file mode 100644 index 19ed4c3b0..000000000 --- a/dexbot/views/gen/unlock_wallet_window.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'dexbot/views/orig/unlock_wallet_window.ui' -# -# Created by: PyQt5 UI code generator 5.9.2 -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore, QtGui, QtWidgets - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName("Dialog") - Dialog.resize(473, 126) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(Dialog.sizePolicy().hasHeightForWidth()) - Dialog.setSizePolicy(sizePolicy) - Dialog.setModal(True) - self.verticalLayout = QtWidgets.QVBoxLayout(Dialog) - self.verticalLayout.setObjectName("verticalLayout") - self.helper_text = QtWidgets.QLabel(Dialog) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Maximum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.helper_text.sizePolicy().hasHeightForWidth()) - self.helper_text.setSizePolicy(sizePolicy) - self.helper_text.setAlignment(QtCore.Qt.AlignCenter) - self.helper_text.setObjectName("helper_text") - self.verticalLayout.addWidget(self.helper_text) - self.widget = QtWidgets.QWidget(Dialog) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.widget.sizePolicy().hasHeightForWidth()) - self.widget.setSizePolicy(sizePolicy) - self.widget.setMaximumSize(QtCore.QSize(16777215, 126)) - self.widget.setObjectName("widget") - self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.widget) - self.horizontalLayout_3.setContentsMargins(-1, 5, -1, 5) - self.horizontalLayout_3.setObjectName("horizontalLayout_3") - self.form_wrap = QtWidgets.QWidget(self.widget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.form_wrap.sizePolicy().hasHeightForWidth()) - self.form_wrap.setSizePolicy(sizePolicy) - self.form_wrap.setObjectName("form_wrap") - self.formLayout = QtWidgets.QFormLayout(self.form_wrap) - self.formLayout.setLabelAlignment(QtCore.Qt.AlignCenter) - self.formLayout.setFormAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignTop) - self.formLayout.setContentsMargins(-1, -1, -1, 0) - self.formLayout.setObjectName("formLayout") - self.password_label = QtWidgets.QLabel(self.form_wrap) - self.password_label.setObjectName("password_label") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.password_label) - self.password_input = QtWidgets.QLineEdit(self.form_wrap) - self.password_input.setMinimumSize(QtCore.QSize(200, 0)) - self.password_input.setEchoMode(QtWidgets.QLineEdit.Password) - self.password_input.setObjectName("password_input") - self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.password_input) - self.ok_button = QtWidgets.QPushButton(self.form_wrap) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.ok_button.sizePolicy().hasHeightForWidth()) - self.ok_button.setSizePolicy(sizePolicy) - self.ok_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) - self.ok_button.setObjectName("ok_button") - self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.ok_button) - self.horizontalLayout_3.addWidget(self.form_wrap) - self.verticalLayout.addWidget(self.widget) - self.password_label.setBuddy(self.password_input) - - self.retranslateUi(Dialog) - QtCore.QMetaObject.connectSlotsByName(Dialog) - - def retranslateUi(self, Dialog): - _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "DEXBot - Unlock wallet")) - self.helper_text.setText(_translate("Dialog", "Please enter your wallet password before continuing.")) - self.password_label.setText(_translate("Dialog", "Wallet password")) - self.ok_button.setText(_translate("Dialog", "OK")) - diff --git a/dexbot/views/notice.py b/dexbot/views/notice.py index 2c6952797..b04fa1d02 100644 --- a/dexbot/views/notice.py +++ b/dexbot/views/notice.py @@ -1,6 +1,6 @@ -from PyQt5 import QtWidgets +from .ui.notice_window_ui import Ui_Dialog -from dexbot.views.gen.notice_window import Ui_Dialog +from PyQt5 import QtWidgets class NoticeDialog(QtWidgets.QDialog): diff --git a/dexbot/views/ui/__init__.py b/dexbot/views/ui/__init__.py new file mode 100644 index 000000000..79f6f6feb --- /dev/null +++ b/dexbot/views/ui/__init__.py @@ -0,0 +1,3 @@ +# This package includes the .ui files and the generated *_ui.py files. +# *_ui.py files are generated and should not be edited. The files are automatically generated when +# `python setup.py install` is ran, or by running `python setup.py build_ui` diff --git a/dexbot/views/orig/bot_item_widget.ui b/dexbot/views/ui/bot_item_widget.ui similarity index 100% rename from dexbot/views/orig/bot_item_widget.ui rename to dexbot/views/ui/bot_item_widget.ui diff --git a/dexbot/views/orig/bot_list_window.ui b/dexbot/views/ui/bot_list_window.ui similarity index 100% rename from dexbot/views/orig/bot_list_window.ui rename to dexbot/views/ui/bot_list_window.ui diff --git a/dexbot/views/orig/confirmation_window.ui b/dexbot/views/ui/confirmation_window.ui similarity index 100% rename from dexbot/views/orig/confirmation_window.ui rename to dexbot/views/ui/confirmation_window.ui diff --git a/dexbot/views/orig/create_bot_window.ui b/dexbot/views/ui/create_bot_window.ui similarity index 100% rename from dexbot/views/orig/create_bot_window.ui rename to dexbot/views/ui/create_bot_window.ui diff --git a/dexbot/views/orig/create_wallet_window.ui b/dexbot/views/ui/create_wallet_window.ui similarity index 100% rename from dexbot/views/orig/create_wallet_window.ui rename to dexbot/views/ui/create_wallet_window.ui diff --git a/dexbot/views/orig/edit_bot_window.ui b/dexbot/views/ui/edit_bot_window.ui similarity index 100% rename from dexbot/views/orig/edit_bot_window.ui rename to dexbot/views/ui/edit_bot_window.ui diff --git a/dexbot/views/orig/notice_window.ui b/dexbot/views/ui/notice_window.ui similarity index 100% rename from dexbot/views/orig/notice_window.ui rename to dexbot/views/ui/notice_window.ui diff --git a/dexbot/views/orig/unlock_wallet_window.ui b/dexbot/views/ui/unlock_wallet_window.ui similarity index 100% rename from dexbot/views/orig/unlock_wallet_window.ui rename to dexbot/views/ui/unlock_wallet_window.ui diff --git a/dexbot/views/unlock_wallet.py b/dexbot/views/unlock_wallet.py index e09b4460f..f67efecc4 100644 --- a/dexbot/views/unlock_wallet.py +++ b/dexbot/views/unlock_wallet.py @@ -1,7 +1,7 @@ -from PyQt5 import QtWidgets +from .ui.unlock_wallet_window_ui import Ui_Dialog +from .notice import NoticeDialog -from dexbot.views.gen.unlock_wallet_window import Ui_Dialog -from dexbot.views.notice import NoticeDialog +from PyQt5 import QtWidgets class UnlockWalletView(QtWidgets.QDialog): diff --git a/pyuic.json b/pyuic.json new file mode 100644 index 000000000..1bf28f2a6 --- /dev/null +++ b/pyuic.json @@ -0,0 +1,11 @@ +{ + "files": [ + ["dexbot/views/ui/*.ui", "dexbot/views/ui/"], + ["dexbot/resources/*.qrc", "dexbot/resources/"] + ], + "hooks": [], + "pyrcc": "pyrcc5", + "pyrcc_options": "", + "pyuic": "pyuic5", + "pyuic_options": "--import-from=dexbot.resources" +} \ No newline at end of file diff --git a/setup.py b/setup.py index 8e3082346..1402d20a9 100755 --- a/setup.py +++ b/setup.py @@ -1,9 +1,22 @@ #!/usr/bin/env python -from setuptools import setup, find_packages +from setuptools import setup +from setuptools.command.install import install + +from pyqt_distutils.build_ui import build_ui VERSION = '0.1.0' + +class InstallCommand(install): + """Customized setuptools install command - converts .ui and .qrc files to .py files + """ + def run(self): + # Workaround for https://github.com/pypa/setuptools/issues/456 + self.do_egg_install() + self.run_command('build_ui') + + setup( name='dexbot', version=VERSION, @@ -26,6 +39,10 @@ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', ], + cmdclass={ + 'build_ui': build_ui, + 'install': InstallCommand, + }, entry_points={ 'console_scripts': [ 'dexbot = dexbot.cli:main', @@ -39,6 +56,7 @@ "sqlalchemy", "appdirs", "pyqt5", + 'pyqt-distutils', "ruamel.yaml" ], dependency_links=[ From 440a071fd94f04631a530284d67ce2f987889206 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 5 Mar 2018 13:24:56 +0200 Subject: [PATCH 0091/1846] Change input width in edit bot and create bot views --- dexbot/views/ui/create_bot_window.ui | 214 +++++++++++++-------------- dexbot/views/ui/edit_bot_window.ui | 2 +- 2 files changed, 108 insertions(+), 108 deletions(-) diff --git a/dexbot/views/ui/create_bot_window.ui b/dexbot/views/ui/create_bot_window.ui index 80c457b81..1d2bc26bc 100644 --- a/dexbot/views/ui/create_bot_window.ui +++ b/dexbot/views/ui/create_bot_window.ui @@ -74,6 +74,112 @@ + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Bitshares Account Details + + + + QFormLayout::WrapLongRows + + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Account + + + account_input + + + + + + + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Private Active Key + + + false + + + true + + + private_key_input + + + + + + + + 0 + 0 + + + + QLineEdit::Password + + + false + + + + + + @@ -233,7 +339,7 @@ - 140 + 151 0 @@ -254,99 +360,6 @@ - - - - Bitshares Account Details - - - - QFormLayout::WrapLongRows - - - - - - 0 - 0 - - - - - 110 - 0 - - - - - 110 - 16777215 - - - - Account - - - account_input - - - - - - - - - - - 0 - 0 - - - - - 110 - 0 - - - - - 110 - 16777215 - - - - Private Active Key - - - false - - - true - - - private_key_input - - - - - - - - 0 - 0 - - - - QLineEdit::Password - - - false - - - - - - @@ -486,19 +499,6 @@ - - - - Qt::Vertical - - - - 20 - 40 - - - - diff --git a/dexbot/views/ui/edit_bot_window.ui b/dexbot/views/ui/edit_bot_window.ui index 7454075eb..b986bf84a 100644 --- a/dexbot/views/ui/edit_bot_window.ui +++ b/dexbot/views/ui/edit_bot_window.ui @@ -356,7 +356,7 @@ - 140 + 151 0 From d16d28173d94a24690d76ca2d0607adf7e83cf14 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 5 Mar 2018 14:01:32 +0200 Subject: [PATCH 0092/1846] Fix code styling problems and reword the confirmation dialog --- dexbot/views/edit_bot.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/dexbot/views/edit_bot.py b/dexbot/views/edit_bot.py index 1a2b49320..a8f896d8e 100644 --- a/dexbot/views/edit_bot.py +++ b/dexbot/views/edit_bot.py @@ -43,15 +43,16 @@ def validate_form(self): error_text = '' 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.'.format(bot_name) + '\n' + error_text += 'Bot name needs to be unique. "{}" is already in use.\n'.format(bot_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 error_text: dialog = NoticeDialog(error_text) @@ -60,14 +61,13 @@ def validate_form(self): else: return True - def handle_save_dialog(self): - dialog = ConfirmationDialog('Saving bot will recreate it: cancel all current orders, stop it, start again' - ' and create new orders based on new settings. ' - '\n Are you sure you want to save bot?') + @staticmethod + def handle_save_dialog(): + dialog = ConfirmationDialog('Saving bot will recreate it: cancel all current orders ' + 'and create new orders based on new settings.\n' + 'Are you sure you want to save the bot?') return dialog.exec_() - - def handle_save(self): if not self.validate_form(): return From 47838b8578bf2b81dc35ba1d0c03d9a554ddc491 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 5 Mar 2018 14:17:26 +0200 Subject: [PATCH 0093/1846] Change the dialog text of edit bot --- dexbot/views/edit_bot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dexbot/views/edit_bot.py b/dexbot/views/edit_bot.py index a8f896d8e..3246c30d7 100644 --- a/dexbot/views/edit_bot.py +++ b/dexbot/views/edit_bot.py @@ -63,8 +63,7 @@ def validate_form(self): @staticmethod def handle_save_dialog(): - dialog = ConfirmationDialog('Saving bot will recreate it: cancel all current orders ' - 'and create new orders based on new settings.\n' + dialog = ConfirmationDialog('Saving the bot will cancel all the current orders.\n' 'Are you sure you want to save the bot?') return dialog.exec_() From 4028200c4d811e7c623bd2e7afaeeb5c5298fc9a Mon Sep 17 00:00:00 2001 From: Nikolay Date: Mon, 5 Mar 2018 15:53:48 +0200 Subject: [PATCH 0094/1846] Add hotfix token none --- dexbot/storage.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dexbot/storage.py b/dexbot/storage.py index 44500692a..cfbec7bb6 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -96,7 +96,8 @@ def __init__(self): def run(self): for func, args, token in iter(self.task_queue.get, None): - args = args+(token,) + if token is not None: + args = args+(token,) func(*args) def get_result(self, token): From e5f35de8acd1ef54995ebffc21149fbc0402a3c7 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 5 Mar 2018 16:47:49 +0200 Subject: [PATCH 0095/1846] Fix orders getting purged on pause bot --- dexbot/bot.py | 2 ++ dexbot/controllers/main_controller.py | 9 --------- dexbot/views/bot_item.py | 1 - 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/dexbot/bot.py b/dexbot/bot.py index 786088eb6..9b73b22ef 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -128,6 +128,8 @@ def run(self): self.notify.listen() def stop(self): + for bot in self.bots: + self.bots[bot].cancel_all() self.notify.websocket.close() def remove_bot(self): diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index b453603d4..f2dc065e6 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -2,8 +2,6 @@ from ruamel.yaml import YAML from bitshares.instance import set_shared_bitshares_instance -from dexbot.basestrategy import BaseStrategy - class MainController: @@ -88,10 +86,3 @@ def remove_bot_config(bot_name): with open("config.yml", "w") as f: yaml.dump(config, f) - - def pause_bot(self, bot_name): - config = self.get_bot_config(bot_name) - strategy = BaseStrategy(config, bot_name) - strategy.purge() - - diff --git a/dexbot/views/bot_item.py b/dexbot/views/bot_item.py index d89a167e7..95144f6f2 100644 --- a/dexbot/views/bot_item.py +++ b/dexbot/views/bot_item.py @@ -49,7 +49,6 @@ def start_bot(self): def pause_bot(self): self.running = False - self.main_ctrl.pause_bot(self.botname) self.pause_button.hide() self.play_button.show() From 2745d9a998ff864d130ef22b65173f1c9f2b9692 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 6 Mar 2018 14:36:39 +0200 Subject: [PATCH 0096/1846] Fix pep8 errors and typos --- dexbot/basestrategy.py | 24 +++++----- dexbot/bot.py | 4 +- dexbot/cli.py | 18 +++++--- dexbot/cli_conf.py | 89 +++++++++++++++++++++----------------- dexbot/find_node.py | 37 ++++++++++------ dexbot/strategies/walls.py | 14 +++--- dexbot/ui.py | 39 ++++++++++------- dexbot/whiptail.py | 59 +++++++++++++------------ setup.py | 2 +- 9 files changed, 164 insertions(+), 122 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index cccb4bafd..240b68d16 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -10,20 +10,20 @@ ConfigElement = collections.namedtuple('ConfigElement','key type default description extra') -# bots need to specify their own configuration values +# Bots need to specify their own configuration values # I want this to be UI-agnostic so a future web or GUI interface can use it too # so each bot can have a class method 'configure' which returns a list of ConfigElement -# named tuples. tuple fields as follows. -# key: the key in the bot config dictionary that gets saved back to config.yml -# type: one of "int", "float", "bool", "string", "choice" -# default: the default value. must be right type. -# description: comments to user, full sentences encouraged -# extra: -# for int & float: a (min, max) tuple -# for string: a regular expression, entries must match it, can be None which equivalent to .* -# for bool, ignored -# for choice: a list of choices, choices are in turn (tag, label) tuples. labels get presented to user, and tag is used -# as the value saved back to the config dict +# named tuples. Tuple fields as follows. +# Key: the key in the bot config dictionary that gets saved back to config.yml +# Type: one of "int", "float", "bool", "string", "choice" +# Default: the default value. must be right type. +# Description: comments to user, full sentences encouraged +# Extra: +# For int & float: a (min, max) tuple +# For string: a regular expression, entries must match it, can be None which equivalent to .* +# For bool, ignored +# For choice: a list of choices, choices are in turn (tag, label) tuples. +# labels get presented to user, and tag is used as the value saved back to the config dict class BaseStrategy(Storage, StateMachine, Events): diff --git a/dexbot/bot.py b/dexbot/bot.py index 0285a7931..c2d590bdd 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -7,8 +7,8 @@ log = logging.getLogger(__name__) # FIXME: currently static list of bot strategies: ? how to enumerate bots available and deploy new bot strategies. -STRATEGIES=[('dexbot.strategies.echo','Echo Test'), - ('dexbot.strategies.follow_orders',"Haywood's Follow Orders")] +STRATEGIES = [('dexbot.strategies.echo', 'Echo Test'), + ('dexbot.strategies.follow_orders', "Haywood's Follow Orders")] class BotInfrastructure(): diff --git a/dexbot/cli.py b/dexbot/cli.py index 7cb257dd7..147c83d75 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -22,7 +22,7 @@ log = logging.getLogger(__name__) -# inital logging before proper setup. +# Initial logging before proper setup. logging.basicConfig( level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s' @@ -65,14 +65,15 @@ def run(ctx): bot = BotInfrastructure(ctx.config) if ctx.obj['systemd']: try: - import sdnotify # a soft dependency on sdnotify -- don't crash on non-systemd systems + import sdnotify # A soft dependency on sdnotify -- don't crash on non-systemd systems n = sdnotify.SystemdNotifier() n.notify("READY=1") except: warning("sdnotify not available") 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 + @main.command() @click.pass_context @@ -84,13 +85,15 @@ def configure(ctx): with open(ctx.obj["configfile"]) as fd: config = yaml.load(fd) else: - config = {} + config = {} configure_dexbot(config) cfg_file = ctx.obj["configfile"] - if not "/" in cfg_file: # save to home directory unless user wants something else + if "/" not in cfg_file: # Save to home directory unless user wants something else cfg_file = os.path.expanduser("~/"+cfg_file) - with open(cfg_file,"w") as fd: - yaml.dump(config,fd,default_flow_style=False) + + with open(cfg_file, "w") as fd: + yaml.dump(config, fd, default_flow_style=False) + click.echo("new configuration saved") if config['systemd_status'] == 'installed': # we are already installed @@ -101,5 +104,6 @@ def configure(ctx): click.echo("starting dexbot daemon") os.system("systemctl --user start dexbot") + if __name__ == '__main__': main() diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 07a73cb20..02a6afe37 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -1,10 +1,10 @@ """ A module to provide an interactive text-based tool for dexbot configuration -The result is takemachine can be run without having to hand-edit config files. +The result is dexbot can be run without having to hand-edit config files. If systemd is detected it will offer to install a user service unit (under ~/.local/share/systemd This requires a per-user systemd process to be runnng -Requires the 'whiptail' tool: so UNIX-like sytems only +Requires the 'whiptail' tool: so UNIX-like systems only Note there is some common cross-UI configuration stuff: look in basestrategy.py It's expected GUI/web interfaces will be re-implementing code in this file, but they should @@ -14,14 +14,19 @@ """ -import importlib, os, os.path, sys, collections, re, tempfile, shutil +import importlib +import os +import os.path +import sys +import re + from dexbot.bot import STRATEGIES from dexbot.whiptail import get_whiptail from dexbot.find_node import start_pings, best_node -SYSTEMD_SERVICE_NAME=os.path.expanduser("~/.local/share/systemd/user/dexbot.service") +SYSTEMD_SERVICE_NAME = os.path.expanduser("~/.local/share/systemd/user/dexbot.service") -SYSTEMD_SERVICE_FILE=""" +SYSTEMD_SERVICE_FILE = """ [Unit] Description=Dexbot @@ -37,27 +42,28 @@ """ -def select_choice(current,choices): +def select_choice(current, choices): """for the radiolist, get us a list with the current value selected""" - return [(tag,text,(current == tag and "ON") or "OFF") for tag,text in choices] + return [(tag, text, (current == tag and "ON") or "OFF") for tag, text in choices] + -def process_config_element(elem,d,config): +def process_config_element(elem, d, config): """ process an item of configuration metadata display a widget as appropriate d: the Dialog object config: the config dctionary for this bot """ if elem.type == "string": - txt = d.prompt(elem.description,config.get(elem.key,elem.default)) + txt = d.prompt(elem.description, config.get(elem.key, elem.default)) if elem.extra: - while not re.match(elem.extra,txt): + while not re.match(elem.extra, txt): d.alert("The value is not valid") - txt = d.prompt(elem.description,config.get(elem.key,elem.default)) + txt = d.prompt(elem.description, config.get(elem.key,elem.default)) config[elem.key] = txt if elem.type == "bool": config[elem.key] = d.confirm(elem.description) if elem.type in ("float", "int"): - txt = d.prompt(elem.description,config.get(elem.key,str(elem.default))) + txt = d.prompt(elem.description, config.get(elem.key, str(elem.default))) while True: try: if elem.type == "int": @@ -72,13 +78,14 @@ def process_config_element(elem,d,config): break except ValueError: d.alert("Not a valid value") - txt = d.prompt(elem.description,config.get(elem.key,str(elem.default))) + txt = d.prompt(elem.description, config.get(elem.key, str(elem.default))) config[elem.key] = val if elem.type == "choice": - config[elem.key] = d.radiolist(elem.description,select_choice(config.get(elem.key,elem.default),elem.extra)) - -def setup_systemd(d,config): - if config.get("systemd_status","install") == "reject": + config[elem.key] = d.radiolist(elem.description, select_choice(config.get(elem.key, elem.default), elem.extra)) + + +def setup_systemd(d, config): + if config.get("systemd_status", "install") == "reject": return # don't nag user if previously said no if not os.path.exists("/etc/systemd"): return # no working systemd @@ -88,23 +95,26 @@ def setup_systemd(d,config): config["systemd_status"] = "installed" return if d.confirm("Do you want to install dexbot as a background (daemon) process?"): - for i in ["~/.local","~/.local/share","~/.local/share/systemd","~/.local/share/systemd/user"]: + for i in ["~/.local", "~/.local/share", "~/.local/share/systemd", "~/.local/share/systemd/user"]: j = os.path.expanduser(i) if not os.path.exists(j): os.mkdir(j) - passwd = d.prompt("The wallet password entered with uptick\nNOTE: this will be saved on disc so the bot can run unattended. This means anyone with access to this computer's file can spend all your money",password=True) + passwd = d.prompt("The wallet password entered with uptick\n" + "NOTE: this will be saved on disc so the bot can run unattended. " + "This means anyone with access to this computer's file can spend all your money", + password=True) fd = os.open(SYSTEMD_SERVICE_NAME, os.O_WRONLY|os.O_CREAT, 0o600) # because we hold password be restrictive with open(fd, "w") as fp: fp.write(SYSTEMD_SERVICE_FILE.format(exe=sys.argv[0],passwd=passwd,homedir=os.path.expanduser("~"))) config['systemd_status'] = 'install' # signal cli.py to set the unit up after writing config file else: config['systemd_status'] = 'reject' - -def configure_bot(d,bot): - strategy = bot.get('module','dexbot.strategies.echo') - bot['module'] = d.radiolist("Choose a bot strategy",select_choice(strategy,STRATEGIES)) - bot['bot'] = 'Strategy' # its always Strategy now, for backwards compatibilty only + +def configure_bot(d, bot): + strategy = bot.get('module', 'dexbot.strategies.echo') + bot['module'] = d.radiolist("Choose a bot strategy", select_choice(strategy, STRATEGIES)) + bot['bot'] = 'Strategy' # its always Strategy now, for backwards compatibility only # import the bot class but we don't __init__ it here klass = getattr( importlib.import_module(bot["module"]), @@ -114,41 +124,42 @@ def configure_bot(d,bot): configs = klass.configure() if configs: for c in configs: - process_config_element(c,d,bot) + process_config_element(c, d, bot) else: - d.alert("This bot type does not have configuration information. You will have to check the bot code and add configuration values to config.yml if required") + d.alert("This bot type does not have configuration information. " + "You will have to check the bot code and add configuration values to config.yml if required") return bot - - + def configure_dexbot(config): d = get_whiptail() - if not 'node' in config: + if 'node' not in config: # start our best node search in the background ping_results = start_pings() - bots = config.get('bots',{}) + bots = config.get('bots', {}) if len(bots) == 0: txt = d.prompt("Your name for the first bot") - config['bots'] = {txt:configure_bot(d,{})} + config['bots'] = {txt: configure_bot(d, {})} else: - botname = d.menu("Select bot to edit",[(i,i) for i in bots]+[('NEW','New bot')]) + botname = d.menu("Select bot to edit", [(i, i) for i in bots]+[('NEW', 'New bot')]) if botname == 'NEW': txt = d.prompt("Your name for the new bot") - config['bots'][txt] = configure_bot(d,{}) + config['bots'][txt] = configure_bot(d, {}) else: - config['bots'][botname] = configure_bot(d,config['bots'][botname]) + config['bots'][botname] = configure_bot(d, config['bots'][botname]) if not 'node' in config: node = best_node(ping_results) if node: config['node'] = node else: # search failed, ask the user - config['node'] = d.prompt("Search for best BitShares node failed.\n\nPlease enter wss:// url of chosen node.") - setup_systemd(d,config) + config['node'] = d.prompt( + "Search for best BitShares node failed.\n\nPlease enter wss:// url of chosen node." + ) + setup_systemd(d, config) d.clear() return config -if __name__=='__main__': + +if __name__ == '__main__': print(repr(configure({}))) - - diff --git a/dexbot/find_node.py b/dexbot/find_node.py index e146c9e62..265dc369c 100644 --- a/dexbot/find_node.py +++ b/dexbot/find_node.py @@ -1,3 +1,9 @@ +import re +from urllib.parse import urlsplit +from subprocess import Popen, STDOUT, PIPE +from platform import system + + """ Routines for finding the closest node """ @@ -23,31 +29,35 @@ "wss://eu.nodes.bitshares.works", "wss://sg.nodes.bitshares.works"] -import re -from urllib.parse import urlsplit -from subprocess import Popen, STDOUT, PIPE -from platform import system - if system() == 'Windows': - ping_cmd = lambda x: ('ping','-n','5','-w','1500',x) ping_re = re.compile(r'Average = ([\d.]+)ms') else: - ping_cmd = lambda x: ('ping','-c5','-n','-w5','-i0.3',x) ping_re = re.compile(r'min/avg/max/mdev = [\d.]+/([\d.]+)') + +def ping_cmd(x): + if system() == 'Windows': + return 'ping', '-n', '5', '-w', '1500', x + else: + return 'ping', '-c5', '-n', '-w5', '-i0.3', x + + def make_ping_proc(host): host = urlsplit(host).netloc.split(':')[0] - return Popen(ping_cmd(host),stdout=PIPE, stderr=STDOUT, universal_newlines=True) + return Popen(ping_cmd(host), stdout=PIPE, stderr=STDOUT, universal_newlines=True) -def process_ping_result(host,proc): + +def process_ping_result(host, proc): out = proc.communicate()[0] try: - return (float(ping_re.search(out).group(1)),host) + return float(ping_re.search(out).group(1)), host except AttributeError: - return (1000000,host) # hosts that fail are last + return 1000000, host # hosts that fail are last + def start_pings(): - return [(i,make_ping_proc(i)) for i in ALL_NODES] + return [(i, make_ping_proc(i)) for i in ALL_NODES] + def best_node(results): try: @@ -56,5 +66,6 @@ def best_node(results): except: return None -if __name__=='__main__': + +if __name__ == '__main__': print(best_node(start_pings())) diff --git a/dexbot/strategies/walls.py b/dexbot/strategies/walls.py index ac8e2b8d2..c9f2994b3 100644 --- a/dexbot/strategies/walls.py +++ b/dexbot/strategies/walls.py @@ -13,12 +13,14 @@ class Walls(BaseStrategy): @classmethod def configure(cls): return BaseStrategy.configure()+[ - ConfigElement("spread","int",5,"the spread between sell and buy as percentage",(0,100)), - ConfigElement("threshold","int",5,"percentage the feed has to move before we change orders",(0,100)), - ConfigElement("buy","float",0.0,"the default amount to buy",(0.0,None)), - ConfigElement("sell","float",0.0,"the default amount to sell",(0.0,None)), - ConfigElement("blocks","int",20,"number of blocks to wait before re-calculating",(0,10000)), - ConfigElement("dry_run","bool",False,"Dry Run Mode\nIf Yes the bot won't buy or sell anything, just log what it would do.\nIf No, the bot will buy and sell for real.",None) + ConfigElement("spread", "int", 5, "the spread between sell and buy as percentage", (0, 100)), + ConfigElement("threshold", "int", 5, "percentage the feed has to move before we change orders", (0, 100)), + ConfigElement("buy", "float", 0.0, "the default amount to buy", (0.0, None)), + ConfigElement("sell", "float", 0.0, "the default amount to sell", (0.0, None)), + ConfigElement("blocks", "int", 20, "number of blocks to wait before re-calculating", (0, 10000)), + ConfigElement("dry_run", "bool", False, + "Dry Run Mode\nIf Yes the bot won't buy or sell anything, just log what it would do.\n" + "If No, the bot will buy and sell for real.", None) ] def __init__(self, *args, **kwargs): diff --git a/dexbot/ui.py b/dexbot/ui.py index fd6c4a102..449b82e4a 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -1,4 +1,5 @@ -import os, sys +import os +import sys import click import logging import yaml @@ -17,30 +18,38 @@ 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('bot %(botname)s using account %(account)s on %(market)s - %(levelname)s - %(message)s') + formatter2 = logging.Formatter( + 'bot %(botname)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') + # 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' + ) 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 - bot %(botname)s using account %(account)s on %(market)s - %(levelname)s - %(message)s' + ) level = getattr(logging, verbosity.upper()) - # use special format for special bots logger + # Use special format for special bots logger ch = logging.StreamHandler() ch.setFormatter(formatter2) logging.getLogger("dexbot.per_bot").addHandler(ch) logging.getLogger("dexbot.per_bot").setLevel(level) - logging.getLogger("dexbot.per_bot").propagate = False # don't double up with root logger - # set the root logger with basic format + logging.getLogger("dexbot.per_bot").propagate = False # Don't double up with root logger + # Set the root logger with basic format ch = logging.StreamHandler() ch.setFormatter(formatter1) logging.getLogger("dexbot").setLevel(level) logging.getLogger("dexbot").addHandler(ch) - # and don't double up on the root logger + # And don't double up on the root logger logging.getLogger("").handlers = [] # GrapheneAPI logging @@ -86,14 +95,14 @@ def new_func(ctx, *args, **kwargs): pwd = os.environ["UNLOCK"] else: if systemd: - # no user available to interact with + # No user available to interact with log.critical("Passphrase not available, exiting") - sys.exit(78) # 'configuation error' in systexits.h + sys.exit(78) # 'configuration error' in systexits.h pwd = click.prompt("Current Wallet Passphrase", hide_input=True) ctx.bitshares.wallet.unlock(pwd) else: if systemd: - # no user available to interact with + # No user available to interact with log.critical("Wallet not installed, cannot run") sys.exit(78) click.echo("No wallet installed yet. Creating ...") diff --git a/dexbot/whiptail.py b/dexbot/whiptail.py index bae67651f..834d2a58b 100644 --- a/dexbot/whiptail.py +++ b/dexbot/whiptail.py @@ -1,8 +1,11 @@ from __future__ import print_function import sys -import shlex, shutil +import shlex +import shutil import itertools import click +import os +import tempfile from subprocess import Popen, PIPE from collections import namedtuple @@ -18,6 +21,7 @@ def flatten(data): return list(itertools.chain.from_iterable(data)) + class Whiptail: def __init__(self, title='', backtitle='', height=20, width=60, @@ -39,26 +43,26 @@ def run(self, control, msg, extra=(), exit_on=(1, 255)): if self.auto_exit and p.returncode in exit_on: print('User cancelled operation.') sys.exit(p.returncode) - return Response(p.returncode, str(err,'utf-8','ignore')) + return Response(p.returncode, str(err, 'utf-8', 'ignore')) def prompt(self, msg, default='', password=False): control = 'passwordbox' if password else 'inputbox' return self.run(control, msg, [default]).value - + def confirm(self, msg, default='yes'): defaultno = '--defaultno' if default == 'no' else '' return self.run('yesno', msg, [defaultno], [255]).returncode == 0 - + def alert(self, msg): self.run('msgbox', msg) - + def view_file(self, path): self.run('textbox', path, ['--scrolltext']) - + def calc_height(self, msg): height_offset = 8 if msg else 7 return [str(self.height - height_offset)] - + def menu(self, msg='', items=(), prefix=' - '): if isinstance(items[0], str): items = [(i, '') for i in items] @@ -66,7 +70,7 @@ def menu(self, msg='', items=(), prefix=' - '): items = [(k, prefix + v) for k, v in items] extra = self.calc_height(msg) + flatten(items) return self.run('menu', msg, extra).value - + def showlist(self, control, msg, items, prefix): if isinstance(items[0], str): items = [(i, '', 'OFF') for i in items] @@ -74,13 +78,13 @@ def showlist(self, control, msg, items, prefix): items = [(k, prefix + v, s) for k, v, s in items] extra = self.calc_height(msg) + flatten(items) return shlex.split(self.run(control, msg, extra).value) - + def radiolist(self, msg='', items=(), prefix=' - '): return self.showlist('radiolist', msg, items, prefix)[0] - + def checklist(self, msg='', items=(), prefix=' - '): return self.showlist('checklist', msg, items, prefix) - + def view_text(self, text): """Whiptail wants a file but we want to provide a text string""" fd, nam = tempfile.mkstemp() @@ -90,11 +94,11 @@ def view_text(self, text): self.view_file(nam) os.unlink(nam) - def clear(self): # tidy up the screen click.clear() - + + class NoWhiptail: """ Imitates the interface of whiptail but uses click only @@ -104,49 +108,50 @@ class NoWhiptail: """ def prompt(self, msg, default='', password=False): - return click.prompt(msg,default=default,hide_input=password) + return click.prompt(msg, default=default, hide_input=password) def confirm(self, msg, default='yes'): - return click.confirm(msg,default=(default=='yes')) - + return click.confirm(msg, default=(default == 'yes')) + def alert(self, msg): click.echo( "[" + click.style("alert", fg="yellow") + "] " + msg ) - + def view_text(self, text): click.echo_via_pager(text) - def menu(self, msg='', items=(), prefix=' - ', default=0): + def menu(self, msg='', items=(), default=0): click.echo(msg+'\n') - if type(items) is dict: items = list(items.items()) + if type(items) is dict: + items = list(items.items()) i = 1 for k, v in items: click.echo("{:>2}) {}".format(i, v)) i += 1 click.echo("\n") - ret = click.prompt("Your choice:",type=int,default=default+1) + ret = click.prompt("Your choice:", type=int, default=default+1) ret = items[ret-1] return ret[0] - - def radiolist(self, msg='', items=(), prefix=' - '): + + def radiolist(self, msg='', items=()): d = 0 default = 0 for k, v, s in items: if s == "ON": default = d d += 1 - return self.menu(msg,[(k,v) for k,v,s in items],default=default) - + return self.menu(msg, [(k, v) for k, v, s in items], default=default) def clear(self): - pass # dont tidy the screen - + pass # Don't tidy the screen + + def get_whiptail(): if shutil.which("whiptail"): d = Whiptail() else: - d = NoWhiptail() # use our own fake whiptail + d = NoWhiptail() # Use our own fake whiptail return d diff --git a/setup.py b/setup.py index 242e60479..4e255c84e 100755 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ "sqlalchemy", "appdirs", "pyqt5", - "sdnotify" + "sdnotify" ], include_package_data=True, ) From bdb8dadb33c1f422b170e754709b38554819cfb7 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 1 Mar 2018 10:34:29 +0200 Subject: [PATCH 0097/1846] 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 0098/1846] - 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 0099/1846] - 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 0100/1846] - 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 0101/1846] - 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 c0e65c3aaf74af2aa7c9273a3423418303c69c6e Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 7 Mar 2018 14:39:29 +0200 Subject: [PATCH 0102/1846] Fix pep8 errors --- dexbot/basestrategy.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index f1b444868..152786283 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -75,7 +75,7 @@ class BaseStrategy(Storage, StateMachine, Events): ] @classmethod - def configure(kls): + def configure(cls): """ Return a list of ConfigElement objects defining the configuration values for this class @@ -87,8 +87,10 @@ def configure(kls): """ # these configs are common to all bots return [ - ConfigElement("account","string","","BitShares account name for the bot to operate with",""), - ConfigElement("market","string","USD:BTS","BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"","[A-Z]+:[A-Z]+") + ConfigElement("account", "string", "", "BitShares account name for the bot to operate with", ""), + ConfigElement("market", "string", "USD:BTS", + "BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"", + "[A-Z]+:[A-Z]+") ] def __init__( From c8119af54ad76711efd76c50c181fc66032fe7ab Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 7 Mar 2018 15:08:03 +0200 Subject: [PATCH 0103/1846] Fix more pep8 errors --- dexbot/cli_conf.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 02a6afe37..097db2621 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -86,12 +86,12 @@ def process_config_element(elem, d, config): def setup_systemd(d, config): if config.get("systemd_status", "install") == "reject": - return # don't nag user if previously said no + return # Don't nag user if previously said no if not os.path.exists("/etc/systemd"): - return # no working systemd + return # No working systemd if os.path.exists(SYSTEMD_SERVICE_NAME): - # dexbot already installed - # so just tell cli.py to quietly restart the daemon + # Dexbot already installed + # So just tell cli.py to quietly restart the daemon config["systemd_status"] = "installed" return if d.confirm("Do you want to install dexbot as a background (daemon) process?"): @@ -106,7 +106,7 @@ def setup_systemd(d, config): fd = os.open(SYSTEMD_SERVICE_NAME, os.O_WRONLY|os.O_CREAT, 0o600) # because we hold password be restrictive with open(fd, "w") as fp: fp.write(SYSTEMD_SERVICE_FILE.format(exe=sys.argv[0],passwd=passwd,homedir=os.path.expanduser("~"))) - config['systemd_status'] = 'install' # signal cli.py to set the unit up after writing config file + config['systemd_status'] = 'install' # Signal cli.py to set the unit up after writing config file else: config['systemd_status'] = 'reject' @@ -147,7 +147,7 @@ def configure_dexbot(config): config['bots'][txt] = configure_bot(d, {}) else: config['bots'][botname] = configure_bot(d, config['bots'][botname]) - if not 'node' in config: + if 'node' not in config: node = best_node(ping_results) if node: config['node'] = node From ac478ca6e29f917dda236d6ccbf3c304a522a7bf Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 8 Mar 2018 13:53:41 +1100 Subject: [PATCH 0104/1846] updatre relevant files from ihaywood3/master fixes config.yml location issue PEP8 syntax throughout --- dexbot/cli.py | 69 +++++++++++++----- dexbot/cli_conf.py | 171 +++++++++++++++++++++++++++++++-------------- dexbot/ui.py | 61 ++++++++++------ dexbot/whiptail.py | 65 +++++++++-------- 4 files changed, 242 insertions(+), 124 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index 7cb257dd7..76c63724e 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -1,8 +1,16 @@ #!/usr/bin/env python3 -import yaml import logging +import os +# we need to do this before importing click +if not "LANG" in os.environ: + os.environ['LANG'] = 'C.UTF-8' import click -import os.path, os, sys +import signal +import os.path +import os +import sys +import appdirs + from .ui import ( verbose, chain, @@ -15,10 +23,10 @@ ) -from dexbot.bot import BotInfrastructure -from dexbot.cli_conf import configure_dexbot -import dexbot.errors as errors - +from .bot import BotInfrastructure +from .cli_conf import configure_dexbot +from . import errors +from . import storage log = logging.getLogger(__name__) @@ -32,7 +40,7 @@ @click.group() @click.option( "--configfile", - default="config.yml", + default=os.path.join(appdirs.user_config_dir("dexbot"),"config.yml"), ) @click.option( '--verbose', @@ -45,6 +53,12 @@ '-d', default=False, help='Run as a daemon from systemd') +@click.option( + '--pidfile', + '-p', + type=str, + default='', + help='File to write PID') @click.pass_context def main(ctx, **kwargs): ctx.obj = {} @@ -61,36 +75,52 @@ 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) + # 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)) + signal.signal(signal.SIGUSR2, lambda x, y: bot.do_next_tick(bot.report_now)) + except ValueError: + log.debug("Cannot set all signals -- not avaiable on this platform") + bot.init_bots() if ctx.obj['systemd']: try: - import sdnotify # a soft dependency on sdnotify -- don't crash on non-systemd systems + import sdnotify # a soft dependency on sdnotify -- don't crash on non-systemd systems n = sdnotify.SystemdNotifier() n.notify("READY=1") - except: - warning("sdnotify not available") - bot.run() + except BaseException: + log.debug("sdnotify not available") + bot.notify.listen() 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 + @main.command() @click.pass_context -@verbose def configure(ctx): """ Interactively configure dexbot """ + cfg_file = ctx.obj["configfile"] if os.path.exists(ctx.obj['configfile']): with open(ctx.obj["configfile"]) as fd: config = yaml.load(fd) else: - config = {} + config = {} + storage.mkdir_p(os.path.dirname(ctx.obj['configfile'])) configure_dexbot(config) - cfg_file = ctx.obj["configfile"] - if not "/" in cfg_file: # save to home directory unless user wants something else - cfg_file = os.path.expanduser("~/"+cfg_file) - with open(cfg_file,"w") as fd: - yaml.dump(config,fd,default_flow_style=False) + with open(cfg_file, "w") as fd: + yaml.dump(config, fd, default_flow_style=False) click.echo("new configuration saved") if config['systemd_status'] == 'installed': # we are already installed @@ -101,5 +131,6 @@ def configure(ctx): click.echo("starting dexbot daemon") os.system("systemctl --user start dexbot") + if __name__ == '__main__': main() diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 07a73cb20..457abec73 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -14,21 +14,29 @@ """ -import importlib, os, os.path, sys, collections, re, tempfile, shutil +import importlib +import os +import os.path +import sys +import collections +import re +import tempfile +import shutil from dexbot.bot import STRATEGIES from dexbot.whiptail import get_whiptail from dexbot.find_node import start_pings, best_node -SYSTEMD_SERVICE_NAME=os.path.expanduser("~/.local/share/systemd/user/dexbot.service") +SYSTEMD_SERVICE_NAME = os.path.expanduser( + "~/.local/share/systemd/user/dexbot.service") -SYSTEMD_SERVICE_FILE=""" +SYSTEMD_SERVICE_FILE = """ [Unit] Description=Dexbot [Service] Type=notify WorkingDirectory={homedir} -ExecStart={exe} --systemd run +ExecStart={exe} --systemd run Environment=PYTHONUNBUFFERED=true Environment=UNLOCK={passwd} @@ -36,28 +44,35 @@ WantedBy=default.target """ - -def select_choice(current,choices): + +def select_choice(current, choices): """for the radiolist, get us a list with the current value selected""" - return [(tag,text,(current == tag and "ON") or "OFF") for tag,text in choices] + return [(tag, text, (current == tag and "ON") or "OFF") + for tag, text in choices] + -def process_config_element(elem,d,config): +def process_config_element(elem, d, config): """ process an item of configuration metadata display a widget as appropriate d: the Dialog object config: the config dctionary for this bot """ if elem.type == "string": - txt = d.prompt(elem.description,config.get(elem.key,elem.default)) + txt = d.prompt(elem.description, config.get(elem.key, elem.default)) if elem.extra: - while not re.match(elem.extra,txt): + while not re.match(elem.extra, txt): d.alert("The value is not valid") - txt = d.prompt(elem.description,config.get(elem.key,elem.default)) + txt = d.prompt( + elem.description, config.get( + elem.key, elem.default)) config[elem.key] = txt if elem.type == "bool": config[elem.key] = d.confirm(elem.description) if elem.type in ("float", "int"): - txt = d.prompt(elem.description,config.get(elem.key,str(elem.default))) + txt = d.prompt( + elem.description, config.get( + elem.key, str( + elem.default))) while True: try: if elem.type == "int": @@ -72,39 +87,57 @@ def process_config_element(elem,d,config): break except ValueError: d.alert("Not a valid value") - txt = d.prompt(elem.description,config.get(elem.key,str(elem.default))) + txt = d.prompt( + elem.description, config.get( + elem.key, str( + elem.default))) config[elem.key] = val if elem.type == "choice": - config[elem.key] = d.radiolist(elem.description,select_choice(config.get(elem.key,elem.default),elem.extra)) - -def setup_systemd(d,config): - if config.get("systemd_status","install") == "reject": - return # don't nag user if previously said no + config[elem.key] = d.radiolist(elem.description, select_choice( + config.get(elem.key, elem.default), elem.extra)) + + +def setup_systemd(d, config): + if config.get("systemd_status", "install") == "reject": + return # don't nag user if previously said no if not os.path.exists("/etc/systemd"): - return # no working systemd + return # no working systemd if os.path.exists(SYSTEMD_SERVICE_NAME): # dexbot already installed # so just tell cli.py to quietly restart the daemon config["systemd_status"] = "installed" return - if d.confirm("Do you want to install dexbot as a background (daemon) process?"): - for i in ["~/.local","~/.local/share","~/.local/share/systemd","~/.local/share/systemd/user"]: + if d.confirm( + "Do you want to install dexbot as a background (daemon) process?"): + for i in ["~/.local", "~/.local/share", + "~/.local/share/systemd", "~/.local/share/systemd/user"]: j = os.path.expanduser(i) if not os.path.exists(j): os.mkdir(j) - passwd = d.prompt("The wallet password entered with uptick\nNOTE: this will be saved on disc so the bot can run unattended. This means anyone with access to this computer's file can spend all your money",password=True) - fd = os.open(SYSTEMD_SERVICE_NAME, os.O_WRONLY|os.O_CREAT, 0o600) # because we hold password be restrictive + passwd = d.prompt( + "The wallet password entered with uptick\nNOTE: this will be saved on disc so the bot can run unattended. This means anyone with access to this computer's file can spend all your money", + password=True) + # because we hold password be restrictive + fd = os.open(SYSTEMD_SERVICE_NAME, os.O_WRONLY | os.O_CREAT, 0o600) with open(fd, "w") as fp: - fp.write(SYSTEMD_SERVICE_FILE.format(exe=sys.argv[0],passwd=passwd,homedir=os.path.expanduser("~"))) - config['systemd_status'] = 'install' # signal cli.py to set the unit up after writing config file + fp.write( + SYSTEMD_SERVICE_FILE.format( + exe=sys.argv[0], + passwd=passwd, + homedir=os.path.expanduser("~"))) + # signal cli.py to set the unit up after writing config file + config['systemd_status'] = 'install' else: config['systemd_status'] = 'reject' - -def configure_bot(d,bot): - strategy = bot.get('module','dexbot.strategies.echo') - bot['module'] = d.radiolist("Choose a bot strategy",select_choice(strategy,STRATEGIES)) - bot['bot'] = 'Strategy' # its always Strategy now, for backwards compatibilty only + +def configure_bot(d, bot): + strategy = bot.get('module', 'dexbot.strategies.echo') + bot['module'] = d.radiolist( + "Choose a bot strategy", select_choice( + strategy, STRATEGIES)) + # its always Strategy now, for backwards compatibilty only + bot['bot'] = 'Strategy' # import the bot class but we don't __init__ it here klass = getattr( importlib.import_module(bot["module"]), @@ -114,41 +147,75 @@ def configure_bot(d,bot): configs = klass.configure() if configs: for c in configs: - process_config_element(c,d,bot) + process_config_element(c, d, bot) else: d.alert("This bot type does not have configuration information. You will have to check the bot code and add configuration values to config.yml if required") return bot - - +def setup_reporter(d, config): + reporter_config = config.get('reports',{}) + frequency = d.radiolist("DEXBot can send an e-mail report on its activities at regular intervals", select_choice( + str(reporter_config.get('days',0)), + [("0",'Never'), ('1','Daily'), ('2','Second-daily'), ('3','Third-daily'), ('7','Weekly')])) + if frequency == '0': + if 'reporter' in config: + del config['reporter'] + return + reporter_config['days'] = int(frequency) + reporter_config['send_to'] = d.prompt("E-mail address to send to",default=reporter_config.get('send_to','')) + reporter_config['send_from'] = d.prompt("E-mail address to send from (blank will use local user and host name)", + default=reporter_config.get('send_from',reporter_config['send_to'])) + reporter_config['server'] = d.prompt("SMTP server to use (blank means this server)", + default=reporter_config.get('server','')) + reporter_config['port'] = d.prompt("SMTP server port to use", + default=reporter_config.get('port','25')) + reporter_config['login'] = d.prompt("Login username for the SMTP server (blank means don't login)", + default=reporter_config.get('login', + reporter_config['send_to'].split('@')[0])) + if reporter_config['login']: + reporter_config['password'] = d.prompt("SMTP server password to use",password=True) + config['reports'] = reporter_config + def configure_dexbot(config): d = get_whiptail() - if not 'node' in config: - # start our best node search in the background - ping_results = start_pings() - bots = config.get('bots',{}) + bots = config.get('bots', {}) if len(bots) == 0: - txt = d.prompt("Your name for the first bot") - config['bots'] = {txt:configure_bot(d,{})} - else: - botname = d.menu("Select bot to edit",[(i,i) for i in bots]+[('NEW','New bot')]) - if botname == 'NEW': - txt = d.prompt("Your name for the new bot") - config['bots'][txt] = configure_bot(d,{}) - else: - config['bots'][botname] = configure_bot(d,config['bots'][botname]) - if not 'node' in config: + ping_results = start_pings() + while True: + txt = d.prompt("Your name for the bot") + config['bots'] = {txt: configure_bot(d, {})} + if not d.confirm("Set up another bot?\n(DEXBOt can run multiple bots in one instance)"): + break + setup_reporter(d, config) + setup_systemd(d, config) node = best_node(ping_results) if node: config['node'] = node else: # search failed, ask the user - config['node'] = d.prompt("Search for best BitShares node failed.\n\nPlease enter wss:// url of chosen node.") - setup_systemd(d,config) + config['node'] = d.prompt( + "Search for best BitShares node failed.\n\nPlease enter wss:// url of chosen node.") + else: + action = d.menu("You have an existing configuration.\nSelect an action:", + [('NEW', 'Create a new bot'), + ('DEL', 'Delete a bot'), + ('EDIT', 'Edit a bot'), + ('CONF', 'Redo general config')]) + if action == 'EDIT': + botname = d.menu("Select bot to edit", [(i, i) for i in bots]) + config['bots'][botname] = configure_bot(d, config['bots'][botname]) + elif action == 'DEL': + botname = d.menu("Select bot to delete", [(i, i) for i in bots]) + del config['bots'][botname] + if action == 'NEW': + txt = d.prompt("Your name for the new bot") + config['bots'][txt] = configure_bot(d, {}) + else: + setup_reporter(d, config) + config['node'] = d.prompt("BitShares node to use",default=config['node']) d.clear() return config -if __name__=='__main__': + +if __name__ == '__main__': print(repr(configure({}))) - - diff --git a/dexbot/ui.py b/dexbot/ui.py index fd6c4a102..62cd68b8f 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -1,7 +1,7 @@ -import os, sys +import os +import sys import click -import logging -import yaml +import logging, logging.config from datetime import datetime from bitshares.price import Price from prettytable import PrettyTable @@ -10,6 +10,7 @@ from bitshares.instance import set_shared_bitshares_instance log = logging.getLogger(__name__) +from dexbot.storage import SQLiteHandler def verbose(f): @click.pass_context @@ -17,24 +18,32 @@ 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): + 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') + 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') 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') + 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') 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') + 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') level = getattr(logging, verbosity.upper()) # use special format for special bots logger ch = logging.StreamHandler() ch.setFormatter(formatter2) logging.getLogger("dexbot.per_bot").addHandler(ch) + logging.getLogger("dexbot.per_bot").addHandler(SQLiteHandler()) # and log to SQLIte DB logging.getLogger("dexbot.per_bot").setLevel(level) - logging.getLogger("dexbot.per_bot").propagate = False # don't double up with root logger + # don't double up with root logger + logging.getLogger("dexbot.per_bot").propagate = False # set the root logger with basic format ch = logging.StreamHandler() ch.setFormatter(formatter1) @@ -42,7 +51,6 @@ def new_func(ctx, *args, **kwargs): logging.getLogger("dexbot").addHandler(ch) # and don't double up on the root logger logging.getLogger("").handlers = [] - # GrapheneAPI logging if ctx.obj["verbose"] > 4: verbosity = [ @@ -51,7 +59,6 @@ def new_func(ctx, *args, **kwargs): log = logging.getLogger("grapheneapi") log.setLevel(getattr(logging, verbosity.upper())) log.addHandler(ch) - if ctx.obj["verbose"] > 8: verbosity = [ "critical", "error", "warn", "info", "debug" @@ -59,7 +66,10 @@ def new_func(ctx, *args, **kwargs): log = logging.getLogger("graphenebase") log.setLevel(getattr(logging, verbosity.upper())) log.addHandler(ch) - + # has the user set logging in the config + if "logging" in ctx.config: + # this is defined in https://docs.python.org/3.4/library/logging.config.html#logging-config-dictschema + logging.config.dictConfig(ctx.config['logging']) return ctx.invoke(f, *args, **kwargs) return update_wrapper(new_func, f) @@ -80,7 +90,7 @@ def unlock(f): @click.pass_context def new_func(ctx, *args, **kwargs): if not ctx.obj.get("unsigned", False): - systemd = ctx.obj.get('systemd',False) + systemd = ctx.obj.get('systemd', False) if ctx.bitshares.wallet.created(): if "UNLOCK" in os.environ: pwd = os.environ["UNLOCK"] @@ -88,8 +98,9 @@ def new_func(ctx, *args, **kwargs): if systemd: # no user available to interact with log.critical("Passphrase not available, exiting") - sys.exit(78) # 'configuation error' in systexits.h - pwd = click.prompt("Current Wallet Passphrase", hide_input=True) + sys.exit(78) # 'configuation error' in sysexits.h + pwd = click.prompt( + "Current Wallet Passphrase", hide_input=True) ctx.bitshares.wallet.unlock(pwd) else: if systemd: @@ -97,7 +108,10 @@ def new_func(ctx, *args, **kwargs): log.critical("Wallet not installed, cannot run") sys.exit(78) click.echo("No wallet installed yet. Creating ...") - pwd = click.prompt("Wallet Encryption Passphrase", hide_input=True, confirmation_prompt=True) + pwd = click.prompt( + "Wallet Encryption Passphrase", + hide_input=True, + confirmation_prompt=True) ctx.bitshares.wallet.create(pwd) return ctx.invoke(f, *args, **kwargs) return update_wrapper(new_func, f) @@ -106,11 +120,14 @@ def new_func(ctx, *args, **kwargs): def configfile(f): @click.pass_context def new_func(ctx, *args, **kwargs): - ctx.config = yaml.load(open(ctx.obj["configfile"])) + try: + ctx.config = yaml.safe_load(open(ctx.obj["configfile"])) + except FileNotFoundError: + alert("Looking for the config file in %s\nNot found!\nTry running 'dexbot configure' to generate\n" % ctx.obj['configfile']) + sys.exit(78) # 'configuation error' in sysexits.h return ctx.invoke(f, *args, **kwargs) return update_wrapper(new_func, f) - def priceChange(new, old): if float(old) == 0.0: return -1 @@ -149,10 +166,10 @@ def confirmwarning(msg): def alert(msg): click.echo( "[" + - click.style("alert", fg="yellow") + + click.style("Alert", fg="red") + "] " + msg ) - +5B def confirmalert(msg): return click.confirm( diff --git a/dexbot/whiptail.py b/dexbot/whiptail.py index bae67651f..5906cd666 100644 --- a/dexbot/whiptail.py +++ b/dexbot/whiptail.py @@ -1,6 +1,7 @@ from __future__ import print_function import sys -import shlex, shutil +import shlex +import shutil import itertools import click from subprocess import Popen, PIPE @@ -18,6 +19,7 @@ def flatten(data): return list(itertools.chain.from_iterable(data)) + class Whiptail: def __init__(self, title='', backtitle='', height=20, width=60, @@ -27,7 +29,7 @@ def __init__(self, title='', backtitle='', height=20, width=60, self.height = height self.width = width self.auto_exit = auto_exit - + def run(self, control, msg, extra=(), exit_on=(1, 255)): cmd = [ 'whiptail', '--title', self.title, '--backtitle', self.backtitle, @@ -39,26 +41,26 @@ def run(self, control, msg, extra=(), exit_on=(1, 255)): if self.auto_exit and p.returncode in exit_on: print('User cancelled operation.') sys.exit(p.returncode) - return Response(p.returncode, str(err,'utf-8','ignore')) + return Response(p.returncode, str(err, 'utf-8', 'ignore')) def prompt(self, msg, default='', password=False): control = 'passwordbox' if password else 'inputbox' return self.run(control, msg, [default]).value - + def confirm(self, msg, default='yes'): defaultno = '--defaultno' if default == 'no' else '' return self.run('yesno', msg, [defaultno], [255]).returncode == 0 - + def alert(self, msg): self.run('msgbox', msg) - + def view_file(self, path): self.run('textbox', path, ['--scrolltext']) - + def calc_height(self, msg): height_offset = 8 if msg else 7 return [str(self.height - height_offset)] - + def menu(self, msg='', items=(), prefix=' - '): if isinstance(items[0], str): items = [(i, '') for i in items] @@ -66,7 +68,7 @@ def menu(self, msg='', items=(), prefix=' - '): items = [(k, prefix + v) for k, v in items] extra = self.calc_height(msg) + flatten(items) return self.run('menu', msg, extra).value - + def showlist(self, control, msg, items, prefix): if isinstance(items[0], str): items = [(i, '', 'OFF') for i in items] @@ -74,13 +76,13 @@ def showlist(self, control, msg, items, prefix): items = [(k, prefix + v, s) for k, v, s in items] extra = self.calc_height(msg) + flatten(items) return shlex.split(self.run(control, msg, extra).value) - + def radiolist(self, msg='', items=(), prefix=' - '): return self.showlist('radiolist', msg, items, prefix)[0] - + def checklist(self, msg='', items=(), prefix=' - '): return self.showlist('checklist', msg, items, prefix) - + def view_text(self, text): """Whiptail wants a file but we want to provide a text string""" fd, nam = tempfile.mkstemp() @@ -90,47 +92,48 @@ def view_text(self, text): self.view_file(nam) os.unlink(nam) - def clear(self): # tidy up the screen click.clear() - + + class NoWhiptail: """ Imitates the interface of whiptail but uses click only - This is very basic CLI: real state-of-the-1970s stuff, + This is very basic CLI: real state-of-the-1970s stuff, but it works *everywhere* """ - + def prompt(self, msg, default='', password=False): - return click.prompt(msg,default=default,hide_input=password) + return click.prompt(msg, default=default, hide_input=password) def confirm(self, msg, default='yes'): - return click.confirm(msg,default=(default=='yes')) - + return click.confirm(msg, default=(default == 'yes')) + def alert(self, msg): click.echo( "[" + click.style("alert", fg="yellow") + "] " + msg ) - + def view_text(self, text): click.echo_via_pager(text) - + def menu(self, msg='', items=(), prefix=' - ', default=0): - click.echo(msg+'\n') - if type(items) is dict: items = list(items.items()) + click.echo(msg + '\n') + if isinstance(items, dict): + items = list(items.items()) i = 1 for k, v in items: click.echo("{:>2}) {}".format(i, v)) i += 1 click.echo("\n") - ret = click.prompt("Your choice:",type=int,default=default+1) - ret = items[ret-1] + ret = click.prompt("Your choice:", type=int, default=default + 1) + ret = items[ret - 1] return ret[0] - + def radiolist(self, msg='', items=(), prefix=' - '): d = 0 default = 0 @@ -138,15 +141,15 @@ def radiolist(self, msg='', items=(), prefix=' - '): if s == "ON": default = d d += 1 - return self.menu(msg,[(k,v) for k,v,s in items],default=default) - + return self.menu(msg, [(k, v) for k, v, s in items], default=default) def clear(self): - pass # dont tidy the screen - + pass # dont tidy the screen + + def get_whiptail(): if shutil.which("whiptail"): d = Whiptail() else: - d = NoWhiptail() # use our own fake whiptail + d = NoWhiptail() # use our own fake whiptail return d From a8890ff784d6ad5b3e1b50bc05279c079d331a28 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 8 Mar 2018 14:13:16 +1100 Subject: [PATCH 0105/1846] fix typos --- dexbot/cli.py | 3 ++- dexbot/ui.py | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index 7ea8a0a14..1d93bee71 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -10,6 +10,7 @@ import os import sys import appdirs +from ruamel import yaml from .ui import ( verbose, @@ -113,7 +114,7 @@ def configure(ctx): cfg_file = ctx.obj["configfile"] if os.path.exists(ctx.obj['configfile']): with open(ctx.obj["configfile"]) as fd: - config = yaml.load(fd) + config = yaml.safe_load(fd) else: config = {} storage.mkdir_p(os.path.dirname(ctx.obj['configfile'])) diff --git a/dexbot/ui.py b/dexbot/ui.py index 443113bcf..3d5e0fa23 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -11,8 +11,6 @@ from bitshares.instance import set_shared_bitshares_instance log = logging.getLogger(__name__) -from dexbot.storage import SQLiteHandler - def verbose(f): @click.pass_context def new_func(ctx, *args, **kwargs): @@ -162,7 +160,6 @@ def alert(msg): click.style("Alert", fg="red") + "] " + msg ) -5B def confirmalert(msg): return click.confirm( From 5d067b723999a33842d56512bb16f9920d520fab Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 8 Mar 2018 10:21:11 +0200 Subject: [PATCH 0106/1846] Change yml loading to use safe_load --- dexbot/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index 6864f815f..9e59a9530 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -86,7 +86,7 @@ def configure(ctx): """ if os.path.exists(ctx.obj['configfile']): with open(ctx.obj["configfile"]) as fd: - config = yaml.load(fd) + config = yaml.safe_load(fd) else: config = {} configure_dexbot(config) From 370795d3e5c86b794312e6450090d6a89df7b9a3 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 8 Mar 2018 21:56:17 +1100 Subject: [PATCH 0107/1846] 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 0108/1846] 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 0109/1846] 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 0110/1846] 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 0111/1846] 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 0112/1846] 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 0113/1846] 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 0114/1846] 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 0115/1846] 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 0116/1846] 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 0117/1846] 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 0118/1846] 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 0119/1846] 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 0120/1846] 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 0121/1846] 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 0122/1846] 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 0123/1846] - 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 0124/1846] 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 0125/1846] 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 0126/1846] 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 5becb2a5780ef3519d8b5889785a24d891be2557 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 15 Mar 2018 20:57:55 +1100 Subject: [PATCH 0127/1846] remove references to UNIX signal handling that should not be there --- dexbot/cli.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index 1d93bee71..e3f996ea7 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -5,7 +5,6 @@ if not "LANG" in os.environ: os.environ['LANG'] = 'C.UTF-8' import click -import signal import os.path import os import sys @@ -80,19 +79,6 @@ def run(ctx): fd.write(str(os.getpid())) 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)) - signal.signal(signal.SIGUSR2, lambda x, y: bot.do_next_tick(bot.report_now)) - except ValueError: - log.debug("Cannot set all signals -- not avaiable on this platform") bot.init_bots() if ctx.obj['systemd']: try: From a540f0baeb6b180c13be09e1f860cd7421e1477d Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 15 Mar 2018 21:00:54 +1100 Subject: [PATCH 0128/1846] strip references to the email reporter --- dexbot/cli_conf.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 457abec73..ba6a9150e 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -152,30 +152,6 @@ def configure_bot(d, bot): d.alert("This bot type does not have configuration information. You will have to check the bot code and add configuration values to config.yml if required") return bot -def setup_reporter(d, config): - reporter_config = config.get('reports',{}) - frequency = d.radiolist("DEXBot can send an e-mail report on its activities at regular intervals", select_choice( - str(reporter_config.get('days',0)), - [("0",'Never'), ('1','Daily'), ('2','Second-daily'), ('3','Third-daily'), ('7','Weekly')])) - if frequency == '0': - if 'reporter' in config: - del config['reporter'] - return - reporter_config['days'] = int(frequency) - reporter_config['send_to'] = d.prompt("E-mail address to send to",default=reporter_config.get('send_to','')) - reporter_config['send_from'] = d.prompt("E-mail address to send from (blank will use local user and host name)", - default=reporter_config.get('send_from',reporter_config['send_to'])) - reporter_config['server'] = d.prompt("SMTP server to use (blank means this server)", - default=reporter_config.get('server','')) - reporter_config['port'] = d.prompt("SMTP server port to use", - default=reporter_config.get('port','25')) - reporter_config['login'] = d.prompt("Login username for the SMTP server (blank means don't login)", - default=reporter_config.get('login', - reporter_config['send_to'].split('@')[0])) - if reporter_config['login']: - reporter_config['password'] = d.prompt("SMTP server password to use",password=True) - config['reports'] = reporter_config - def configure_dexbot(config): d = get_whiptail() bots = config.get('bots', {}) @@ -186,7 +162,6 @@ def configure_dexbot(config): config['bots'] = {txt: configure_bot(d, {})} if not d.confirm("Set up another bot?\n(DEXBOt can run multiple bots in one instance)"): break - setup_reporter(d, config) setup_systemd(d, config) node = best_node(ping_results) if node: @@ -211,7 +186,6 @@ def configure_dexbot(config): txt = d.prompt("Your name for the new bot") config['bots'][txt] = configure_bot(d, {}) else: - setup_reporter(d, config) config['node'] = d.prompt("BitShares node to use",default=config['node']) d.clear() return config From 917f87af581d3af5aab80a0a8fe51d9f68cd1102 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 15 Mar 2018 13:44:41 +0200 Subject: [PATCH 0129/1846] Remove follow orders strategy for now It will be added back once follow orders branch is merged --- dexbot/bot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dexbot/bot.py b/dexbot/bot.py index 85bb57d69..bc09f0e9f 100644 --- a/dexbot/bot.py +++ b/dexbot/bot.py @@ -20,8 +20,7 @@ # GUIs can add a handler to this logger to get a stream of events re the running bots. # FIXME: currently static list of bot strategies: ? how to enumerate bots available and deploy new bot strategies. -STRATEGIES = [('dexbot.strategies.echo', 'Echo Test'), - ('dexbot.strategies.follow_orders', "Haywood's Follow Orders")] +STRATEGIES = [('dexbot.strategies.echo', 'Echo Test')] class BotInfrastructure(threading.Thread): From d9b554a74a5b67039833095a8b5f23123a954cc6 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 16 Mar 2018 13:38:24 +0200 Subject: [PATCH 0130/1846] Fix pep8 errors --- dexbot/cli.py | 4 ++-- dexbot/cli_conf.py | 8 +++----- dexbot/find_node.py | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index f05f75f48..1e601e9f1 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -39,7 +39,7 @@ @click.group() @click.option( "--configfile", - default=os.path.join(appdirs.user_config_dir("dexbot"),"config.yml"), + default=os.path.join(appdirs.user_config_dir("dexbot"), "config.yml"), ) @click.option( '--verbose', @@ -75,7 +75,7 @@ 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) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 787f41788..cb9833fe9 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -18,10 +18,7 @@ import os import os.path import sys -import collections import re -import tempfile -import shutil from dexbot.bot import STRATEGIES from dexbot.whiptail import get_whiptail from dexbot.find_node import start_pings, best_node @@ -154,6 +151,7 @@ def configure_bot(d, bot): "You will have to check the bot code and add configuration values to config.yml if required") return bot + def configure_dexbot(config): d = get_whiptail() bots = config.get('bots', {}) @@ -162,7 +160,7 @@ def configure_dexbot(config): while True: txt = d.prompt("Your name for the bot") config['bots'] = {txt: configure_bot(d, {})} - if not d.confirm("Set up another bot?\n(DEXBOt can run multiple bots in one instance)"): + if not d.confirm("Set up another bot?\n(DEXBot can run multiple bots in one instance)"): break setup_systemd(d, config) node = best_node(ping_results) @@ -188,7 +186,7 @@ def configure_dexbot(config): txt = d.prompt("Your name for the new bot") config['bots'][txt] = configure_bot(d, {}) else: - config['node'] = d.prompt("BitShares node to use",default=config['node']) + config['node'] = d.prompt("BitShares node to use", default=config['node']) d.clear() return config diff --git a/dexbot/find_node.py b/dexbot/find_node.py index 265dc369c..9baffb106 100644 --- a/dexbot/find_node.py +++ b/dexbot/find_node.py @@ -63,7 +63,7 @@ def best_node(results): try: r = sorted([process_ping_result(*i) for i in results]) return r[0][1] - except: + except BaseException: return None From 9936948cb6487150a414e91afe2030179d2a6f4e Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 16 Mar 2018 13:43:48 +0200 Subject: [PATCH 0131/1846] Remove some nodes from the node list Removed nodes.bitshares.works nodes because they fail ssl handshake And removed uptick.rocks because it should only be used for testing --- dexbot/find_node.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/dexbot/find_node.py b/dexbot/find_node.py index 9baffb106..0c344aa53 100644 --- a/dexbot/find_node.py +++ b/dexbot/find_node.py @@ -16,7 +16,6 @@ "wss://bitshares-api.wancloud.io/ws", "wss://openledger.hk/ws", "wss://bitshares.apasia.tech/ws", - "wss://uptick.rocks" "wss://bitshares.crypto.fans/ws", "wss://kc-us-dex.xeldal.com/ws", "wss://api.bts.blckchnd.com", @@ -24,10 +23,7 @@ "wss://bitshares.dacplay.org/ws", "wss://bit.btsabc.org/ws", "wss://bts.ai.la/ws", - "wss://ws.gdex.top", - "wss://us.nodes.bitshares.works", - "wss://eu.nodes.bitshares.works", - "wss://sg.nodes.bitshares.works"] + "wss://ws.gdex.top"] if system() == 'Windows': ping_re = re.compile(r'Average = ([\d.]+)ms') From 35f618728df7994fa7bb4729fdd5a84473fe31dc Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 16 Mar 2018 13:45:38 +0200 Subject: [PATCH 0132/1846] Add node connection checking before start up --- dexbot/cli.py | 8 +++----- dexbot/find_node.py | 16 +++++++++++++--- dexbot/ui.py | 13 +++++++++++++ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index 1e601e9f1..ba0226f21 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -13,13 +13,10 @@ from .ui import ( verbose, + check_connection, chain, unlock, - configfile, - confirmwarning, - confirmalert, - warning, - alert, + configfile ) from .bot import BotInfrastructure @@ -68,6 +65,7 @@ def main(ctx, **kwargs): @main.command() @click.pass_context @configfile +@check_connection @chain @unlock @verbose diff --git a/dexbot/find_node.py b/dexbot/find_node.py index 0c344aa53..ab97d5466 100644 --- a/dexbot/find_node.py +++ b/dexbot/find_node.py @@ -25,6 +25,8 @@ "wss://bts.ai.la/ws", "wss://ws.gdex.top"] +FAILED_PING_AMOUNT = 1000000 + if system() == 'Windows': ping_re = re.compile(r'Average = ([\d.]+)ms') else: @@ -48,14 +50,14 @@ def process_ping_result(host, proc): try: return float(ping_re.search(out).group(1)), host except AttributeError: - return 1000000, host # hosts that fail are last + return FAILED_PING_AMOUNT, host # Hosts that fail are last def start_pings(): return [(i, make_ping_proc(i)) for i in ALL_NODES] -def best_node(results): +def best_node(results=start_pings()): try: r = sorted([process_ping_result(*i) for i in results]) return r[0][1] @@ -63,5 +65,13 @@ def best_node(results): return None +def is_host_online(host): + result = make_ping_proc(host) + ping = process_ping_result(host, result)[0] + if ping >= FAILED_PING_AMOUNT: + return False + return True + + if __name__ == '__main__': - print(best_node(start_pings())) + print(best_node()) diff --git a/dexbot/ui.py b/dexbot/ui.py index af322426b..432fd4e28 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -7,6 +7,9 @@ from functools import update_wrapper from bitshares import BitShares from bitshares.instance import set_shared_bitshares_instance + +from . import find_node + log = logging.getLogger(__name__) @@ -72,6 +75,16 @@ def new_func(ctx, *args, **kwargs): return update_wrapper(new_func, f) +def check_connection(f): + @click.pass_context + def new_func(ctx, *args, **kwargs): + if not find_node.is_host_online(ctx.config['node']): + node = find_node.best_node() + ctx.config['node'] = node + return ctx.invoke(f, *args, **kwargs) + return update_wrapper(new_func, f) + + def chain(f): @click.pass_context def new_func(ctx, *args, **kwargs): From bf6796ce49964552e3f0757912e29c6ff8fb6e54 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 16 Mar 2018 14:10:31 +0200 Subject: [PATCH 0133/1846] Fix pep8 error --- dexbot/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index ba0226f21..49d2386d2 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -2,7 +2,7 @@ import logging import os # we need to do this before importing click -if not "LANG" in os.environ: +if "LANG" not in os.environ: os.environ['LANG'] = 'C.UTF-8' import click import os.path From 2fabd77bfcdf50dee5c2469d9f7e753b664f7495 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Sat, 17 Mar 2018 04:25:39 +1100 Subject: [PATCH 0134/1846] 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 0135/1846] 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 0136/1846] 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 0b5b5f65727a12f5ab929ff2e35bfd3e80e902e2 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 19 Mar 2018 09:13:03 +0200 Subject: [PATCH 0137/1846] Remove pidfile changes They are added as a seperate pr --- dexbot/cli.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index 49d2386d2..e7c2d7ccf 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -6,7 +6,6 @@ os.environ['LANG'] = 'C.UTF-8' import click import os.path -import os import sys import appdirs from ruamel import yaml @@ -49,12 +48,6 @@ '-d', default=False, help='Run as a daemon from systemd') -@click.option( - '--pidfile', - '-p', - type=str, - default='', - help='File to write PID') @click.pass_context def main(ctx, **kwargs): ctx.obj = {} @@ -72,9 +65,6 @@ 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.init_bots() From e9176ee2eb8b9f2a5771a68eadd9f001cd8c40c4 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 19 Mar 2018 13:33:35 +0200 Subject: [PATCH 0138/1846] 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 0139/1846] 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 0140/1846] 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 0141/1846] 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 0142/1846] 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 0143/1846] 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 0144/1846] 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 0145/1846] 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 0146/1846] 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 0147/1846] 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 0148/1846] 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 0149/1846] 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 0150/1846] 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 0151/1846] 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 0152/1846] 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 0153/1846] 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 0154/1846] 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 0155/1846] 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 0156/1846] 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 0157/1846] 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 0158/1846] 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 0159/1846] 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 0160/1846] 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 0161/1846] 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 0162/1846] 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 0163/1846] 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 0164/1846] 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 0165/1846] 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 0166/1846] 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 0167/1846] 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 0168/1846] 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 0169/1846] 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 0170/1846] 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 0171/1846] 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 0172/1846] 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 0173/1846] 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 0174/1846] 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 0175/1846] 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 0176/1846] 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 0177/1846] 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 0178/1846] 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 0179/1846] 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 499441a7f02c730bd8197d61b2a8f659fef2addf Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 9 Apr 2018 15:13:06 +0300 Subject: [PATCH 0180/1846] Change pidfile removal logic --- dexbot/cli.py | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index 8e70b8987..16f0ab6ef 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -76,33 +76,35 @@ def run(ctx): with open(ctx.obj['pidfile'], 'w') as fd: fd.write(str(os.getpid())) try: + worker = WorkerInfrastructure(ctx.config) + # Set up signalling. do it here as of no relevance to GUI + kill_workers = worker_job(worker, worker.stop) + # These first two UNIX & Windows + signal.signal(signal.SIGTERM, kill_workers) + signal.signal(signal.SIGINT, kill_workers) try: - worker = WorkerInfrastructure(ctx.config) - # Set up signalling. do it here as of no relevance to GUI - kill_workers = worker_job(worker, worker.stop) - # These first two UNIX & Windows - signal.signal(signal.SIGTERM, kill_workers) - signal.signal(signal.SIGINT, kill_workers) + # These signals are UNIX-only territory, will ValueError here on Windows + 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: + log.debug("Cannot set all signals -- not available on this platform") + if ctx.obj['systemd']: try: - # These signals are UNIX-only territory, will ValueError here on Windows - 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: - log.debug("Cannot set all signals -- not available on this platform") - if ctx.obj['systemd']: - try: - import sdnotify # A soft dependency on sdnotify -- don't crash on non-systemd systems - n = sdnotify.SystemdNotifier() - n.notify("READY=1") - except BaseException: - log.debug("sdnotify not available") - worker.run() - finally: - if ctx.obj['pidfile']: - os.unlink(ctx.obj['pidfile']) + import sdnotify # A soft dependency on sdnotify -- don't crash on non-systemd systems + n = sdnotify.SystemdNotifier() + n.notify("READY=1") + except BaseException: + log.debug("sdnotify not available") + worker.run() except errors.NoWorkersAvailable: sys.exit(70) # 70= "Software error" in /usr/include/sysexts.h + finally: + if ctx.obj['pidfile']: + try: + os.remove(ctx.obj['pidfile']) + except OSError: + pass @main.command() From 4847af4e9b151c23cc0f181323bd9bd0a31f5948 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 11 Apr 2018 10:25:22 +0300 Subject: [PATCH 0181/1846] Change mkdir function location to helper module --- dexbot/cli.py | 4 ++-- dexbot/helper.py | 10 ++++++++++ dexbot/storage.py | 16 +++------------- 3 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 dexbot/helper.py diff --git a/dexbot/cli.py b/dexbot/cli.py index 16f0ab6ef..6e237207a 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -16,7 +16,7 @@ from .worker import WorkerInfrastructure from .cli_conf import configure_dexbot from . import errors -from . import storage +from . import helper from ruamel import yaml # We need to do this before importing click @@ -118,7 +118,7 @@ def configure(ctx): config = yaml.safe_load(fd) else: config = {} - storage.mkdir_p(os.path.dirname(ctx.obj['configfile'])) + helper.mkdir(os.path.dirname(ctx.obj['configfile'])) configure_dexbot(config) with open(cfg_file, "w") as fd: diff --git a/dexbot/helper.py b/dexbot/helper.py new file mode 100644 index 000000000..31e4bb619 --- /dev/null +++ b/dexbot/helper.py @@ -0,0 +1,10 @@ +import os + + +def mkdir(d): + try: + os.makedirs(d) + except FileExistsError: + return + except OSError: + raise diff --git a/dexbot/storage.py b/dexbot/storage.py index ec785284a..175fd1bde 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -9,6 +9,8 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker +from . import helper + Base = declarative_base() # For dexbot.sqlite file @@ -17,18 +19,6 @@ storageDatabase = "dexbot.sqlite" -def mkdir_p(d): - if os.path.isdir(d): - return - else: - try: - os.makedirs(d) - except FileExistsError: - return - except OSError: - raise - - class Config(Base): __tablename__ = 'config' @@ -184,6 +174,6 @@ def clear(self, category): sqlDataBaseFile = os.path.join(data_dir, storageDatabase) # Create directory for sqlite file -mkdir_p(data_dir) +helper.mkdir(data_dir) db_worker = DatabaseWorker() From 9325a0033ff96d4519cac6847a302bb29ecd9c55 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 11 Apr 2018 10:37:03 +0300 Subject: [PATCH 0182/1846] 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 0183/1846] 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 0184/1846] 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 0185/1846] 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 0186/1846] 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 1e9df9361263f01219de8a730a76b572f58c1238 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 12 Apr 2018 13:31:47 +0300 Subject: [PATCH 0187/1846] Change find_node.py structure Made some functions private and removed the unnesessary print --- dexbot/find_node.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/dexbot/find_node.py b/dexbot/find_node.py index ab97d5466..035470a5d 100644 --- a/dexbot/find_node.py +++ b/dexbot/find_node.py @@ -33,19 +33,14 @@ ping_re = re.compile(r'min/avg/max/mdev = [\d.]+/([\d.]+)') -def ping_cmd(x): +def _ping_cmd(x): if system() == 'Windows': return 'ping', '-n', '5', '-w', '1500', x else: return 'ping', '-c5', '-n', '-w5', '-i0.3', x -def make_ping_proc(host): - host = urlsplit(host).netloc.split(':')[0] - return Popen(ping_cmd(host), stdout=PIPE, stderr=STDOUT, universal_newlines=True) - - -def process_ping_result(host, proc): +def _process_ping_result(host, proc): out = proc.communicate()[0] try: return float(ping_re.search(out).group(1)), host @@ -53,25 +48,26 @@ def process_ping_result(host, proc): return FAILED_PING_AMOUNT, host # Hosts that fail are last +def make_ping(host): + host = urlsplit(host).netloc.split(':')[0] + return Popen(_ping_cmd(host), stdout=PIPE, stderr=STDOUT, universal_newlines=True) + + def start_pings(): - return [(i, make_ping_proc(i)) for i in ALL_NODES] + return [(i, make_ping(i)) for i in ALL_NODES] def best_node(results=start_pings()): try: - r = sorted([process_ping_result(*i) for i in results]) + r = sorted([_process_ping_result(*i) for i in results]) return r[0][1] except BaseException: return None def is_host_online(host): - result = make_ping_proc(host) - ping = process_ping_result(host, result)[0] + result = make_ping(host) + ping = _process_ping_result(host, result)[0] if ping >= FAILED_PING_AMOUNT: return False return True - - -if __name__ == '__main__': - print(best_node()) From c3f8899e071c117c4631c6acb4a30ecf3fe7abc4 Mon Sep 17 00:00:00 2001 From: sergio Date: Thu, 12 Apr 2018 17:08:00 +0200 Subject: [PATCH 0188/1846] - 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 0189/1846] - 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 c256917bf9bc3d7e6b4a42812150e707d75df74a Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 13 Apr 2018 11:53:10 +0300 Subject: [PATCH 0190/1846] Change bot to worker in cli_conf.py --- dexbot/cli_conf.py | 79 ++++++++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index cb9833fe9..2bc477685 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -8,9 +8,8 @@ Note there is some common cross-UI configuration stuff: look in basestrategy.py It's expected GUI/web interfaces will be re-implementing code in this file, but they should -understand the common code so bot strategy writers can define their configuration once +understand the common code so worker strategy writers can define their configuration once for each strategy class. - """ @@ -19,7 +18,7 @@ import os.path import sys import re -from dexbot.bot import STRATEGIES +from dexbot.worker import STRATEGIES from dexbot.whiptail import get_whiptail from dexbot.find_node import start_pings, best_node @@ -43,16 +42,16 @@ def select_choice(current, choices): - """for the radiolist, get us a list with the current value selected""" + """ For the radiolist, get us a list with the current value selected """ return [(tag, text, (current == tag and "ON") or "OFF") for tag, text in choices] def process_config_element(elem, d, config): """ - process an item of configuration metadata display a widget as appropriate + Process an item of configuration metadata display a widget as appropriate d: the Dialog object - config: the config dctionary for this bot + config: the config dictionary for this worker """ if elem.type == "string": txt = d.prompt(elem.description, config.get(elem.key, elem.default)) @@ -112,10 +111,10 @@ def setup_systemd(d, config): if not os.path.exists(j): os.mkdir(j) passwd = d.prompt("The wallet password entered with uptick\n" - "NOTE: this will be saved on disc so the bot can run unattended. " + "NOTE: this will be saved on disc so the worker can run unattended. " "This means anyone with access to this computer's file can spend all your money", password=True) - # because we hold password be restrictive + # Because we hold password be restrictive fd = os.open(SYSTEMD_SERVICE_NAME, os.O_WRONLY | os.O_CREAT, 0o600) with open(fd, "w") as fp: fp.write( @@ -123,73 +122,69 @@ def setup_systemd(d, config): exe=sys.argv[0], passwd=passwd, homedir=os.path.expanduser("~"))) - # signal cli.py to set the unit up after writing config file + # Signal cli.py to set the unit up after writing config file config['systemd_status'] = 'install' else: config['systemd_status'] = 'reject' -def configure_bot(d, bot): - strategy = bot.get('module', 'dexbot.strategies.echo') - bot['module'] = d.radiolist( - "Choose a bot strategy", select_choice( +def configure_worker(d, worker): + strategy = worker.get('module', 'dexbot.strategies.echo') + worker['module'] = d.radiolist( + "Choose a worker strategy", select_choice( strategy, STRATEGIES)) - # its always Strategy now, for backwards compatibilty only - bot['bot'] = 'Strategy' - # import the bot class but we don't __init__ it here + # It's always Strategy now, for backwards compatibility only + worker['worker'] = 'Strategy' + # Import the worker class but we don't __init__ it here klass = getattr( - importlib.import_module(bot["module"]), - bot["bot"] + importlib.import_module(worker["module"]), + worker["worker"] ) - # use class metadata for per-bot configuration + # Use class metadata for per-worker configuration configs = klass.configure() if configs: for c in configs: - process_config_element(c, d, bot) + process_config_element(c, d, worker) else: - d.alert("This bot type does not have configuration information. " - "You will have to check the bot code and add configuration values to config.yml if required") - return bot + d.alert("This worker type does not have configuration information. " + "You will have to check the worker code and add configuration values to config.yml if required") + return worker def configure_dexbot(config): d = get_whiptail() - bots = config.get('bots', {}) - if len(bots) == 0: + workers = config.get('workers', {}) + if len(workers) == 0: ping_results = start_pings() while True: - txt = d.prompt("Your name for the bot") - config['bots'] = {txt: configure_bot(d, {})} - if not d.confirm("Set up another bot?\n(DEXBot can run multiple bots in one instance)"): + txt = d.prompt("Your name for the worker") + config['workers'] = {txt: configure_worker(d, {})} + if not d.confirm("Set up another worker?\n(DEXBot can run multiple workers in one instance)"): break setup_systemd(d, config) node = best_node(ping_results) if node: config['node'] = node else: - # search failed, ask the user + # Search failed, ask the user config['node'] = d.prompt( "Search for best BitShares node failed.\n\nPlease enter wss:// url of chosen node.") else: action = d.menu("You have an existing configuration.\nSelect an action:", - [('NEW', 'Create a new bot'), - ('DEL', 'Delete a bot'), - ('EDIT', 'Edit a bot'), + [('NEW', 'Create a new worker'), + ('DEL', 'Delete a worker'), + ('EDIT', 'Edit a worker'), ('CONF', 'Redo general config')]) if action == 'EDIT': - botname = d.menu("Select bot to edit", [(i, i) for i in bots]) - config['bots'][botname] = configure_bot(d, config['bots'][botname]) + worker_name = d.menu("Select worker to edit", [(i, i) for i in workers]) + config['workers'][worker_name] = configure_worker(d, config['workers'][worker_name]) elif action == 'DEL': - botname = d.menu("Select bot to delete", [(i, i) for i in bots]) - del config['bots'][botname] + worker_name = d.menu("Select worker to delete", [(i, i) for i in workers]) + del config['workers'][worker_name] if action == 'NEW': - txt = d.prompt("Your name for the new bot") - config['bots'][txt] = configure_bot(d, {}) + txt = d.prompt("Your name for the new worker") + config['workers'][txt] = configure_worker(d, {}) else: config['node'] = d.prompt("BitShares node to use", default=config['node']) d.clear() return config - - -if __name__ == '__main__': - print(repr(configure({}))) From 56a0952eedc4feeb5b7deebf462a1480371583f9 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 13 Apr 2018 12:03:37 +0300 Subject: [PATCH 0191/1846] Change config.yml save location in GUI and optimise config accessing --- app.py | 13 ++- .../controllers/create_worker_controller.py | 10 +-- dexbot/controllers/main_controller.py | 85 ++++++++----------- 3 files changed, 52 insertions(+), 56 deletions(-) diff --git a/app.py b/app.py index 45992f4b9..8054e440a 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,7 @@ import sys +import os +import appdirs from PyQt5 import Qt from bitshares import BitShares @@ -14,7 +16,14 @@ class App(Qt.QApplication): def __init__(self, sys_argv): super(App, self).__init__(sys_argv) - config = MainController.load_config() + # Make sure config file exists + config_path = os.path.join(appdirs.user_config_dir('dexbot'), 'config.yml') + if not os.path.exists(config_path): + config = {'node': 'wss://bitshares.openledger.info/ws', 'workers': {}} + MainController.create_config(config) + else: + config = MainController.load_config() + bitshares_instance = BitShares(config['node']) # Wallet unlock @@ -26,7 +35,7 @@ def __init__(self, sys_argv): if unlock_view.exec_(): bitshares_instance = unlock_ctrl.bitshares - self.main_ctrl = MainController(bitshares_instance) + self.main_ctrl = MainController(bitshares_instance, config) self.main_view = MainView(self.main_ctrl) self.main_view.show() else: diff --git a/dexbot/controllers/create_worker_controller.py b/dexbot/controllers/create_worker_controller.py index 6e556a216..fa8a3822e 100644 --- a/dexbot/controllers/create_worker_controller.py +++ b/dexbot/controllers/create_worker_controller.py @@ -72,9 +72,8 @@ def is_account_valid(self, account, private_key): else: return False - @staticmethod - def is_account_in_use(account): - workers = MainController.get_workers_data() + def is_account_in_use(self, account): + workers = self.main_ctrl.get_workers_data() for worker_name, worker in workers.items(): if worker['account'] == account: return True @@ -88,13 +87,12 @@ def add_private_key(self, private_key): # Private key already added pass - @staticmethod - def get_unique_worker_name(): + def get_unique_worker_name(self): """ Returns unique worker name "Worker %n", where %n is the next available index """ index = 1 - workers = MainController.get_workers_data().keys() + workers = self.main_ctrl.get_workers_data().keys() worker_name = "Worker {0}".format(index) while worker_name in workers: worker_name = "Worker {0}".format(index) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index e7a4e5e98..9adaef824 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -1,16 +1,21 @@ +import os import logging from dexbot.worker import WorkerInfrastructure -from ruamel.yaml import YAML +import appdirs +from ruamel import yaml from bitshares.instance import set_shared_bitshares_instance +CONFIG_PATH = os.path.join(appdirs.user_config_dir('dexbot'), 'config.yml') + class MainController: - def __init__(self, bitshares_instance): + def __init__(self, bitshares_instance, config): self.bitshares_instance = bitshares_instance set_shared_bitshares_instance(bitshares_instance) + self.config = config self.worker_manager = None # Configure logging @@ -51,60 +56,44 @@ def remove_worker(self, worker_name): WorkerInfrastructure.remove_offline_worker(config, worker_name) @staticmethod - def load_config(): - yaml = YAML() - with open('config.yml', 'r') as f: - return yaml.load(f) + def create_config(config): + with open(CONFIG_PATH, 'w') as f: + yaml.dump(config, f, default_flow_style=False) @staticmethod - def get_workers_data(): - """ - Returns dict of all the workers data - """ - with open('config.yml', 'r') as f: - yaml = YAML() - return yaml.load(f)['workers'] - - @staticmethod - 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: - yaml = YAML() - config = yaml.load(f) - config['workers'] = {worker_name: config['workers'][worker_name]} - return config + def load_config(): + with open(CONFIG_PATH, 'r') as f: + return yaml.safe_load(f) - @staticmethod - def remove_worker_config(worker_name): - yaml = YAML() - with open('config.yml', 'r') as f: - config = yaml.load(f) + def refresh_config(self): + self.config = self.load_config() - config['workers'].pop(worker_name, None) + def get_workers_data(self): + """ Returns dict of all the workers data + """ + return self.config['workers'] - with open("config.yml", "w") as f: - yaml.dump(config, f) + def get_worker_config(self, worker_name): + """ Returns config file data with only the data from a specific worker + """ + config = self.config + config['workers'] = {worker_name: config['workers'][worker_name]} + return config - @staticmethod - def add_worker_config(worker_name, worker_data): - yaml = YAML() - with open('config.yml', 'r') as f: - config = yaml.load(f) + def remove_worker_config(self, worker_name): + self.config['workers'].pop(worker_name, None) - config['workers'][worker_name] = worker_data + with open(CONFIG_PATH, 'w') as f: + yaml.dump(self.config, f) - with open("config.yml", "w") as f: - yaml.dump(config, f) + def add_worker_config(self, worker_name, worker_data): + self.config['workers'][worker_name] = worker_data - @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) + with open(CONFIG_PATH, 'w') as f: + yaml.dump(self.config, f, default_flow_style=False) - workers = config['workers'] + def replace_worker_config(self, worker_name, new_worker_name, worker_data): + workers = self.config['workers'] # Rotate the dict keys to keep order for _ in range(len(workers)): key, value = workers.popitem(False) @@ -113,5 +102,5 @@ def replace_worker_config(worker_name, new_worker_name, worker_data): else: workers[key] = value - with open("config.yml", "w") as f: - yaml.dump(config, f) + with open(CONFIG_PATH, 'w') as f: + yaml.dump(self.config, f, default_flow_style=False) From 2a5e2de2b4918f066e57604641c10776ad9b51d1 Mon Sep 17 00:00:00 2001 From: sergio Date: Thu, 12 Apr 2018 19:37:26 +0200 Subject: [PATCH 0192/1846] - 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 0193/1846] - 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 0194/1846] 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 fcee6247a44555a461c997239978eada09f069e8 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 13 Apr 2018 14:11:03 +0300 Subject: [PATCH 0195/1846] Remove bot.py --- dexbot/bot.py | 152 -------------------------------------------------- 1 file changed, 152 deletions(-) delete mode 100644 dexbot/bot.py diff --git a/dexbot/bot.py b/dexbot/bot.py deleted file mode 100644 index bc09f0e9f..000000000 --- a/dexbot/bot.py +++ /dev/null @@ -1,152 +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. - -# FIXME: currently static list of bot strategies: ? how to enumerate bots available and deploy new bot strategies. -STRATEGIES = [('dexbot.strategies.echo', 'Echo Test')] - - -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 - - 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_dsabled':(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: - 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): - 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.warning("disabled") - 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("The bot %s has been disabled" % 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() From b913fb9eef726ee5392353e2a7078740580e67df Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 17 Apr 2018 14:40:47 +0300 Subject: [PATCH 0196/1846] 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 0197/1846] 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 0198/1846] 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 0199/1846] 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 0200/1846] 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 0201/1846] 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 0202/1846] 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 0203/1846] 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 0204/1846] 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 0205/1846] 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 0206/1846] 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 0207/1846] 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 0208/1846] 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 0209/1846] 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 0210/1846] 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 0211/1846] 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 0212/1846] 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 0213/1846] 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 0214/1846] 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 0215/1846] 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 0216/1846] 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 0217/1846] 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 0218/1846] 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 0219/1846] 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 0220/1846] 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 0221/1846] 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 0222/1846] 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 0223/1846] 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 0224/1846] 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 0225/1846] 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 0226/1846] 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 0227/1846] 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 0228/1846] 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 0229/1846] 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 0230/1846] 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 0231/1846] 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 0232/1846] 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 0233/1846] 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 0234/1846] 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 0235/1846] 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 0236/1846] 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 0237/1846] 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 0238/1846] 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 0239/1846] 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 0240/1846] 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 0241/1846] 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 0242/1846] 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 0243/1846] 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 0244/1846] 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 0245/1846] 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 0246/1846] 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 0247/1846] 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 0248/1846] 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 0249/1846] 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 0250/1846] 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 0251/1846] 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 0252/1846] 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 0253/1846] 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 0254/1846] 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 7c11209e38cce6718868584190dd0fcc51a3a5d1 Mon Sep 17 00:00:00 2001 From: Petri Kanerva Date: Wed, 16 May 2018 15:54:37 +0300 Subject: [PATCH 0255/1846] Change the tab order of first run password setup to be more logical --- dexbot/views/ui/create_wallet_window.ui | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dexbot/views/ui/create_wallet_window.ui b/dexbot/views/ui/create_wallet_window.ui index 4a22f3ead..6e79eb32d 100644 --- a/dexbot/views/ui/create_wallet_window.ui +++ b/dexbot/views/ui/create_wallet_window.ui @@ -171,6 +171,11 @@ + + password_input + confirm_password_input + ok_button + From 81cc44e0cb4072645a3ccc38fc9c71ebdac9393b Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 17 May 2018 12:36:12 +0300 Subject: [PATCH 0256/1846] 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 0257/1846] 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 0258/1846] 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 0259/1846] 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 0260/1846] 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 0261/1846] 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 0262/1846] 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 0263/1846] 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 0264/1846] 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 0265/1846] 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 d3caa25ca51114e1c476726490a46205dd39666c Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Fri, 18 May 2018 18:37:37 +0300 Subject: [PATCH 0266/1846] test --- 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 830fd6b2e..91e940847 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -101,15 +101,16 @@ def replace_order(self, 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 amount = order['quote']['amount'] + self.remove_order(order) new_order = self.market_sell(amount, price) else: # Sell order price = (order['price'] ** -1) / self.spread amount = order['quote']['amount'] + self.remove_order(order) new_order = self.market_buy(amount, price) self.save_order(new_order) From 14d5a27350f172958bef2e919f0195f0dbd87c38 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Fri, 18 May 2018 19:13:01 +0300 Subject: [PATCH 0267/1846] Attempt to fix def place_order --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 91e940847..00f661fa9 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -103,12 +103,12 @@ def replace_order(self, order): self.log.info('Change detected, updating orders') 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'] self.remove_order(order) new_order = self.market_sell(amount, price) else: # Sell order - price = (order['price'] ** -1) / self.spread + price = order['price'] / (1 + self.spread) amount = order['quote']['amount'] self.remove_order(order) new_order = self.market_buy(amount, price) From fa3db927a7d86f675cbb7ff20ea8a5a5c7ba21a6 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Fri, 18 May 2018 20:05:00 +0300 Subject: [PATCH 0268/1846] Remove worker amount limit in gui only to be used with a pretty responsive node, most likely a personal or local one. --- dexbot/views/worker_list.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 832828bcf..ca2e58a00 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -38,9 +38,9 @@ def __init__(self, main_ctrl): # 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 + # 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) @@ -67,8 +67,8 @@ def remove_worker_widget(self, worker_name): self.worker_widgets.pop(worker_name, None) self.num_of_workers -= 1 - if self.num_of_workers < self.max_workers: - self.ui.add_worker_button.setEnabled(True) + # if self.num_of_workers < self.max_workers: + # self.ui.add_worker_button.setEnabled(True) @gui_error def handle_add_worker(self): From 8d6ec2ff04511a9e2a596a43e23036b7218232dd Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Fri, 18 May 2018 20:25:01 +0300 Subject: [PATCH 0269/1846] Update worker_list.py --- dexbot/views/worker_list.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index ca2e58a00..ae5290bfb 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -60,8 +60,8 @@ def add_worker_widget(self, worker_name): self.worker_widgets[worker_name] = widget self.num_of_workers += 1 - if self.num_of_workers >= self.max_workers: - self.ui.add_worker_button.setEnabled(False) + # 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) From 9f63b8e4e4d3956236ed7b3faca9ea9c60c1dff4 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Sat, 19 May 2018 21:15:08 +1000 Subject: [PATCH 0270/1846] 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 0271/1846] 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 0272/1846] 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 0273/1846] 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 0274/1846] 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 0275/1846] 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 0276/1846] 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 0277/1846] 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 0278/1846] 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 0279/1846] 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 0280/1846] 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 0281/1846] 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 0282/1846] 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 0283/1846] 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 0284/1846] 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 0285/1846] 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 0286/1846] 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 0287/1846] 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 0288/1846] 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 0289/1846] 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 0290/1846] 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 0291/1846] 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 0292/1846] 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 0293/1846] 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 0294/1846] 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 273b2a466699d835f2101f0cca97765426e4942a Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 24 May 2018 11:00:58 +1000 Subject: [PATCH 0295/1846] updater crashes if there are no orders to check --- dexbot/strategies/staggered_orders.py | 30 ++++++++++++++++----------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 34b18bf87..f7818c6ba 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -102,6 +102,7 @@ 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('order is {}'.format(order)) if order['base']['symbol'] == self.market['base']['symbol']: # Buy order price = order['price'] * (1 + self.spread) amount = order['quote']['amount'] @@ -247,15 +248,20 @@ def update_gui_profit(self): pass def update_gui_slider(self): - ticker = self.market.ticker() - latest_price = ticker.get('latest').get('price') - 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 - percentage = 50 - else: - percentage = (total_balance['base'] / total) * 100 - idle_add(self.view.set_worker_slider, self.worker_name, percentage) - self['slider'] = percentage + + orders = self.fetch_orders() + if orders: + ticker = self.market.ticker() + if 'latest' in ticker and ticker['latest']: + latest_price = ticker['latest'].get('price') + if latest_price: + order_ids = 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 + percentage = 50 + else: + percentage = (total_balance['base'] / total) * 100 + idle_add(self.view.set_worker_slider, self.worker_name, percentage) + self['slider'] = percentage From 565aa98c13a6d050c8dfcd1ebf0edfcea6f7980f Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 24 May 2018 11:46:58 +1000 Subject: [PATCH 0296/1846] dump order object if not valid --- 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 f7818c6ba..354c370b8 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -102,7 +102,7 @@ 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('order is {}'.format(order)) + assert order['base'], "order is deformed {}".format(dict(order)) if order['base']['symbol'] == self.market['base']['symbol']: # Buy order price = order['price'] * (1 + self.spread) amount = order['quote']['amount'] From 57afa15459735c835cd5b7a8e9b7af5005367668 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 24 May 2018 14:50:44 +0300 Subject: [PATCH 0297/1846] 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 0298/1846] 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 0299/1846] 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 0300/1846] 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 0301/1846] 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 0302/1846] 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 0303/1846] 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 0304/1846] 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 0305/1846] 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 0306/1846] 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 0307/1846] 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 0308/1846] 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 0309/1846] 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']) From 6434664ffa15915d207b142fdec26fd65de8722a Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 25 May 2018 14:01:39 +0300 Subject: [PATCH 0310/1846] Merge stash --- dexbot/basestrategy.py | 10 ++- dexbot/cli_conf.py | 4 + dexbot/config.py | 90 +++++++++++++++++++ .../controllers/create_worker_controller.py | 12 +-- dexbot/controllers/main_controller.py | 59 +----------- dexbot/gui.py | 15 ++-- dexbot/views/worker_item.py | 14 +-- dexbot/views/worker_list.py | 7 +- dexbot/worker.py | 2 +- 9 files changed, 133 insertions(+), 80 deletions(-) create mode 100644 dexbot/config.py diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 19a491458..a666b591c 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -1,10 +1,11 @@ -import collections import logging +import collections import time import math from .storage import Storage from .statemachine import StateMachine +from .config import Config from events import Events import bitsharesapi @@ -103,8 +104,8 @@ def configure(cls): def __init__( self, - config, name, + config=None, onAccount=None, onOrderMatched=None, onOrderPlaced=None, @@ -143,6 +144,11 @@ def __init__( # Redirect this event to also call order placed and order matched self.onMarketUpdate += self._callbackPlaceFillOrders + if config: + self.config = config + else: + self.config = config = Config.get_worker_config_file(name) + self.config = config self.worker = config["workers"][name] self._account = Account( diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 2bc477685..f37634f88 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -18,9 +18,11 @@ import os.path import sys import re + from dexbot.worker import STRATEGIES from dexbot.whiptail import get_whiptail from dexbot.find_node import start_pings, best_node +from dexbot.basestrategy import BaseStrategy SYSTEMD_SERVICE_NAME = os.path.expanduser( "~/.local/share/systemd/user/dexbot.service") @@ -181,6 +183,8 @@ def configure_dexbot(config): elif action == 'DEL': worker_name = d.menu("Select worker to delete", [(i, i) for i in workers]) del config['workers'][worker_name] + strategy = BaseStrategy(worker_name) + strategy.purge() # Cancel the orders of the bot if action == 'NEW': txt = d.prompt("Your name for the new worker") config['workers'][txt] = configure_worker(d, {}) diff --git a/dexbot/config.py b/dexbot/config.py new file mode 100644 index 000000000..6a4b1a479 --- /dev/null +++ b/dexbot/config.py @@ -0,0 +1,90 @@ +import os + +import appdirs +from ruamel import yaml + +CONFIG_PATH = os.path.join(appdirs.user_config_dir('dexbot'), 'config.yml') + + +class Config(dict): + + def __init__(self, config=None): + super().__init__() + if config: + self.config = config + else: + self.config = self.load_config() + + def __setitem__(self, key, value): + self.config[key] = value + + def __getitem__(self, key): + return self.config[key] + + def __delitem__(self, key): + del self.config[key] + + def __contains__(self, key): + return key in self.config + + @staticmethod + def create_config(config): + with open(CONFIG_PATH, 'w') as f: + yaml.dump(config, f, default_flow_style=False) + + @staticmethod + def load_config(): + with open(CONFIG_PATH, 'r') as f: + return yaml.load(f, Loader=yaml.RoundTripLoader) + + def refresh_config(self): + self.config = self.load_config() + + def get_workers_data(self): + """ Returns dict of all the workers data + """ + return self.config['workers'] + + @staticmethod + def get_worker_config_file(worker_name): + """ Returns config file data with only the data from a specific worker. + Config loaded from a file + """ + with open(CONFIG_PATH, 'r') as f: + config = yaml.load(f, Loader=yaml.RoundTripLoader) + + config['workers'] = {worker_name: config['workers'][worker_name]} + return config + + def get_worker_config(self, worker_name): + """ Returns config file data with only the data from a specific worker. + Config loaded from memory + """ + config = self.config + config['workers'] = {worker_name: config['workers'][worker_name]} + return config + + def remove_worker_config(self, worker_name): + self.config['workers'].pop(worker_name, None) + + with open(CONFIG_PATH, 'w') as f: + yaml.dump(self.config, f) + + def add_worker_config(self, worker_name, worker_data): + self.config['workers'][worker_name] = worker_data + + with open(CONFIG_PATH, 'w') as f: + yaml.dump(self.config, f, default_flow_style=False) + + def replace_worker_config(self, worker_name, new_worker_name, worker_data): + workers = self.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_PATH, 'w') as f: + yaml.dump(self.config, f, default_flow_style=False) diff --git a/dexbot/controllers/create_worker_controller.py b/dexbot/controllers/create_worker_controller.py index eed7a4f4c..838f76abf 100644 --- a/dexbot/controllers/create_worker_controller.py +++ b/dexbot/controllers/create_worker_controller.py @@ -47,9 +47,11 @@ def base_assets(self): ] return assets - @staticmethod - def is_worker_name_valid(worker_name): - worker_names = MainController.get_workers_data().keys() + 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.config.get_workers_data().keys() # Check that the name is unique if worker_name in worker_names: return False @@ -88,7 +90,7 @@ def is_account_valid(self, account, private_key): return False def is_account_in_use(self, account): - workers = self.main_ctrl.get_workers_data() + workers = self.main_ctrl.config.get_workers_data() for worker_name, worker in workers.items(): if worker['account'] == account: return True @@ -107,7 +109,7 @@ def get_unique_worker_name(): """ Returns unique worker name "Worker %n", where %n is the next available index """ index = 1 - workers = self.main_ctrl.get_workers_data().keys() + workers = self.main_ctrl.config.get_workers_data().keys() worker_name = "Worker {0}".format(index) while worker_name in workers: worker_name = "Worker {0}".format(index) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 77e9e3349..8c909c2c0 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -1,17 +1,13 @@ -import os import logging import sys from dexbot import config_file, VERSION from dexbot.worker import WorkerInfrastructure +from dexbot.config import Config from dexbot.views.errors import PyQtHandler -import appdirs -from ruamel import yaml from bitshares.instance import set_shared_bitshares_instance -CONFIG_PATH = os.path.join(appdirs.user_config_dir('dexbot'), 'config.yml') - class MainController: @@ -56,59 +52,10 @@ def remove_worker(self, worker_name): self.worker_manager.stop(worker_name) else: # Worker not running - config = self.get_worker_config(worker_name) + config = self.config.get_worker_config(worker_name) WorkerInfrastructure.remove_offline_worker(config, worker_name) else: # Worker manager not running - config = self.get_worker_config(worker_name) + config = self.config.get_worker_config(worker_name) WorkerInfrastructure.remove_offline_worker(config, worker_name) - @staticmethod - def create_config(config): - with open(CONFIG_PATH, 'w') as f: - yaml.dump(config, f, default_flow_style=False) - - @staticmethod - def load_config(): - with open(CONFIG_PATH, 'r') as f: - return yaml.safe_load(f) - - def refresh_config(self): - self.config = self.load_config() - - def get_workers_data(self): - """ Returns dict of all the workers data - """ - return self.config['workers'] - - def get_worker_config(self, worker_name): - """ Returns config file data with only the data from a specific worker - """ - config = self.config - config['workers'] = {worker_name: config['workers'][worker_name]} - return config - - def remove_worker_config(self, worker_name): - self.config['workers'].pop(worker_name, None) - - with open(CONFIG_PATH, 'w') as f: - yaml.dump(self.config, f) - - def add_worker_config(self, worker_name, worker_data): - self.config['workers'][worker_name] = worker_data - - with open(CONFIG_PATH, 'w') as f: - yaml.dump(self.config, f, default_flow_style=False) - - def replace_worker_config(self, worker_name, new_worker_name, worker_data): - workers = self.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_PATH, 'w') as f: - yaml.dump(self.config, f, default_flow_style=False) diff --git a/dexbot/gui.py b/dexbot/gui.py index 0b1dd2c8e..e3714a6d6 100644 --- a/dexbot/gui.py +++ b/dexbot/gui.py @@ -1,10 +1,11 @@ import sys import os -import appdirs +from ruamel import yaml from PyQt5 import Qt from bitshares import BitShares +from dexbot.config import Config, CONFIG_PATH from dexbot.controllers.main_controller import MainController from dexbot.views.worker_list import MainView from dexbot.controllers.wallet_controller import WalletController @@ -17,12 +18,14 @@ def __init__(self, sys_argv): super(App, self).__init__(sys_argv) # Make sure config file exists - config_path = os.path.join(appdirs.user_config_dir('dexbot'), 'config.yml') - if not os.path.exists(config_path): - config = {'node': 'wss://bitshares.openledger.info/ws', 'workers': {}} - MainController.create_config(config) + if not os.path.exists(CONFIG_PATH): + config_data = {'node': 'wss://bitshares.openledger.info/ws', 'workers': {}} + config = Config(config_data) else: - config = MainController.load_config() + config = Config() + + with open(CONFIG_PATH, 'r') as f: + test = yaml.load(f, Loader=yaml.RoundTripLoader) bitshares_instance = BitShares(config['node']) diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 54d2d8ca0..429b1117b 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -11,13 +11,13 @@ class WorkerItemWidget(QtWidgets.QWidget, Ui_widget): - def __init__(self, worker_name, config, main_ctrl, view): + def __init__(self, worker_name, worker_config, main_ctrl, view): super().__init__() self.main_ctrl = main_ctrl self.running = False self.worker_name = worker_name - self.worker_config = config + self.worker_config = worker_config self.view = view self.setupUi(self) @@ -28,7 +28,7 @@ def __init__(self, worker_name, config, main_ctrl, view): 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) + self.setup_ui_data(worker_config) def setup_ui_data(self, config): worker_name = list(config['workers'].keys())[0] @@ -100,7 +100,7 @@ def remove_widget_dialog(self): return_value = dialog.exec_() if return_value: self.remove_widget() - self.main_ctrl.remove_worker_config(self.worker_name) + self.main_ctrl.config.remove_worker_config(self.worker_name) def remove_widget(self): self.main_ctrl.remove_worker(self.worker_name) @@ -124,7 +124,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.main_ctrl.config.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 diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 020256742..afaa9aa5e 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -18,6 +18,7 @@ class MainView(QtWidgets.QMainWindow): def __init__(self, main_ctrl): self.main_ctrl = main_ctrl super(MainView, self).__init__() + self.config = main_ctrl.config self.ui = Ui_MainWindow() self.ui.setupUi(self) self.worker_container = self.ui.verticalLayout @@ -31,7 +32,7 @@ def __init__(self, main_ctrl): self.ui.add_worker_button.clicked.connect(lambda: self.handle_add_worker()) # Load worker widgets from config file - workers = main_ctrl.get_workers_data() + workers = self.config.get_workers_data() for worker_name in workers: self.add_worker_widget(worker_name) @@ -50,7 +51,7 @@ def __init__(self, main_ctrl): self.statusbar_updater.start() def add_worker_widget(self, worker_name): - config = self.main_ctrl.get_worker_config(worker_name) + config = self.config.get_worker_config(worker_name) widget = WorkerItemWidget(worker_name, config, self.main_ctrl, self) widget.setFixedSize(widget.frameSize()) self.worker_container.addWidget(widget) @@ -76,7 +77,7 @@ def handle_add_worker(self): # 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.config.add_worker_config(worker_name, create_worker_dialog.worker_data) self.add_worker_widget(worker_name) def set_worker_name(self, worker_name, value): diff --git a/dexbot/worker.py b/dexbot/worker.py index ffb3045a7..74e81f7e1 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -216,7 +216,7 @@ def remove_market(self, worker_name): @staticmethod def remove_offline_worker(config, worker_name): # Initialize the base strategy to get control over the data - strategy = BaseStrategy(config, worker_name) + strategy = BaseStrategy(worker_name, config) strategy.purge() def do_next_tick(self, job): From d35a83ed796e153efcc4e437fd1f63cbc4c8439a Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 28 May 2018 10:36:40 +1000 Subject: [PATCH 0311/1846] bring cli_conf.py to up-date with upstream changes --- dexbot/cli_conf.py | 71 ++++++++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index ba6a9150e..5eda0628f 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -1,14 +1,15 @@ """ A module to provide an interactive text-based tool for dexbot configuration -The result is takemachine can be run without having to hand-edit config files. +The result is dexbot can be run without having to hand-edit config files. If systemd is detected it will offer to install a user service unit (under ~/.local/share/systemd This requires a per-user systemd process to be runnng -Requires the 'whiptail' tool: so UNIX-like sytems only +Requires the 'whiptail' tool for text-based configuration (so UNIX only) +if not available, falls back to a line-based configurator ("NoWhiptail") Note there is some common cross-UI configuration stuff: look in basestrategy.py It's expected GUI/web interfaces will be re-implementing code in this file, but they should -understand the common code so bot strategy writers can define their configuration once +understand the common code so worker strategy writers can define their configuration once for each strategy class. """ @@ -22,10 +23,20 @@ import re import tempfile import shutil -from dexbot.bot import STRATEGIES + from dexbot.whiptail import get_whiptail from dexbot.find_node import start_pings, best_node + +# FIXME: auto-discovery of strategies would be cool but can't figure out a way +STRATEGIES = [ + {'tag': 'relative', + 'class': 'dexbot.strategies.relative_orders', + 'name': 'Relative Orders'}, + {'tag': 'stagger', + 'class': 'dexbot.strategies.staggered_orders', + 'name': 'Staggered Orders'}] + SYSTEMD_SERVICE_NAME = os.path.expanduser( "~/.local/share/systemd/user/dexbot.service") @@ -55,7 +66,7 @@ def process_config_element(elem, d, config): """ process an item of configuration metadata display a widget as appropriate d: the Dialog object - config: the config dctionary for this bot + config: the config dctionary for this worker """ if elem.type == "string": txt = d.prompt(elem.description, config.get(elem.key, elem.default)) @@ -115,7 +126,9 @@ def setup_systemd(d, config): if not os.path.exists(j): os.mkdir(j) passwd = d.prompt( - "The wallet password entered with uptick\nNOTE: this will be saved on disc so the bot can run unattended. This means anyone with access to this computer's file can spend all your money", + """The wallet password +NOTE: this will be saved on disc so the bot can run unattended. This means +anyone with access to this computer can spend all your money""", password=True) # because we hold password be restrictive fd = os.open(SYSTEMD_SERVICE_NAME, os.O_WRONLY | os.O_CREAT, 0o600) @@ -131,35 +144,43 @@ def setup_systemd(d, config): config['systemd_status'] = 'reject' -def configure_bot(d, bot): - strategy = bot.get('module', 'dexbot.strategies.echo') - bot['module'] = d.radiolist( +def configure_worker(d, worker): + strategy = worker.get('module', 'dexbot.strategies.echo') + for i in STRATEGIES: + if strategy == i['class']: + strategy = i['tag'] + worker['module'] = d.radiolist( "Choose a bot strategy", select_choice( - strategy, STRATEGIES)) + strategy, [(i['tag'], i['name']) for i in STRATEGIES])) + for i in STRATEGIES: + if i['tag'] == worker['module']: + worker['module'] = i['class'] # its always Strategy now, for backwards compatibilty only - bot['bot'] = 'Strategy' - # import the bot class but we don't __init__ it here + worker['bot'] = 'Strategy' + # import the strategy class but we don't __init__ it here klass = getattr( - importlib.import_module(bot["module"]), - bot["bot"] + importlib.import_module(worker["module"]), + 'Strategy' ) # use class metadata for per-bot configuration configs = klass.configure() if configs: for c in configs: - process_config_element(c, d, bot) + process_config_element(c, d, worker) else: - d.alert("This bot type does not have configuration information. You will have to check the bot code and add configuration values to config.yml if required") - return bot + d.alert("This strategy does not have configuration information. You will have to check the worker code and add configuration values to config.yml manually") + return worker + def configure_dexbot(config): d = get_whiptail() - bots = config.get('bots', {}) - if len(bots) == 0: + workers = config.get('workers', {}) + config['workers'] = workers + if len(workers) == 0: ping_results = start_pings() while True: txt = d.prompt("Your name for the bot") - config['bots'] = {txt: configure_bot(d, {})} + config['workers'][txt] = configure_worker(d, {}) if not d.confirm("Set up another bot?\n(DEXBOt can run multiple bots in one instance)"): break setup_systemd(d, config) @@ -177,16 +198,16 @@ def configure_dexbot(config): ('EDIT', 'Edit a bot'), ('CONF', 'Redo general config')]) if action == 'EDIT': - botname = d.menu("Select bot to edit", [(i, i) for i in bots]) - config['bots'][botname] = configure_bot(d, config['bots'][botname]) + botname = d.menu("Select bot to edit", [(i, i) for i in workers]) + config['workers'][botname] = configure_worker(d, config['workers'][botname]) elif action == 'DEL': botname = d.menu("Select bot to delete", [(i, i) for i in bots]) - del config['bots'][botname] + del config['workers'][botname] if action == 'NEW': txt = d.prompt("Your name for the new bot") - config['bots'][txt] = configure_bot(d, {}) + config['workers'][txt] = configure_bot(d, {}) else: - config['node'] = d.prompt("BitShares node to use",default=config['node']) + config['node'] = d.prompt("BitShares node to use", default=config['node']) d.clear() return config From 8b4876014c54f78ea5512a7d57d79f1e7e0ced98 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 28 May 2018 11:21:03 +1000 Subject: [PATCH 0312/1846] easyconfigurise the strategies --- dexbot/strategies/relative_orders.py | 11 ++++++++++- dexbot/strategies/staggered_orders.py | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 64a38b93a..28ea449d2 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -1,6 +1,6 @@ import math -from dexbot.basestrategy import BaseStrategy +from dexbot.basestrategy import BaseStrategy, ConfigElement from dexbot.queue.idle_queue import idle_add @@ -8,6 +8,15 @@ class Strategy(BaseStrategy): """ Relative Orders strategy """ + @classmethod + def configure(cls): + return BaseStrategy.configure(cls) + [ + ConfigElement('center_price_dynamic', 'bool', False, "Dynamic centre price", None), + ConfigElement('amount_relative', 'bool', False, + "Amount is expressed as a percentage of the account balance of quote/base asset", None), + ConfigElement('amount', 'float', 1.0, 'The amount of buy/sell orders', (0.0, None)), + ConfigElement('spread', 'float', 5.0, 'The percentage difference between buy and sell', (0.0, 100.0))] + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.log.info("Initializing Relative Orders") diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 574acee56..4427bfc6d 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1,6 +1,6 @@ import math -from dexbot.basestrategy import BaseStrategy +from dexbot.basestrategy import BaseStrategy, ConfigElement from dexbot.queue.idle_queue import idle_add @@ -8,6 +8,15 @@ class Strategy(BaseStrategy): """ Staggered Orders strategy """ + @classmethod + def configure(cls): + return BaseStrategy.configure(cls) + [ + ConfigElement('upper_bound', 'float', 1.0, 'The top price in the range', (0.0, None)), + ConfigElement('lower_bound', 'float', 1.0, 'The bottom price in the range', (0.0, None)), + ConfigElement('increment', 'float', 1.0, 'The percentage difference between staggered orders', (0.0, 100.0)), + ConfigElement('amount', 'float', 1.0, 'The amount of buy/sell orders', (0.0, None)), + ConfigElement('spread', 'float', 5.0, 'The percentage difference between buy and sell', (0.0, 100.0))] + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.log.info("Initializing Staggered Orders") From bc15e8133c19e37c4b7bb7ec62c9789810aa57c0 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 28 May 2018 11:30:36 +1000 Subject: [PATCH 0313/1846] make setup.py work where PyQt5 isn't available --- setup.py | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/setup.py b/setup.py index 701716022..7ba78686a 100755 --- a/setup.py +++ b/setup.py @@ -2,7 +2,28 @@ from setuptools import setup, find_packages from distutils.command import build as build_module -from pyqt_distutils.build_ui import build_ui +cmdclass = {} +console_scripts = ['dexbot-cli = dexbot.cli:main'] +install_requires = [ + "bitshares==0.1.16", + "uptick>=0.1.4", + "click", + "sqlalchemy", + "appdirs", + "sdnotify", + "ruamel.yaml>=0.15.37" +] + +try: + from pyqt_distutils.build_ui import build_ui + cmdclass = { + 'build_ui': build_ui, + 'build': BuildCommand + } + console_scripts.append('dexbot-gui = dexbot.gui:main') + install_requires.extend("pyqt5", "pyqt-distutils") +except: + print("GUI not available") from dexbot import VERSION, APP_NAME @@ -32,27 +53,11 @@ def run(self): 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', ], - cmdclass={ - 'build_ui': build_ui, - 'build': BuildCommand - }, + cmdclass=cmdclass, entry_points={ - 'console_scripts': [ - 'dexbot-cli = dexbot.cli:main', - 'dexbot-gui = dexbot.gui:main', - ], + 'console_scripts': console_scripts }, - install_requires=[ - "bitshares==0.1.16", - "uptick>=0.1.4", - "click", - "sqlalchemy", - "appdirs", - "pyqt5", - "sdnotify", - 'pyqt-distutils', - "ruamel.yaml>=0.15.37" - ], + install_requires=install_requires, 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" From 2a0adddc187c017dc0360dab2bd4c38cd958ff78 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 28 May 2018 12:16:19 +1000 Subject: [PATCH 0314/1846] fix bot/worker references --- dexbot/cli_conf.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 5eda0628f..8167c3ec4 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -198,12 +198,12 @@ def configure_dexbot(config): ('EDIT', 'Edit a bot'), ('CONF', 'Redo general config')]) if action == 'EDIT': - botname = d.menu("Select bot to edit", [(i, i) for i in workers]) - config['workers'][botname] = configure_worker(d, config['workers'][botname]) + workername = d.menu("Select bot to edit", [(i, i) for i in workers]) + config['workers'][workername] = configure_worker(d, config['workers'][botname]) elif action == 'DEL': - botname = d.menu("Select bot to delete", [(i, i) for i in bots]) - del config['workers'][botname] - if action == 'NEW': + workername = d.menu("Select bot to delete", [(i, i) for i in workers]) + del config['workers'][workername] + elif action == 'NEW': txt = d.prompt("Your name for the new bot") config['workers'][txt] = configure_bot(d, {}) else: From 18d69a53b7870a1aa5365550df1c0350ac70bd97 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 28 May 2018 12:40:09 +1000 Subject: [PATCH 0315/1846] fixes to get relative orders working --- dexbot/basestrategy.py | 4 ++-- dexbot/strategies/relative_orders.py | 3 ++- dexbot/worker.py | 6 ++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 1d6280950..9c5c3cef1 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -24,7 +24,7 @@ # I want this to be UI-agnostic so a future web or GUI interface can use it too # so each bot can have a class method 'configure' which returns a list of ConfigElement # named tuples. tuple fields as follows. -# key: the key in the bot config dictionary that gets saved back to config.yml +# key: the key in the worker config dictionary that gets saved back to config.yml # type: one of "int", "float", "bool", "string", "choice" # default: the default value. must be right type. # description: comments to user, full sentences encouraged @@ -90,7 +90,7 @@ def configure(kls): Return a list of ConfigElement objects defining the configuration values for this class User interfaces should then generate widgets based on this values, gather - data and save back to the config dictionary for the bot. + data and save back to the config dictionary for the worker. NOTE: when overriding you almost certainly will want to call the ancestor and then add your config values to the list. diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 28ea449d2..37fef46cb 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -10,8 +10,9 @@ class Strategy(BaseStrategy): @classmethod def configure(cls): - return BaseStrategy.configure(cls) + [ + return BaseStrategy.configure() + [ ConfigElement('center_price_dynamic', 'bool', False, "Dynamic centre price", None), + ConfigElement('center_price', 'float', 0.0, "Initial center price", (0, 0, None)), ConfigElement('amount_relative', 'bool', False, "Amount is expressed as a percentage of the account balance of quote/base asset", None), ConfigElement('amount', 'float', 1.0, 'The amount of buy/sell orders', (0.0, None)), diff --git a/dexbot/worker.py b/dexbot/worker.py index 66de545df..ff4ddd87b 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -85,9 +85,11 @@ def init_workers(self, config): def update_notify(self): if not self.config['workers']: - log.critical("No workers to launch, exiting") + log.critical("No workers configured to launch, exiting") + raise errors.NoWorkersAvailable() + if not self.workers: + log.critical("No workers actually running") raise errors.NoWorkersAvailable() - if self.notify: # Update the notification instance self.notify.reset_subscriptions(list(self.accounts), list(self.markets)) From a9f28f4698ec9a94b021a175dc5c11de0bcb28cd Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 28 May 2018 12:52:14 +1000 Subject: [PATCH 0316/1846] get staggered orders to start --- 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 4427bfc6d..8a1d34d30 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -10,7 +10,7 @@ class Strategy(BaseStrategy): @classmethod def configure(cls): - return BaseStrategy.configure(cls) + [ + return BaseStrategy.configure() + [ ConfigElement('upper_bound', 'float', 1.0, 'The top price in the range', (0.0, None)), ConfigElement('lower_bound', 'float', 1.0, 'The bottom price in the range', (0.0, None)), ConfigElement('increment', 'float', 1.0, 'The percentage difference between staggered orders', (0.0, 100.0)), From 70376507945b0b0a64ee78dd3a920ff0b95bad9a Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 28 May 2018 13:08:01 +1000 Subject: [PATCH 0317/1846] staggered orders needs a while to set up --- dexbot/cli_conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 8167c3ec4..667703b7b 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -48,6 +48,7 @@ Type=notify WorkingDirectory={homedir} ExecStart={exe} --systemd run +TimeoutSec=20m Environment=PYTHONUNBUFFERED=true Environment=UNLOCK={passwd} From 6a0252dd621d7126878cb07377fb588d38a7d665 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 28 May 2018 14:33:13 +1000 Subject: [PATCH 0318/1846] fix for GUI mode --- setup.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index 7ba78686a..0c2addda4 100755 --- a/setup.py +++ b/setup.py @@ -14,6 +14,13 @@ "ruamel.yaml>=0.15.37" ] + +class BuildCommand(build_module.build): + def run(self): + self.run_command('build_ui') + build_module.build.run(self) + + try: from pyqt_distutils.build_ui import build_ui cmdclass = { @@ -21,19 +28,13 @@ 'build': BuildCommand } console_scripts.append('dexbot-gui = dexbot.gui:main') - install_requires.extend("pyqt5", "pyqt-distutils") -except: - print("GUI not available") + install_requires.extend(["pyqt-distutils"]) +except BaseException as e: + print("GUI not available: {}".format(e)) from dexbot import VERSION, APP_NAME -class BuildCommand(build_module.build): - def run(self): - self.run_command('build_ui') - build_module.build.run(self) - - setup( name=APP_NAME, version=VERSION, From 18fd9b634abf7a625f9e299953335a8cdd38415e Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Mon, 28 May 2018 10:04:00 +0300 Subject: [PATCH 0319/1846] Fixed missing increment from initial spread of Staggered Orders Changed "offset_center_price" to work symmetrically --- dexbot/basestrategy.py | 4 ++-- dexbot/strategies/staggered_orders.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 06cf73738..014e1faa9 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -182,8 +182,8 @@ def calculate_offset_center_price(self, spread, center_price=None, order_ids=Non percentage = 0 else: percentage = (total_balance['base'] / total) - lowest_price = center_price / math.sqrt(1 + spread) - highest_price = center_price * math.sqrt(1 + spread) + lowest_price = center_price / (1 + spread / 2) + highest_price = center_price * (1 + spread / 2) offset_center_price = ((highest_price - lowest_price) * percentage) + lowest_price return offset_center_price diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 93911042f..db3278d06 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -166,10 +166,10 @@ def check_orders(self, *args, **kwargs): @staticmethod def calculate_buy_prices(center_price, spread, increment, lower_bound): buy_prices = [] - if lower_bound > center_price / math.sqrt(1 + spread): + if lower_bound > center_price / math.sqrt(1 + increment + spread): return buy_prices - buy_price = center_price / math.sqrt(1 + spread) + buy_price = center_price / math.sqrt(1 + increment + spread) while buy_price > lower_bound: buy_prices.append(buy_price) buy_price = buy_price / (1 + increment) @@ -178,10 +178,10 @@ def calculate_buy_prices(center_price, spread, increment, lower_bound): @staticmethod def calculate_sell_prices(center_price, spread, increment, upper_bound): sell_prices = [] - if upper_bound < center_price * math.sqrt(1 + spread): + if upper_bound < center_price * math.sqrt(1 + increment + spread): return sell_prices - sell_price = center_price * math.sqrt(1 + spread) + sell_price = center_price * math.sqrt(1 + increment spread) while sell_price < upper_bound: sell_prices.append(sell_price) sell_price = sell_price * (1 + increment) From 4480450acf479a0ba32e0578f09ccea435d57b9d Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 28 May 2018 17:46:43 +1000 Subject: [PATCH 0320/1846] var name not changed --- dexbot/cli_conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 667703b7b..339254949 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -200,7 +200,7 @@ def configure_dexbot(config): ('CONF', 'Redo general config')]) if action == 'EDIT': workername = d.menu("Select bot to edit", [(i, i) for i in workers]) - config['workers'][workername] = configure_worker(d, config['workers'][botname]) + config['workers'][workername] = configure_worker(d, config['workers'][workername]) elif action == 'DEL': workername = d.menu("Select bot to delete", [(i, i) for i in workers]) del config['workers'][workername] From d0c764f707e6d4085d00e0714e810cb42827cefa Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 28 May 2018 17:53:56 +1000 Subject: [PATCH 0321/1846] use / instead of : as market separator --- dexbot/basestrategy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 9c5c3cef1..29ebff129 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -98,8 +98,8 @@ def configure(kls): # these configs are common to all bots return [ ConfigElement("account", "string", "", "BitShares account name for the bot to operate with", ""), - ConfigElement("market", "string", "USD:BTS", - "BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"", "[A-Z]+:[A-Z]+") + ConfigElement("market", "string", "USD/BTS", + "BitShares market to operate on, in the format ASSET/OTHERASSET, for example \"USD/BTS\"", r"[A-Z\.]+/[A-Z\.]+") ] def __init__( From 4acffa505bd719e5296fc7f20a0a80791b7afc46 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 28 May 2018 17:54:48 +1000 Subject: [PATCH 0322/1846] whiptail really, really doesn't like floats --- dexbot/cli_conf.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 339254949..a5c19574d 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -81,10 +81,7 @@ def process_config_element(elem, d, config): if elem.type == "bool": config[elem.key] = d.confirm(elem.description) if elem.type in ("float", "int"): - txt = d.prompt( - elem.description, config.get( - elem.key, str( - elem.default))) + txt = d.prompt(elem.description, str(config.get(elem.key, elem.default))) while True: try: if elem.type == "int": @@ -99,10 +96,7 @@ def process_config_element(elem, d, config): break except ValueError: d.alert("Not a valid value") - txt = d.prompt( - elem.description, config.get( - elem.key, str( - elem.default))) + txt = d.prompt(elem.description, str(config.get(elem.key, elem.default))) config[elem.key] = val if elem.type == "choice": config[elem.key] = d.radiolist(elem.description, select_choice( From 6c8e545f0ee6c02d419d3c5bb0bc4f630c0b62b8 Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Mon, 28 May 2018 20:56:52 +1000 Subject: [PATCH 0323/1846] use the offical DEXBot node --- dexbot/__init__.py | 2 +- dexbot/cli.py | 12 ++++----- dexbot/cli_conf.py | 10 -------- dexbot/find_node.py | 60 --------------------------------------------- 4 files changed, 7 insertions(+), 77 deletions(-) delete mode 100644 dexbot/find_node.py diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 9a1effc88..a4e21117d 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -12,7 +12,7 @@ config_file = os.path.join(config_dir, "config.yml") default_config = """ -node: wss://bitshares.openledger.info/ws +node: wss://status200.bitshares.apasia.tech/ws workers: {} """ diff --git a/dexbot/cli.py b/dexbot/cli.py index 610f765d6..ed3ac43f4 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -12,7 +12,7 @@ import appdirs from ruamel import yaml -from dexbot import config_file +from dexbot import config_file, default_config from dexbot.ui import ( verbose, chain, @@ -114,12 +114,12 @@ def configure(ctx): """ Interactively configure dexbot """ cfg_file = ctx.obj["configfile"] - if os.path.exists(ctx.obj['configfile']): - with open(ctx.obj["configfile"]) as fd: - config = yaml.safe_load(fd) - else: - config = {} + if not os.path.exists(ctx.obj['configfile']): storage.mkdir_p(os.path.dirname(ctx.obj['configfile'])) + with open(ctx.obj['configfile'], 'w') as fd: + fd.write(default_config) + with open(ctx.obj["configfile"]) as fd: + config = yaml.safe_load(fd) configure_dexbot(config) with open(cfg_file, "w") as fd: yaml.dump(config, fd, default_flow_style=False) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index a5c19574d..d86f7a710 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -25,8 +25,6 @@ import shutil from dexbot.whiptail import get_whiptail -from dexbot.find_node import start_pings, best_node - # FIXME: auto-discovery of strategies would be cool but can't figure out a way STRATEGIES = [ @@ -172,20 +170,12 @@ def configure_dexbot(config): workers = config.get('workers', {}) config['workers'] = workers if len(workers) == 0: - ping_results = start_pings() while True: txt = d.prompt("Your name for the bot") config['workers'][txt] = configure_worker(d, {}) if not d.confirm("Set up another bot?\n(DEXBOt can run multiple bots in one instance)"): break setup_systemd(d, config) - node = best_node(ping_results) - if node: - config['node'] = node - else: - # search failed, ask the user - config['node'] = d.prompt( - "Search for best BitShares node failed.\n\nPlease enter wss:// url of chosen node.") else: action = d.menu("You have an existing configuration.\nSelect an action:", [('NEW', 'Create a new bot'), diff --git a/dexbot/find_node.py b/dexbot/find_node.py deleted file mode 100644 index e146c9e62..000000000 --- a/dexbot/find_node.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Routines for finding the closest node -""" - -# list kindly provided by Cryptick from the DEXBot Telegram channel -ALL_NODES = ["wss://eu.openledger.info/ws", - "wss://bitshares.openledger.info/ws", - "wss://dexnode.net/ws", - "wss://japan.bitshares.apasia.tech/ws", - "wss://bitshares-api.wancloud.io/ws", - "wss://openledger.hk/ws", - "wss://bitshares.apasia.tech/ws", - "wss://uptick.rocks" - "wss://bitshares.crypto.fans/ws", - "wss://kc-us-dex.xeldal.com/ws", - "wss://api.bts.blckchnd.com", - "wss://btsza.co.za:8091/ws", - "wss://bitshares.dacplay.org/ws", - "wss://bit.btsabc.org/ws", - "wss://bts.ai.la/ws", - "wss://ws.gdex.top", - "wss://us.nodes.bitshares.works", - "wss://eu.nodes.bitshares.works", - "wss://sg.nodes.bitshares.works"] - -import re -from urllib.parse import urlsplit -from subprocess import Popen, STDOUT, PIPE -from platform import system - -if system() == 'Windows': - ping_cmd = lambda x: ('ping','-n','5','-w','1500',x) - ping_re = re.compile(r'Average = ([\d.]+)ms') -else: - ping_cmd = lambda x: ('ping','-c5','-n','-w5','-i0.3',x) - ping_re = re.compile(r'min/avg/max/mdev = [\d.]+/([\d.]+)') - -def make_ping_proc(host): - host = urlsplit(host).netloc.split(':')[0] - return Popen(ping_cmd(host),stdout=PIPE, stderr=STDOUT, universal_newlines=True) - -def process_ping_result(host,proc): - out = proc.communicate()[0] - try: - return (float(ping_re.search(out).group(1)),host) - except AttributeError: - return (1000000,host) # hosts that fail are last - -def start_pings(): - return [(i,make_ping_proc(i)) for i in ALL_NODES] - -def best_node(results): - try: - r = sorted([process_ping_result(*i) for i in results]) - return r[0][1] - except: - return None - -if __name__=='__main__': - print(best_node(start_pings())) From 707d4d29e3afdcfb5893105a8119798558e35fbc Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 28 May 2018 14:31:05 +0300 Subject: [PATCH 0324/1846] Fix syntax error and add sqrt to center price calculation --- dexbot/__init__.py | 2 +- dexbot/basestrategy.py | 4 ++-- dexbot/strategies/staggered_orders.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 9a1effc88..2c167b4cb 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.3' +VERSION = '0.2.4' AUTHOR = "codaone" __version__ = VERSION diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 014e1faa9..06cf73738 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -182,8 +182,8 @@ def calculate_offset_center_price(self, spread, center_price=None, order_ids=Non percentage = 0 else: percentage = (total_balance['base'] / total) - lowest_price = center_price / (1 + spread / 2) - highest_price = center_price * (1 + spread / 2) + 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 diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index db3278d06..1181ca697 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -181,7 +181,7 @@ def calculate_sell_prices(center_price, spread, increment, upper_bound): if upper_bound < center_price * math.sqrt(1 + increment + spread): return sell_prices - sell_price = center_price * math.sqrt(1 + increment spread) + sell_price = center_price * math.sqrt(1 + increment + spread) while sell_price < upper_bound: sell_prices.append(sell_price) sell_price = sell_price * (1 + increment) From 26e4ecf9d017919f32b8b95ed71d98b0c23b545a Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 29 May 2018 19:14:11 +0500 Subject: [PATCH 0325/1846] Do not cancel all orders in place_orders() Previous staggered blockchain orders should be preserved after restarting the worker. --- dexbot/strategies/staggered_orders.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 1181ca697..d717c30d9 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -137,7 +137,6 @@ def place_order(self, order): 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(): if not self.get_order(order_id): From a1283593c26cbd1f3d5a3e5ade76c77df7daa45d Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 30 May 2018 08:00:15 +0300 Subject: [PATCH 0326/1846] Change dexbot version number to 0.2.5 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 2c167b4cb..c0d9881bf 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.4' +VERSION = '0.2.5' AUTHOR = "codaone" __version__ = VERSION From e1f0ad982a9838673e4960fe88177e0721db47c8 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 30 May 2018 12:09:25 +0300 Subject: [PATCH 0327/1846] Change config object logic Also do some code clean up --- dexbot/__init__.py | 19 ---- dexbot/basestrategy.py | 1 - dexbot/config.py | 98 +++++++++++++------ .../controllers/create_worker_controller.py | 17 ++-- dexbot/controllers/main_controller.py | 3 +- dexbot/gui.py | 15 +-- dexbot/views/worker_item.py | 36 +++---- dexbot/views/worker_list.py | 17 ++-- 8 files changed, 107 insertions(+), 99 deletions(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 9a1effc88..256c4880f 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,23 +1,4 @@ -import pathlib -import os -from appdirs import user_config_dir - APP_NAME = "dexbot" VERSION = '0.2.3' AUTHOR = "codaone" __version__ = VERSION - - -config_dir = user_config_dir(APP_NAME, appauthor=AUTHOR) -config_file = os.path.join(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/basestrategy.py b/dexbot/basestrategy.py index 1c50a7c60..fe352dd1d 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -150,7 +150,6 @@ def __init__( else: self.config = config = Config.get_worker_config_file(name) - self.config = config self.worker = config["workers"][name] self._account = Account( self.worker["account"], diff --git a/dexbot/config.py b/dexbot/config.py index 6a4b1a479..30941ccb2 100644 --- a/dexbot/config.py +++ b/dexbot/config.py @@ -1,57 +1,76 @@ import os +import pathlib import appdirs from ruamel import yaml +from collections import OrderedDict -CONFIG_PATH = os.path.join(appdirs.user_config_dir('dexbot'), 'config.yml') +CONFIG_DIR = appdirs.user_config_dir('dexbot') +CONFIG_FILE = os.path.join(CONFIG_DIR, 'config.yml') -class Config(dict): + +class Config: def __init__(self, config=None): - super().__init__() if config: - self.config = config + self.create_config(config) + self._config = self.load_config() else: - self.config = self.load_config() + if not os.path.isfile(CONFIG_FILE): + self.create_config(self.default_data) + self._config = self.load_config() def __setitem__(self, key, value): - self.config[key] = value + self._config[key] = value def __getitem__(self, key): - return self.config[key] + return self._config[key] def __delitem__(self, key): - del self.config[key] + del self._config[key] def __contains__(self, key): - return key in self.config + return key in self._config + + @property + def default_data(self): + return {'node': 'wss://status200.bitshares.apasia.tech/ws', 'workers': {}} + + @property + def workers_data(self): + """ Returns dict of all the workers data + """ + return self._config['workers'] + + def dict(self): + """ Returns a dict instance of the stored data + """ + return self._config @staticmethod def create_config(config): - with open(CONFIG_PATH, 'w') as f: + if not os.path.exists(CONFIG_DIR): + pathlib.Path(CONFIG_DIR).mkdir(parents=True, exist_ok=True) + + with open(CONFIG_FILE, 'w') as f: yaml.dump(config, f, default_flow_style=False) @staticmethod def load_config(): - with open(CONFIG_PATH, 'r') as f: - return yaml.load(f, Loader=yaml.RoundTripLoader) + with open(CONFIG_FILE, 'r') as f: + return Config.ordered_load(f, loader=yaml.SafeLoader) def refresh_config(self): - self.config = self.load_config() - - def get_workers_data(self): - """ Returns dict of all the workers data - """ - return self.config['workers'] + self._config = self.load_config() @staticmethod def get_worker_config_file(worker_name): """ Returns config file data with only the data from a specific worker. Config loaded from a file """ - with open(CONFIG_PATH, 'r') as f: - config = yaml.load(f, Loader=yaml.RoundTripLoader) + with open(CONFIG_FILE, 'r') as f: + config = Config.ordered_load(f, loader=yaml.SafeLoader) config['workers'] = {worker_name: config['workers'][worker_name]} return config @@ -60,24 +79,24 @@ def get_worker_config(self, worker_name): """ Returns config file data with only the data from a specific worker. Config loaded from memory """ - config = self.config - config['workers'] = {worker_name: config['workers'][worker_name]} + config = self._config.copy() + config['workers'] = OrderedDict({worker_name: config['workers'][worker_name]}) return config def remove_worker_config(self, worker_name): - self.config['workers'].pop(worker_name, None) + self._config['workers'].pop(worker_name, None) - with open(CONFIG_PATH, 'w') as f: - yaml.dump(self.config, f) + with open(CONFIG_FILE, 'w') as f: + yaml.dump(self._config, f) def add_worker_config(self, worker_name, worker_data): - self.config['workers'][worker_name] = worker_data + self._config['workers'][worker_name] = worker_data - with open(CONFIG_PATH, 'w') as f: - yaml.dump(self.config, f, default_flow_style=False) + with open(CONFIG_FILE, 'w') as f: + yaml.dump(self._config, f, default_flow_style=False) def replace_worker_config(self, worker_name, new_worker_name, worker_data): - workers = self.config['workers'] + workers = self._config['workers'] # Rotate the dict keys to keep order for _ in range(len(workers)): key, value = workers.popitem(False) @@ -86,5 +105,22 @@ def replace_worker_config(self, worker_name, new_worker_name, worker_data): else: workers[key] = value - with open(CONFIG_PATH, 'w') as f: - yaml.dump(self.config, f, default_flow_style=False) + with open(CONFIG_FILE, 'w') as f: + yaml.dump(self._config, f, default_flow_style=False) + + @staticmethod + def ordered_load(stream, loader=None, object_pairs_hook=OrderedDict): + if loader is None: + loader = yaml.UnsafeLoader + + class OrderedLoader(loader): + pass + + def construct_mapping(mapping_loader, node): + mapping_loader.flatten_mapping(node) + return object_pairs_hook(mapping_loader.construct_pairs(node)) + + OrderedLoader.add_constructor( + yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, + construct_mapping) + return yaml.load(stream, OrderedLoader) diff --git a/dexbot/controllers/create_worker_controller.py b/dexbot/controllers/create_worker_controller.py index 838f76abf..26b132ad6 100644 --- a/dexbot/controllers/create_worker_controller.py +++ b/dexbot/controllers/create_worker_controller.py @@ -1,7 +1,7 @@ import collections from dexbot.views.errors import gui_error -from dexbot.controllers.main_controller import MainController +from dexbot.config import Config from dexbot.views.notice import NoticeDialog from dexbot.views.confirmation import ConfirmationDialog from dexbot.views.strategy_form import StrategyFormWidget @@ -47,11 +47,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.config.get_workers_data().keys() + @staticmethod + def is_worker_name_valid(worker_name): + worker_names = Config().workers_data.keys() # Check that the name is unique if worker_name in worker_names: return False @@ -89,8 +87,9 @@ def is_account_valid(self, account, private_key): else: return False - def is_account_in_use(self, account): - workers = self.main_ctrl.config.get_workers_data() + @staticmethod + def is_account_in_use(account): + workers = Config().workers_data for worker_name, worker in workers.items(): if worker['account'] == account: return True @@ -109,7 +108,7 @@ def get_unique_worker_name(): """ Returns unique worker name "Worker %n", where %n is the next available index """ index = 1 - workers = self.main_ctrl.config.get_workers_data().keys() + workers = Config().workers_data.keys() worker_name = "Worker {0}".format(index) while worker_name in workers: worker_name = "Worker {0}".format(index) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index b073711fe..759073976 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -1,9 +1,8 @@ import logging import sys -from dexbot import config_file, VERSION +from dexbot import VERSION from dexbot.worker import WorkerInfrastructure -from dexbot.config import Config from dexbot.views.errors import PyQtHandler from bitshares.instance import set_shared_bitshares_instance diff --git a/dexbot/gui.py b/dexbot/gui.py index e3714a6d6..5a1ab4997 100644 --- a/dexbot/gui.py +++ b/dexbot/gui.py @@ -1,11 +1,9 @@ import sys -import os -from ruamel import yaml from PyQt5 import Qt from bitshares import BitShares -from dexbot.config import Config, CONFIG_PATH +from dexbot.config import Config from dexbot.controllers.main_controller import MainController from dexbot.views.worker_list import MainView from dexbot.controllers.wallet_controller import WalletController @@ -17,16 +15,7 @@ class App(Qt.QApplication): def __init__(self, sys_argv): super(App, self).__init__(sys_argv) - # Make sure config file exists - if not os.path.exists(CONFIG_PATH): - config_data = {'node': 'wss://bitshares.openledger.info/ws', 'workers': {}} - config = Config(config_data) - else: - config = Config() - - with open(CONFIG_PATH, 'r') as f: - test = yaml.load(f, Loader=yaml.RoundTripLoader) - + config = Config() bitshares_instance = BitShares(config['node']) # Wallet unlock diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 94860961f..3fa02bbc1 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -1,23 +1,22 @@ from .ui.worker_item_widget_ui import Ui_widget from .confirmation import ConfirmationDialog from .edit_worker import EditWorkerView +from .errors import gui_error from dexbot.storage import db_worker from dexbot.controllers.create_worker_controller import CreateWorkerController -from dexbot.views.errors import gui_error - from PyQt5 import QtWidgets class WorkerItemWidget(QtWidgets.QWidget, Ui_widget): - def __init__(self, worker_name, worker_config, main_ctrl, view): + def __init__(self, worker_name, main_ctrl, view): super().__init__() self.main_ctrl = main_ctrl self.running = False self.worker_name = worker_name - self.worker_config = worker_config + self.worker_config = self.main_ctrl.config.get_worker_config(worker_name) self.view = view self.setupUi(self) @@ -25,10 +24,10 @@ def __init__(self, worker_name, worker_config, main_ctrl, view): 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.remove_button.clicked.connect(lambda: self.handle_remove_worker()) self.edit_button.clicked.connect(lambda: self.handle_edit_worker()) - self.setup_ui_data(worker_config) + self.setup_ui_data(self.worker_config) def setup_ui_data(self, config): worker_name = list(config['workers'].keys())[0] @@ -96,24 +95,27 @@ def set_worker_profit(self, value): def set_worker_slider(self, value): self.order_slider.setSliderPosition(value) + def set_status(self, status): + self.worker_status.setText(status) + @gui_error - def remove_widget_dialog(self): - dialog = ConfirmationDialog('Are you sure you want to remove worker "{}"?'.format(self.worker_name)) + def handle_remove_worker(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.config.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) - self.view.ui.add_worker_button.setEnabled(True) + self.main_ctrl.config.remove_worker_config(self.worker_name) + self.deleteLater() def reload_widget(self, worker_name): """ Reload the data of the widget """ - self.worker_config = self.main_ctrl.get_worker_config(worker_name) + self.worker_config = self.main_ctrl.config.get_worker_config(worker_name) self.setup_ui_data(self.worker_config) self._pause_worker() @@ -126,10 +128,10 @@ def handle_edit_worker(self): # User clicked save if return_value: new_worker_name = edit_worker_dialog.worker_name + self.view.change_worker_widget_name(self.worker_name, new_worker_name) + self.main_ctrl.remove_worker(self.worker_name) self.main_ctrl.config.replace_worker_config(self.worker_name, - new_worker_name, edit_worker_dialog.worker_data) - self.reload_widget(self.worker_name, new_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 010112d5c..e4d03bf33 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -33,7 +33,7 @@ def __init__(self, main_ctrl): self.ui.add_worker_button.clicked.connect(lambda: self.handle_add_worker()) # Load worker widgets from config file - workers = self.config.get_workers_data() + workers = self.config.workers_data for worker_name in workers: self.add_worker_widget(worker_name) @@ -52,8 +52,7 @@ def __init__(self, main_ctrl): self.statusbar_updater.start() def add_worker_widget(self, worker_name): - config = self.config.get_worker_config(worker_name) - widget = WorkerItemWidget(worker_name, config, self.main_ctrl, self) + widget = WorkerItemWidget(worker_name, self.main_ctrl, self) widget.setFixedSize(widget.frameSize()) self.worker_container.addWidget(widget) self.worker_widgets[worker_name] = widget @@ -70,6 +69,10 @@ def remove_worker_widget(self, worker_name): if self.num_of_workers < self.max_workers: self.ui.add_worker_button.setEnabled(True) + def change_worker_widget_name(self, old_worker_name, new_worker_name): + worker_data = self.worker_widgets.pop(old_worker_name) + self.worker_widgets[new_worker_name] = worker_data + @gui_error def handle_add_worker(self): create_worker_dialog = CreateWorkerView(self.main_ctrl.bitshares_instance) @@ -122,9 +125,7 @@ def _update_statusbar_message(self): time.sleep(0.5) def set_statusbar_message(self): - config = self.main_ctrl.load_config() - node = config['node'] - + node = self.config['node'] try: start = time.time() BitSharesNodeRPC(node, num_retries=1) @@ -139,4 +140,6 @@ def set_statusbar_message(self): def set_worker_status(self, worker_name, level, status): if worker_name != 'NONE': - self.worker_widgets[worker_name].set_status(status) + worker = self.worker_widgets.get(worker_name, None) + if worker: + worker.set_status(status) From 55f0f16acfd60832ccf29e14bd97707b8a44902e Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 30 May 2018 12:25:33 +0300 Subject: [PATCH 0328/1846] Fix import statements for the cli --- dexbot/cli.py | 4 ++-- dexbot/ui.py | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index cdd24406c..213bdd770 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -5,7 +5,7 @@ import signal import sys -from dexbot import config_file +from dexbot.config import CONFIG_FILE from dexbot.ui import ( verbose, check_connection, @@ -37,7 +37,7 @@ @click.group() @click.option( "--configfile", - default=config_file, + default=CONFIG_FILE, ) @click.option( '--verbose', diff --git a/dexbot/ui.py b/dexbot/ui.py index d842b94a0..372d0f3c7 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -2,14 +2,11 @@ import sys import logging import logging.config -from datetime import datetime -from prettytable import PrettyTable from functools import update_wrapper from . import find_node import click -from bitshares.price import Price from ruamel import yaml from bitshares import BitShares from bitshares.instance import set_shared_bitshares_instance From 948d8559bc564bf66d14e402e27f7091eb9217ab Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 30 May 2018 14:49:08 +0300 Subject: [PATCH 0329/1846] Remove find_node remnants --- dexbot/cli.py | 1 - dexbot/ui.py | 12 ------------ docs/configuration.rst | 8 +------- 3 files changed, 1 insertion(+), 20 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index a907ec834..1dc061001 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -66,7 +66,6 @@ def main(ctx, **kwargs): @main.command() @click.pass_context @configfile -@check_connection @chain @unlock @verbose diff --git a/dexbot/ui.py b/dexbot/ui.py index 372d0f3c7..fa666ee7f 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -4,8 +4,6 @@ import logging.config from functools import update_wrapper -from . import find_node - import click from ruamel import yaml from bitshares import BitShares @@ -76,16 +74,6 @@ def new_func(ctx, *args, **kwargs): return update_wrapper(new_func, f) -def check_connection(f): - @click.pass_context - def new_func(ctx, *args, **kwargs): - if not find_node.is_host_online(ctx.config['node']): - node = find_node.best_node() - ctx.config['node'] = node - return ctx.invoke(f, *args, **kwargs) - return update_wrapper(new_func, f) - - def chain(f): @click.pass_context def new_func(ctx, *args, **kwargs): diff --git a/docs/configuration.rst b/docs/configuration.rst index 7fd521bc7..2cbfe19ee 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -41,13 +41,7 @@ The configuration consists of a series of questions about the bots you wish to c DEXBot needs to have a public node (also called "witness") that gives access to the BitShares blockchain. - The configuration tool will ping a standard list of nodes and use the one with the least latency. If this fails - (most likely because you are not online), the config tool will ask you to enter a value here. - - If think this process is wrong or the list should have servers added/removed (see ``dexbot/find_nodes.py``)) - please file a - `Github bug report `_ . - + DEXBot uses ``wss://status200.bitshares.apasia.tech/ws`` as its default node If you run your own witness node then you can edit ``config.yml`` to change the node value. 5. Systemd. From 82b7cdca329d69677e0011ebcbeb534454d041a0 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 30 May 2018 15:08:15 +0300 Subject: [PATCH 0330/1846] Add config file path changing feature to Config --- dexbot/cli.py | 22 ++++------------- dexbot/config.py | 63 +++++++++++++++++++++++++++++++++--------------- dexbot/helper.py | 18 ++++++++++++++ 3 files changed, 67 insertions(+), 36 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index 1dc061001..b20af1507 100644 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -5,10 +5,9 @@ import signal import sys -from dexbot.config import CONFIG_FILE +from dexbot.config import Config, DEFAULT_CONFIG_FILE from dexbot.ui import ( verbose, - check_connection, chain, unlock, configfile @@ -37,7 +36,7 @@ @click.group() @click.option( "--configfile", - default=CONFIG_FILE, + default=DEFAULT_CONFIG_FILE, ) @click.option( '--verbose', @@ -101,10 +100,7 @@ def run(ctx): sys.exit(70) # 70= "Software error" in /usr/include/sysexts.h finally: if ctx.obj['pidfile']: - try: - os.remove(ctx.obj['pidfile']) - except OSError: - pass + helper.remove(ctx.obj['pidfile']) @main.command() @@ -112,17 +108,9 @@ def run(ctx): def configure(ctx): """ Interactively configure dexbot """ - cfg_file = ctx.obj["configfile"] - if not os.path.exists(ctx.obj['configfile']): - helper.mkdir(os.path.dirname(ctx.obj['configfile'])) - with open(ctx.obj['configfile'], 'w') as fd: - fd.write(default_config) - with open(ctx.obj["configfile"]) as fd: - config = yaml.safe_load(fd) + config = Config(ctx.obj['configfile']) configure_dexbot(config) - - with open(cfg_file, "w") as fd: - yaml.dump(config, fd, default_flow_style=False) + config.save_config() click.echo("New configuration saved") if config['systemd_status'] == 'installed': diff --git a/dexbot/config.py b/dexbot/config.py index 30941ccb2..9aa7a5441 100644 --- a/dexbot/config.py +++ b/dexbot/config.py @@ -6,20 +6,29 @@ from collections import OrderedDict -CONFIG_DIR = appdirs.user_config_dir('dexbot') -CONFIG_FILE = os.path.join(CONFIG_DIR, 'config.yml') +DEFAULT_CONFIG_DIR = appdirs.user_config_dir('dexbot') +DEFAULT_CONFIG_FILE = os.path.join(DEFAULT_CONFIG_DIR, 'config.yml') class Config: - def __init__(self, config=None): + def __init__(self, config=None, path=None): + """ Creates or loads the config file based on if it exists. + """ + if path: + self.config_dir = os.path.dirname(config) + self.config_file = config + else: + self.config_dir = DEFAULT_CONFIG_DIR + self.config_file = DEFAULT_CONFIG_FILE + if config: - self.create_config(config) - self._config = self.load_config() + self.create_config(config, path) + self._config = self.load_config(path) else: - if not os.path.isfile(CONFIG_FILE): - self.create_config(self.default_data) - self._config = self.load_config() + if not os.path.isfile(self.config_file): + self.create_config(self.default_data, path) + self._config = self.load_config(path) def __setitem__(self, key, value): self._config[key] = value @@ -49,27 +58,43 @@ def dict(self): return self._config @staticmethod - def create_config(config): - if not os.path.exists(CONFIG_DIR): - pathlib.Path(CONFIG_DIR).mkdir(parents=True, exist_ok=True) + def create_config(config, path=None): + if not path: + config_dir = DEFAULT_CONFIG_DIR + config_file = DEFAULT_CONFIG_FILE + else: + config_dir = os.path.dirname(path) + config_file = path - with open(CONFIG_FILE, 'w') as f: + if not os.path.exists(config_dir): + pathlib.Path(config_dir).mkdir(parents=True, exist_ok=True) + + with open(config_file, 'w') as f: yaml.dump(config, f, default_flow_style=False) @staticmethod - def load_config(): - with open(CONFIG_FILE, 'r') as f: + def load_config(path=None): + if not path: + path = DEFAULT_CONFIG_FILE + with open(path, 'r') as f: return Config.ordered_load(f, loader=yaml.SafeLoader) + def save_config(self): + with open(self.config_file, 'w') as f: + yaml.dump(self._config, f, default_flow_style=False) + def refresh_config(self): self._config = self.load_config() @staticmethod - def get_worker_config_file(worker_name): + def get_worker_config_file(worker_name, path=None): """ Returns config file data with only the data from a specific worker. Config loaded from a file """ - with open(CONFIG_FILE, 'r') as f: + if not path: + path = DEFAULT_CONFIG_FILE + + with open(path, 'r') as f: config = Config.ordered_load(f, loader=yaml.SafeLoader) config['workers'] = {worker_name: config['workers'][worker_name]} @@ -86,13 +111,13 @@ def get_worker_config(self, worker_name): def remove_worker_config(self, worker_name): self._config['workers'].pop(worker_name, None) - with open(CONFIG_FILE, 'w') as f: + with open(self.config_file, 'w') as f: yaml.dump(self._config, f) def add_worker_config(self, worker_name, worker_data): self._config['workers'][worker_name] = worker_data - with open(CONFIG_FILE, 'w') as f: + with open(self.config_file, 'w') as f: yaml.dump(self._config, f, default_flow_style=False) def replace_worker_config(self, worker_name, new_worker_name, worker_data): @@ -105,7 +130,7 @@ def replace_worker_config(self, worker_name, new_worker_name, worker_data): else: workers[key] = value - with open(CONFIG_FILE, 'w') as f: + with open(self.config_file, 'w') as f: yaml.dump(self._config, f, default_flow_style=False) @staticmethod diff --git a/dexbot/helper.py b/dexbot/helper.py index 31e4bb619..67b28c466 100644 --- a/dexbot/helper.py +++ b/dexbot/helper.py @@ -1,4 +1,6 @@ import os +import shutil +import errno def mkdir(d): @@ -8,3 +10,19 @@ def mkdir(d): return except OSError: raise + + +def remove(path): + """ Removes a file or a directory even if they don't exist + """ + if os.path.isfile(path): + try: + os.remove(path) + except OSError as e: + if e.errno != errno.ENOENT: + raise + elif os.path.isdir(path): + try: + shutil.rmtree(path) + except FileNotFoundError: + return From 3624eadad459bfd2ba781581c219b998a1894bcd Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 30 May 2018 15:38:08 +0300 Subject: [PATCH 0331/1846] Fix Config logic --- dexbot/config.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/dexbot/config.py b/dexbot/config.py index 9aa7a5441..5cc58bd9a 100644 --- a/dexbot/config.py +++ b/dexbot/config.py @@ -10,25 +10,28 @@ DEFAULT_CONFIG_FILE = os.path.join(DEFAULT_CONFIG_DIR, 'config.yml') -class Config: +class Config(dict): def __init__(self, config=None, path=None): """ Creates or loads the config file based on if it exists. + :param dict config: data used to create the config file + :param str path: path to the config file """ + super().__init__() if path: - self.config_dir = os.path.dirname(config) - self.config_file = config + self.config_dir = os.path.dirname(path) + self.config_file = path else: self.config_dir = DEFAULT_CONFIG_DIR self.config_file = DEFAULT_CONFIG_FILE if config: - self.create_config(config, path) - self._config = self.load_config(path) + self.create_config(config, self.config_file) + self._config = self.load_config(self.config_file) else: if not os.path.isfile(self.config_file): - self.create_config(self.default_data, path) - self._config = self.load_config(path) + self.create_config(self.default_data, self.config_file) + self._config = self.load_config(self.config_file) def __setitem__(self, key, value): self._config[key] = value From bc4dd6687bd88ada11fcbed9d6ce36226f780e00 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 30 May 2018 21:45:14 +0500 Subject: [PATCH 0332/1846] Prevent double removal in staggered strategy Closes: #152 --- dexbot/strategies/staggered_orders.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index d717c30d9..fdf225e03 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -105,7 +105,6 @@ def place_reverse_order(self, 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'] * (1 + self.spread) From 83e24b0aed7cd9300f75d9b2ad61a5b1f9024869 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 31 May 2018 09:43:51 +0300 Subject: [PATCH 0333/1846] Change dexbot version number to 0.2.6 --- dexbot/__init__.py | 2 +- dexbot/strategies/staggered_orders.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index c0d9881bf..11b5a922d 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.5' +VERSION = '0.2.6' AUTHOR = "codaone" __version__ = VERSION diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index fdf225e03..07daddc45 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -104,8 +104,6 @@ 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') - if order['base']['symbol'] == self.market['base']['symbol']: # Buy order price = order['price'] * (1 + self.spread) amount = order['quote']['amount'] From 7dea64a613595265c50fa26e92442f9abbbf9fbb Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 31 May 2018 13:03:18 +0500 Subject: [PATCH 0334/1846] Fix logic in staggered orders maintenance Actual behavior =========== In the staggered orders strategy we have a following logic at the initialization: ```python if self['setup_done']: self.place_orders() else: self.init_strategy() ``` By this logic a fresh worker will do `self.init_strategy()`. It will: 1. Create a market orders 2. Save orders to the database Next, we're stopped worker and let it be offline. During this time some orders will be filled. Next, we're starting worker again. This time `self.place_orders()` will be executed. This means **bot will place already filled orders again** likely causing an error `Insufficient buy/sell balance`: ```python def place_orders(self): """ Place all the orders found in the database """ orders = self.fetch_orders() for order_id, order in orders.items(): if not self.get_order(order_id): self.place_order(order) ``` Expected behavior ============= Instead of trying to repeat already filled order bot must place a reverse order. The logic I expect: 1. Init strategy, place orders, save them to the dadabase 2. Put bot offline 3. Let orders fill 4. Start worker 5. Worker fetches orders from database and check whether they exists on the market 6. Order found in the database but not present in the orderbook. Causes? Order was cancelled manually or filled. Assume we should not manually touch bot's order. Then, the only way order disappeared is that it was filled! 7. So we know order was filled, so place reverse order. Closes: #160 --- 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 07daddc45..ea30a87bb 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -30,7 +30,7 @@ def __init__(self, *args, **kwargs): self.lower_bound = self.worker['lower_bound'] if self['setup_done']: - self.place_orders() + self.check_orders() else: self.init_strategy() @@ -133,6 +133,7 @@ def place_order(self, order): def place_orders(self): """ Place all the orders found in the database + FIXME: unused method """ orders = self.fetch_orders() for order_id, order in orders.items(): From 3f1eed6929ab153878f385d928ac0a52ecb019c7 Mon Sep 17 00:00:00 2001 From: Petri Kanerva Date: Thu, 31 May 2018 16:11:05 +0300 Subject: [PATCH 0335/1846] New visuals for worker and worker list --- dexbot/resources/font/SourceSansPro-Bold.ttf | Bin 0 -> 291424 bytes .../font/SourceSansVariable-Roman.ttf | Bin 0 -> 491040 bytes dexbot/resources/fonts.qrc | 5 + dexbot/resources/icons.qrc | 4 + dexbot/resources/img/dexbot.png | Bin 0 -> 5519 bytes dexbot/resources/svg/dexbot.svg | 1 + dexbot/resources/svg/modifystrategy.svg | 1 + dexbot/resources/svg/simplestrategy.svg | 1 + dexbot/views/edit_worker.py | 8 +- dexbot/views/ui/edit_worker_window.ui | 17 + dexbot/views/ui/worker_item_widget.ui | 924 +++++++++--------- dexbot/views/ui/worker_list_window.ui | 153 ++- dexbot/views/worker_item.py | 49 +- dexbot/views/worker_list.py | 4 +- 14 files changed, 653 insertions(+), 514 deletions(-) create mode 100644 dexbot/resources/font/SourceSansPro-Bold.ttf create mode 100644 dexbot/resources/font/SourceSansVariable-Roman.ttf create mode 100644 dexbot/resources/fonts.qrc create mode 100644 dexbot/resources/img/dexbot.png create mode 100644 dexbot/resources/svg/dexbot.svg create mode 100644 dexbot/resources/svg/modifystrategy.svg create mode 100644 dexbot/resources/svg/simplestrategy.svg diff --git a/dexbot/resources/font/SourceSansPro-Bold.ttf b/dexbot/resources/font/SourceSansPro-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5d65c93242fc3776b379ab98bb5e6e33af4b25df GIT binary patch literal 291424 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
TV03jbdR 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 H{s8$K@VVub literal 0 HcmV?d00001 diff --git a/dexbot/resources/font/SourceSansVariable-Roman.ttf b/dexbot/resources/font/SourceSansVariable-Roman.ttf new file mode 100644 index 0000000000000000000000000000000000000000..96fc52a395fc62b389d03744fc9fb6858503b5c4 GIT binary patch literal 491040 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 z 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

^qjEsfBHm|U8r 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 =H7^cYJC6OZbakUU2B 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

cjQ)b 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 + + font/SourceSansPro-Bold.ttf + + diff --git a/dexbot/resources/icons.qrc b/dexbot/resources/icons.qrc index 66469651f..1ce322b8e 100644 --- a/dexbot/resources/icons.qrc +++ b/dexbot/resources/icons.qrc @@ -1,5 +1,9 @@ + svg/modifystrategy.svg + img/dexbot.png + svg/dexbot.svg + svg/simplestrategy.svg img/bin.png img/pause.png img/pen.png diff --git a/dexbot/resources/img/dexbot.png b/dexbot/resources/img/dexbot.png new file mode 100644 index 0000000000000000000000000000000000000000..2e7e867125e019a66d485f4d6a89573b9fc32551 GIT binary patch literal 5519 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 a3b|jE-z*KictE*f1!$`3!mE@mk^cj+9!yXG literal 0 HcmV?d00001 diff --git a/dexbot/resources/svg/dexbot.svg b/dexbot/resources/svg/dexbot.svg new file mode 100644 index 000000000..e011220cf --- /dev/null +++ b/dexbot/resources/svg/dexbot.svg @@ -0,0 +1 @@ +Asset 2dexbot \ No newline at end of file diff --git a/dexbot/resources/svg/modifystrategy.svg b/dexbot/resources/svg/modifystrategy.svg new file mode 100644 index 000000000..7e30ac73d --- /dev/null +++ b/dexbot/resources/svg/modifystrategy.svg @@ -0,0 +1 @@ +Asset 1 \ No newline at end of file diff --git a/dexbot/resources/svg/simplestrategy.svg b/dexbot/resources/svg/simplestrategy.svg new file mode 100644 index 000000000..0a1819df2 --- /dev/null +++ b/dexbot/resources/svg/simplestrategy.svg @@ -0,0 +1 @@ +Asset 1 \ No newline at end of file diff --git a/dexbot/views/edit_worker.py b/dexbot/views/edit_worker.py index d7ea03881..26ba14f17 100644 --- a/dexbot/views/edit_worker.py +++ b/dexbot/views/edit_worker.py @@ -8,9 +8,10 @@ class EditWorkerView(QtWidgets.QDialog, Ui_Dialog): - def __init__(self, controller, worker_name, config): + def __init__(self, parent_widget, controller, worker_name, config): super().__init__() self.controller = controller + self.parent_widget = parent_widget self.setupUi(self) worker_data = config['workers'][worker_name] @@ -44,6 +45,7 @@ def __init__(self, controller, worker_name, config): 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.remove_button.clicked.connect(self.handle_remove) 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 = {} @@ -156,3 +158,7 @@ def handle_save(self): } self.worker_name = self.worker_name_input.text() self.accept() + + def handle_remove(self): + self.parent_widget.remove_widget_dialog() + self.reject() \ No newline at end of file diff --git a/dexbot/views/ui/edit_worker_window.ui b/dexbot/views/ui/edit_worker_window.ui index 354ce3da0..d3cc60084 100644 --- a/dexbot/views/ui/edit_worker_window.ui +++ b/dexbot/views/ui/edit_worker_window.ui @@ -416,6 +416,23 @@ + + + + border: 1px solid #8f8f91; +border-radius: 4px; +background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #eb9994, stop: 1 #cc2522); +min-width: 80px; +min-height: 23px; +color: #fff; + + + + Delete + + + diff --git a/dexbot/views/ui/worker_item_widget.ui b/dexbot/views/ui/worker_item_widget.ui index 971260d3e..1b48497cd 100644 --- a/dexbot/views/ui/worker_item_widget.ui +++ b/dexbot/views/ui/worker_item_widget.ui @@ -2,551 +2,562 @@ widget - - true - 0 0 - 480 - 138 + 333 + 275 - - - 0 - 0 - + + + Source Sans Pro + 75 + true + - widget + Form + + + background-color: #3A6257; +color: #ffffff; +border-radius: 20px; + - - - 0 - - - 0 - - - 0 - - - 0 - + - - - - 0 - 0 - - - - - 480 - 137 - - - - - 16777215 - 16777215 - - - - Qt::LeftToRight - - - false + + + + Source Sans Pro + 75 + true + - .QFrame { border: 1px solid #005B78; border-radius: 4px; } -* { background-color: white; } - - - - QFrame::StyledPanel + border-radius: 20px; - - QFrame::Raised - - - - - - - 0 - 0 - + + + + + + 16777215 + 100 + - - - 0 - - - 0 - - - 1 - + + + Source Sans Pro + 75 + true + + + + + + + + + 0 + 0 + + + + + 16777215 + 200 + + - 12 + Source Sans Pro + 20 75 true + + Bot name + + + + + + + + 10 + 9 + + + + + 10 + 9 + + - color: #005B78; + margin-top: 2px; - Worker name + - - Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + :/bot_widget/svg/simplestrategy.svg - - Qt::TextSelectableByMouse + + true - + + + + 0 + 0 + + 16777215 - 16777215 + 16777042 - 9 + Source Sans Pro + 7 75 true - - false + + SIMPLE STRATEGY + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + true + + + + 16777215 + 1 + + + + + Source Sans Pro + 75 + true + + + + background: black; + + + Qt::Horizontal + + + + + + + + Source Sans Pro + 75 + true + + + + + + + + Source Sans Pro + 75 + true + - color: #005B78; + color: #99F75C; - RELATIVE ORDERS + +0.0% - - Qt::AlignBottom|Qt::AlignRight|Qt::AlignTrailing + + + + + + + Source Sans Pro + 75 + true + - - Qt::TextSelectableByMouse + + + + + + + + + + 280 + 30 + + + + + 280 + 30 + + + + + Source Sans Pro + 75 + true + + + + border-radius: 15px; +background: #ffffff; +max-height: 30px; +padding: 0px; +margin: 0px; + + + + 5 + + + 5 + + + 5 + + + 5 + - + + + + 0 + 0 + + - 28 - 16777215 + 16777191 + 30 - - PointingHandCursor + + + Source Sans Pro + 9 + 75 + true + + + + Qt::LeftToRight - border: 0; + background: #dfa93b; +border-radius: 10px; padding-left: 5px; - + EUR - - - :/bot_widget/img/pen.png:/bot_widget/img/pen.png + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - 28 - 16777215 - + + + + 0 + 0 + - - PointingHandCursor + + + Source Sans Pro + 75 + true + - border: 0; + border-radius: 10px; padding-left: 5px; background: #6794ec; - - - - - :/bot_widget/img/bin.png:/bot_widget/img/bin.png - - - - 20 - 20 - + USD - - - - 0 + + + + + Source Sans Pro + 75 + true + + + + border-radius: 0px; - - - - - - - 130 - 0 - + + + + + + 0 + 60 + + + + + Source Sans Pro + 75 + true + + + + + 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 11 - 75 - true - - - - color: #005B78; - - - BTS/USD - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - Qt::TextSelectableByMouse - - - - - - - - 10 - 75 - true - - - - color: #00D05A; - - - 1 + + 5 + + + 5 + + + 5 + + + 5 + + + + + + 50 + 24 + + + + + 50 + 24 + + + + Qt::LeftToRight + + + border-radius: 12px; +background: #ffffff; +max-height: 24px; +padding: 0px; +margin: 0px; + + + + 2 - - +0.0% + + 2 - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + 2 - - Qt::TextSelectableByMouse + + 2 - - - - - - - - - - - - 0 - 0 - - - - - 5 - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - 0 + + + + + 20 + 20 + + + + + 20 + 24 + + + + Qt::LeftToRight + + + background-color: #3A6257; border-radius: 10px;float:right; + + + + + + + + + + + Source Sans Pro + 6 + 75 + true + - - 0 + + TURN WORKER ON - - 0 + + + + + + + + + + 0 + 60 + + + + + Source Sans Pro + 75 + true + + + + + 0 + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + 0 + 24 + - - - - - 75 - true - - - - color: #005B78; - - - Base - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 75 - true - - - - color: #005B78; - - - Quote - - - - - - - - - - - 0 - 0 - - - - - - - - 5 + + + Source Sans Pro + 75 + true + - - 0 + + PointingHandCursor - - 5 + + color: #ffffff - - 0 + + - - - - false - - - - 0 - 0 - - - - QSlider::groove:horizontal { -height: 2px; -background: #005B78; -} -QSlider::handle:horizontal { -background: #005B78; -width: 15px; -margin: -5px 0; -} -QSlider { -border-left: 2px solid #005B78; -border-right: 2px solid #005B78; -} - - - 100 - - - 50 - - - false - - - Qt::Horizontal - - - QSlider::TicksAbove - - - - - - - - - - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - PointingHandCursor - - - border: 0; - - - - - - - - :/bot_widget/img/pause.png:/bot_widget/img/pause.png - - - - 30 - 30 - - - - - - - - - 0 - 0 - - - - PointingHandCursor - - - - - - border: 0; - - - - - - - :/bot_widget/img/play.png:/bot_widget/img/play.png - - - - 30 - 30 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - + + + :/bot_widget/svg/modifystrategy.svg:/bot_widget/svg/modifystrategy.svg + + + + 30 + 30 + + + + true + + + + + + + + Source Sans Pro + 6 + 75 + true + + + + + + + MODIFY STRATEGY + + + + + + + + + + + + + + Source Sans Pro + 75 + true + + + + Worker not running + + + Qt::AlignCenter + + + true + + @@ -555,6 +566,7 @@ border-right: 2px solid #005B78; + diff --git a/dexbot/views/ui/worker_list_window.ui b/dexbot/views/ui/worker_list_window.ui index 8d90a9691..d924e5f85 100644 --- a/dexbot/views/ui/worker_list_window.ui +++ b/dexbot/views/ui/worker_list_window.ui @@ -6,7 +6,7 @@ 0 0 - 814 + 809 513 @@ -16,8 +16,18 @@ DEXBot + + + - background-color: #EDEDED + * { + background-color: #152B2A; +} + +QScrollBar { + background-color: #ffffff; +} + @@ -51,47 +61,83 @@ 100 - - - - - - - DEXBot - - - - - - - - - - - PointingHandCursor - - - -1 - - - Add worker - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + + + + + + + + + + + + 0 + 0 + + + + + 0 + 60 + + + + + 100 + 60 + + + + + + + :/bot_widget/img/dexbot.png + + + true + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + 250 + 16777214 + + + + PointingHandCursor + + + -1 + + + border: 0px; background-color: #3A6257; width: 250px; height: 20px; border-radius: 10px; color: #ffffff; + + + Add worker + + + + + @@ -107,6 +153,9 @@ false + + + QFrame::NoFrame @@ -125,14 +174,14 @@ - 389 + 0 0 - 18 + 791 18 - + 0 0 @@ -140,7 +189,10 @@ -7 - + + color: white; + + @@ -150,8 +202,13 @@ ArrowCursor + + color: #fff; + - + + + diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 387ed57b8..206070739 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -1,12 +1,14 @@ +import re + 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 dexbot.views.errors import gui_error +from dexbot.resources import icons_rc -from PyQt5 import QtWidgets +from PyQt5 import QtCore, QtWidgets class WorkerItemWidget(QtWidgets.QWidget, Ui_widget): @@ -21,13 +23,12 @@ def __init__(self, worker_name, config, main_ctrl, view): 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.toggle.mouseReleaseEvent=self.toggle_worker + self.onoff.mouseReleaseEvent=self.toggle_worker + self.setup_ui_data(config) def setup_ui_data(self, config): @@ -49,6 +50,23 @@ def setup_ui_data(self, config): else: self.set_worker_slider(50) + @gui_error + def toggle_worker(self): + if self.horizontalLayout_5.alignment() != QtCore.Qt.AlignRight: + toggle_alignment = QtCore.Qt.AlignRight + toggle_label_text = "TURN WORKER OFF" + else: + toggle_alignment = QtCore.Qt.AlignLeft + toggle_label_text = "TURN WORKER ON" + + _translate = QtCore.QCoreApplication.translate + self.toggle_label.setText(_translate("widget", toggle_label_text)) + self.horizontalLayout_5.setAlignment(toggle_alignment) + + # TODO: better way of repainting the widget + self.toggle.hide() + self.toggle.show() + @gui_error def start_worker(self): self._start_worker() @@ -78,6 +96,10 @@ def set_worker_account(self, value): def set_worker_market(self, value): self.currency_label.setText(value) + values = re.split("[/:]", value) + self.base_asset_label.setText(values[0]) + self.quote_asset_label.setText(values[1]) + def set_worker_profit(self, value): value = float(value) if value >= 0: @@ -87,7 +109,17 @@ def set_worker_profit(self, value): self.profit_label.setText(value) def set_worker_slider(self, value): - self.order_slider.setSliderPosition(value) + barWidth = self.bar.width(); + + spacing = self.bar.layout().spacing(); + margin_left = self.bar.layout().contentsMargins().left() + margin_right = self.bar.layout().contentsMargins().right() + total_padding = spacing + margin_left + margin_right + + base_width = (barWidth-total_padding) * (value/100) + + self.base_asset_label.setMaximumWidth(base_width) + self.base_asset_label.setMinimumWidth(base_width) @gui_error def remove_widget_dialog(self): @@ -113,7 +145,7 @@ def reload_widget(self, worker_name): @gui_error def handle_edit_worker(self): controller = CreateWorkerController(self.main_ctrl) - edit_worker_dialog = EditWorkerView(controller, self.worker_name, self.worker_config) + edit_worker_dialog = EditWorkerView(self, controller, self.worker_name, self.worker_config) return_value = edit_worker_dialog.exec_() # User clicked save @@ -123,3 +155,4 @@ 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 + diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 832828bcf..e5ee8a03f 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -10,7 +10,7 @@ from dexbot.queue.idle_queue import idle_add from .errors import gui_error -from PyQt5 import QtWidgets +from PyQt5 import QtCore, QtGui, QtWidgets from bitsharesapi.bitsharesnoderpc import BitSharesNodeRPC @@ -52,6 +52,8 @@ def __init__(self, main_ctrl): ) self.statusbar_updater.start() + QtGui.QFontDatabase.addApplicationFont(":/bot_widget/font/SourceSansPro-Bold.ttf") + 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) From 53eb65870d57f30bcd602bd3114db82cb3ff2625 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 31 May 2018 21:58:42 +0500 Subject: [PATCH 0336/1846] Implement separate method for pausing a worker Staggered orders isn't supposed to cancel all orders on pause. The implemented solution allows worker to differentiate actions "on stop" event and "on pause" event. --- dexbot/basestrategy.py | 6 ++++++ dexbot/controllers/main_controller.py | 3 +++ dexbot/strategies/staggered_orders.py | 5 +++++ dexbot/views/worker_item.py | 2 +- dexbot/worker.py | 22 +++++++++++++++++++--- 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 06cf73738..d6a25276a 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -333,6 +333,12 @@ def cancel_all(self): self.cancel(self.orders) self.log.info("Orders canceled") + def pause(self): + """ Pause worker. User presed "pause" button in the GUI + """ + # By default, just call cancel_all(); strategies may override this method + self.cancel_all() + def market_buy(self, amount, price, return_none=False): symbol = self.market['base']['symbol'] precision = self.market['base']['precision'] diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 17f67de7f..45458c8fd 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -46,6 +46,9 @@ def create_worker(self, worker_name, config, view): def stop_worker(self, worker_name): self.worker_manager.stop(worker_name) + def pause_worker(self, worker_name): + self.worker_manager.pause(worker_name) + def remove_worker(self, worker_name): # 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/staggered_orders.py b/dexbot/strategies/staggered_orders.py index ea30a87bb..a3fd32850 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -100,6 +100,11 @@ def init_strategy(self): self['setup_done'] = True self.log.info("Done placing orders") + def pause(self, *args, **kwargs): + """ Override pause() method because we don't want to remove orders + """ + self.log.info("Got pause command, stopping and leaving orders on the market") + def place_reverse_order(self, order): """ Replaces an order with a reverse order buy orders become sell orders and sell orders become buy orders diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 8544f7455..34e4e0328 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -68,7 +68,7 @@ def _start_worker(self): def pause_worker(self): self.set_status("Pausing worker") self._pause_worker() - self.main_ctrl.stop_worker(self.worker_name) + self.main_ctrl.pause_worker(self.worker_name) def _pause_worker(self): self.running = False diff --git a/dexbot/worker.py b/dexbot/worker.py index 66de545df..40194d33c 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -174,7 +174,12 @@ def run(self): self.update_notify() self.notify.listen() - def stop(self, worker_name=None): + def stop(self, worker_name=None, pause=None): + """ Used to stop the worker(s) + :param str worker_name: name of the worker to stop + :param bool pause: optional argument which tells worker if it was + stopped or just paused + """ if worker_name and len(self.workers) > 1: # Kill only the specified worker self.remove_market(worker_name) @@ -183,16 +188,27 @@ def stop(self, worker_name=None): self.config['workers'].pop(worker_name) self.accounts.remove(account) - self.workers[worker_name].cancel_all() + if pause: + self.workers[worker_name].pause() + else: + 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() + if pause: + self.workers[worker].pause() + else: + self.workers[worker].cancel_all() if self.notify: self.notify.websocket.close() + def pause(self, *args, **kwargs): + """ GUI should call this method when pausing a worker. + """ + self.stop(pause=True, *args, **kwargs) + def remove_worker(self, worker_name=None): if worker_name: self.workers[worker_name].purge() From 3ea3a6be435fda52dd407b05cef2938a31b7b355 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 31 May 2018 22:04:19 +0500 Subject: [PATCH 0337/1846] Use WorkerInfrastructure.pause() to handle cli signals For staggered orders pausing worker via gui and exiting cli via Ctrl-C shoud produce the same result: just exit from worker leaving orders on the market. Closes: #137 --- dexbot/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 dexbot/cli.py diff --git a/dexbot/cli.py b/dexbot/cli.py old mode 100644 new mode 100755 index 75dff3903..faf2f8bd2 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -64,7 +64,7 @@ def run(ctx): try: worker = WorkerInfrastructure(ctx.config) # Set up signalling. do it here as of no relevance to GUI - kill_workers = worker_job(worker, worker.stop) + kill_workers = worker_job(worker, worker.pause) # These first two UNIX & Windows signal.signal(signal.SIGTERM, kill_workers) signal.signal(signal.SIGINT, kill_workers) From 4a12ce0e93d32f90f825e982e99b40ee2c534470 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 1 Jun 2018 09:49:51 +0300 Subject: [PATCH 0338/1846] Fix Config class logic --- dexbot/config.py | 11 +++++++---- dexbot/views/worker_item.py | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/dexbot/config.py b/dexbot/config.py index 5cc58bd9a..5d96b75e2 100644 --- a/dexbot/config.py +++ b/dexbot/config.py @@ -1,12 +1,14 @@ import os import pathlib +from dexbot import APP_NAME, AUTHOR + import appdirs from ruamel import yaml from collections import OrderedDict -DEFAULT_CONFIG_DIR = appdirs.user_config_dir('dexbot') +DEFAULT_CONFIG_DIR = appdirs.user_config_dir(APP_NAME, appauthor=AUTHOR) DEFAULT_CONFIG_FILE = os.path.join(DEFAULT_CONFIG_DIR, 'config.yml') @@ -79,6 +81,7 @@ def create_config(config, path=None): def load_config(path=None): if not path: path = DEFAULT_CONFIG_FILE + with open(path, 'r') as f: return Config.ordered_load(f, loader=yaml.SafeLoader) @@ -87,7 +90,7 @@ def save_config(self): yaml.dump(self._config, f, default_flow_style=False) def refresh_config(self): - self._config = self.load_config() + self._config = self.load_config(self.config_file) @staticmethod def get_worker_config_file(worker_name, path=None): @@ -100,7 +103,7 @@ def get_worker_config_file(worker_name, path=None): with open(path, 'r') as f: config = Config.ordered_load(f, loader=yaml.SafeLoader) - config['workers'] = {worker_name: config['workers'][worker_name]} + config['workers'] = OrderedDict({worker_name: config['workers'][worker_name]}) return config def get_worker_config(self, worker_name): @@ -115,7 +118,7 @@ def remove_worker_config(self, worker_name): self._config['workers'].pop(worker_name, None) with open(self.config_file, 'w') as f: - yaml.dump(self._config, f) + yaml.dump(self._config, f, default_flow_style=False) def add_worker_config(self, worker_name, worker_data): self._config['workers'][worker_name] = worker_data diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 3fa02bbc1..e3581f1ae 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -30,7 +30,7 @@ def __init__(self, worker_name, main_ctrl, view): self.setup_ui_data(self.worker_config) def setup_ui_data(self, config): - worker_name = list(config['workers'].keys())[0] + worker_name = self.worker_name self.set_worker_name(worker_name) market = config['workers'][worker_name]['market'] @@ -133,5 +133,5 @@ def handle_edit_worker(self): self.main_ctrl.config.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 + self.reload_widget(new_worker_name) From c3ff6bbc85a9a2405309312cb77ee96988520bda Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 1 Jun 2018 10:12:39 +0300 Subject: [PATCH 0339/1846] Fix GUI crash --- dexbot/views/ui/worker_item_widget.ui | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/views/ui/worker_item_widget.ui b/dexbot/views/ui/worker_item_widget.ui index da982246d..0d90a0fb1 100644 --- a/dexbot/views/ui/worker_item_widget.ui +++ b/dexbot/views/ui/worker_item_widget.ui @@ -7,7 +7,7 @@ 0 0 333 - 275 + 292 @@ -115,7 +115,7 @@ border-radius: 20px; - + 0 @@ -559,7 +559,7 @@ margin: 0px; - + From 4e954d6b317446f928b48bffcbe2a3e59a3e4c01 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 1 Jun 2018 10:13:37 +0300 Subject: [PATCH 0340/1846] Fix worker_item.py stuff --- dexbot/views/worker_item.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 192ef6452..ae66c255a 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -104,8 +104,8 @@ def set_worker_market(self, value): self.currency_label.setText(value) values = re.split("[/:]", value) - self.base_asset_label.setText(values[0]) - self.quote_asset_label.setText(values[1]) + self.base_asset_label.setText(values[1]) + self.quote_asset_label.setText(values[0]) def set_worker_profit(self, value): value = float(value) @@ -116,15 +116,14 @@ def set_worker_profit(self, value): self.profit_label.setText(value) def set_worker_slider(self, value): - barWidth = self.bar.width(); + bar_width = self.bar.width() - spacing = self.bar.layout().spacing(); + spacing = self.bar.layout().spacing() margin_left = self.bar.layout().contentsMargins().left() margin_right = self.bar.layout().contentsMargins().right() total_padding = spacing + margin_left + margin_right - base_width = (barWidth-total_padding) * (value/100) - + base_width = (bar_width - total_padding) * (value / 100) self.base_asset_label.setMaximumWidth(base_width) self.base_asset_label.setMinimumWidth(base_width) From c225450fcd48000ee01f12eeab939969d51613f8 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 1 Jun 2018 12:24:46 +0300 Subject: [PATCH 0341/1846] Fix worker stopping related logic And reword some log messages --- dexbot/basestrategy.py | 2 +- dexbot/cli.py | 2 +- dexbot/controllers/main_controller.py | 5 +---- dexbot/strategies/staggered_orders.py | 2 +- dexbot/worker.py | 15 +++------------ 5 files changed, 7 insertions(+), 19 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index d6a25276a..277d13d3c 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -334,7 +334,7 @@ def cancel_all(self): self.log.info("Orders canceled") def pause(self): - """ Pause worker. User presed "pause" button in the GUI + """ Pause the worker """ # By default, just call cancel_all(); strategies may override this method self.cancel_all() diff --git a/dexbot/cli.py b/dexbot/cli.py index faf2f8bd2..cbbe5f3e3 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -64,7 +64,7 @@ def run(ctx): try: worker = WorkerInfrastructure(ctx.config) # Set up signalling. do it here as of no relevance to GUI - kill_workers = worker_job(worker, worker.pause) + kill_workers = worker_job(worker, lambda: worker.stop(pause=True)) # These first two UNIX & Windows signal.signal(signal.SIGTERM, kill_workers) signal.signal(signal.SIGINT, kill_workers) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 45458c8fd..e5a8b31a6 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -43,11 +43,8 @@ def create_worker(self, worker_name, config, view): self.worker_manager.daemon = True self.worker_manager.start() - def stop_worker(self, worker_name): - self.worker_manager.stop(worker_name) - def pause_worker(self, worker_name): - self.worker_manager.pause(worker_name) + self.worker_manager.stop(worker_name, pause=True) def remove_worker(self, worker_name): # Todo: Add some threading here so that the GUI doesn't freeze diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index a3fd32850..6e8d18817 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -103,7 +103,7 @@ def init_strategy(self): def pause(self, *args, **kwargs): """ Override pause() method because we don't want to remove orders """ - self.log.info("Got pause command, stopping and leaving orders on the market") + self.log.info("Stopping and leaving orders on the market") def place_reverse_order(self, order): """ Replaces an order with a reverse order diff --git a/dexbot/worker.py b/dexbot/worker.py index 40194d33c..cfb0d3558 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -174,7 +174,7 @@ def run(self): self.update_notify() self.notify.listen() - def stop(self, worker_name=None, pause=None): + def stop(self, worker_name=None, pause=False): """ Used to stop the worker(s) :param str worker_name: name of the worker to stop :param bool pause: optional argument which tells worker if it was @@ -190,25 +190,16 @@ def stop(self, worker_name=None, pause=None): self.accounts.remove(account) if pause: self.workers[worker_name].pause() - else: - 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: - if pause: + if pause: + for worker in self.workers: self.workers[worker].pause() - else: - self.workers[worker].cancel_all() if self.notify: self.notify.websocket.close() - def pause(self, *args, **kwargs): - """ GUI should call this method when pausing a worker. - """ - self.stop(pause=True, *args, **kwargs) - def remove_worker(self, worker_name=None): if worker_name: self.workers[worker_name].purge() From 85d9ca2d6fe8bb7f0d723900f0f9f45696caa282 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 1 Jun 2018 12:37:54 +0300 Subject: [PATCH 0342/1846] Add logging message to staggered orders --- dexbot/strategies/staggered_orders.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 6e8d18817..172c8c0f1 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -34,6 +34,8 @@ def __init__(self, *args, **kwargs): else: self.init_strategy() + self.log.info('Done initializing Staggered Orders') + if self.view: self.update_gui_profit() self.update_gui_slider() From 84038364615ed5066c0c7e3e379280eea249b94d Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 1 Jun 2018 12:38:16 +0300 Subject: [PATCH 0343/1846] Change dexbot version number to 0.2.7 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 11b5a922d..bbe0ebdc1 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.6' +VERSION = '0.2.7' AUTHOR = "codaone" __version__ = VERSION From 822592de3999d583d4cf61c6d819db42b4b1c7b4 Mon Sep 17 00:00:00 2001 From: Petri Kanerva Date: Fri, 1 Jun 2018 12:40:53 +0300 Subject: [PATCH 0344/1846] Fix narrow bar squaring up in worker GUI --- dexbot/views/ui/worker_item_widget.ui | 39 +++++++++++++++++++++------ dexbot/views/worker_item.py | 11 +++++++- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/dexbot/views/ui/worker_item_widget.ui b/dexbot/views/ui/worker_item_widget.ui index 0d90a0fb1..2a97527e5 100644 --- a/dexbot/views/ui/worker_item_widget.ui +++ b/dexbot/views/ui/worker_item_widget.ui @@ -84,6 +84,9 @@ border-radius: 20px; Bot name + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + @@ -112,6 +115,9 @@ border-radius: 20px; true + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + @@ -142,6 +148,9 @@ border-radius: 20px; Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + @@ -198,6 +207,9 @@ border-radius: 20px; +0.0% + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + @@ -212,6 +224,9 @@ border-radius: 20px; + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + @@ -266,6 +281,12 @@ margin: 0px; 0 + + + 20 + 0 + + 16777191 @@ -298,7 +319,7 @@ border-radius: 10px; padding-left: 5px; - + 0 0 @@ -440,6 +461,9 @@ margin: 0px; TURN WORKER ON + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + @@ -531,6 +555,9 @@ margin: 0px; MODIFY STRATEGY + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + @@ -540,7 +567,7 @@ margin: 0px; - + Source Sans Pro @@ -557,12 +584,8 @@ margin: 0px; true - - - - - - + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index ae66c255a..714661ba6 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -122,8 +122,17 @@ def set_worker_slider(self, value): margin_left = self.bar.layout().contentsMargins().left() margin_right = self.bar.layout().contentsMargins().right() total_padding = spacing + margin_left + margin_right + usable_width = (bar_width - total_padding) + + + # So we keep the roundness of bars. + # If bar width is less than 2 * border-radius, it squares the corners + base_width = usable_width * (value / 100) + if (base_width < 20): + base_width = 20 + if (base_width > usable_width - 20): + base_width = usable_width - 20 - base_width = (bar_width - total_padding) * (value / 100) self.base_asset_label.setMaximumWidth(base_width) self.base_asset_label.setMinimumWidth(base_width) From 45ff6984ebdec78f74f8f0b2ae5e415ca4252852 Mon Sep 17 00:00:00 2001 From: Petri Kanerva Date: Fri, 1 Jun 2018 13:26:35 +0300 Subject: [PATCH 0345/1846] Fix errors raised from earlier master merge --- dexbot/views/edit_worker.py | 6 ++++++ dexbot/views/ui/edit_worker_window.ui | 3 +++ dexbot/views/ui/worker_item_widget.ui | 3 +++ dexbot/views/worker_item.py | 8 +++----- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/dexbot/views/edit_worker.py b/dexbot/views/edit_worker.py index 1b9984f72..fa0306d00 100644 --- a/dexbot/views/edit_worker.py +++ b/dexbot/views/edit_worker.py @@ -36,6 +36,12 @@ def __init__(self, parent_widget, bitshares_instance, worker_name, config): 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.remove_button.clicked.connect(self.handle_remove) self.controller.change_strategy_form(worker_data) self.worker_data = {} + + def handle_remove(self): + self.parent_widget.remove_widget_dialog() + self.reject() + diff --git a/dexbot/views/ui/edit_worker_window.ui b/dexbot/views/ui/edit_worker_window.ui index b1a6e822c..7149be5b7 100644 --- a/dexbot/views/ui/edit_worker_window.ui +++ b/dexbot/views/ui/edit_worker_window.ui @@ -239,6 +239,9 @@ + + PointingHandCursor + border: 1px solid #8f8f91; border-radius: 4px; diff --git a/dexbot/views/ui/worker_item_widget.ui b/dexbot/views/ui/worker_item_widget.ui index 2a97527e5..10a0d43bf 100644 --- a/dexbot/views/ui/worker_item_widget.ui +++ b/dexbot/views/ui/worker_item_widget.ui @@ -400,6 +400,9 @@ border-radius: 10px; padding-left: 5px; 24 + + PointingHandCursor + Qt::LeftToRight diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 714661ba6..6544df8f0 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -55,13 +55,15 @@ def setup_ui_data(self, config): self.set_worker_slider(50) @gui_error - def toggle_worker(self): + def toggle_worker(self, args): if self.horizontalLayout_5.alignment() != QtCore.Qt.AlignRight: toggle_alignment = QtCore.Qt.AlignRight toggle_label_text = "TURN WORKER OFF" + self.start_worker() else: toggle_alignment = QtCore.Qt.AlignLeft toggle_label_text = "TURN WORKER ON" + self.pause_worker() _translate = QtCore.QCoreApplication.translate self.toggle_label.setText(_translate("widget", toggle_label_text)) @@ -79,8 +81,6 @@ def start_worker(self): def _start_worker(self): self.running = True - self.pause_button.show() - self.play_button.hide() @gui_error def pause_worker(self): @@ -90,8 +90,6 @@ def pause_worker(self): def _pause_worker(self): self.running = False - self.pause_button.hide() - self.play_button.show() def set_worker_name(self, value): self.worker_name_label.setText(value) From a9453b7ec9eee78c1df4a053ca6dbd2408dd1bd3 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 1 Jun 2018 13:28:38 +0300 Subject: [PATCH 0346/1846] Change offset_center_price calculation --- dexbot/basestrategy.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 277d13d3c..33eeedaf8 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -178,13 +178,21 @@ def calculate_offset_center_price(self, spread, center_price=None, order_ids=Non 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 + if not total: + balance = 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 + # Returns a value between -1 and 1 + balance = (total_balance['base'] / total) * 2 - 1 + + if balance < 0: + # With less of base asset center price should be offset downward + offset_center_price = calculated_center_price / math.sqrt(1 + spread * (balance * -1)) + elif balance > 0: + # With more of base asset center price will be offset upwards + offset_center_price = calculated_center_price * math.sqrt(1 + spread * balance) + else: + offset_center_price = calculated_center_price + return offset_center_price @property From 8279793d2cd8e248499bc09fb339ccf3e10a83df Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 1 Jun 2018 13:57:36 +0300 Subject: [PATCH 0347/1846] Fix GUI rework code --- dexbot/views/worker_item.py | 18 ++++++++---------- dexbot/views/worker_list.py | 4 +++- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 6544df8f0..7aeabb2f6 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -26,8 +26,8 @@ def __init__(self, worker_name, config, main_ctrl, view): self.edit_button.clicked.connect(lambda: self.handle_edit_worker()) - self.toggle.mouseReleaseEvent=self.toggle_worker - self.onoff.mouseReleaseEvent=self.toggle_worker + self.toggle.mouseReleaseEvent = lambda _: self.toggle_worker() + self.onoff.mouseReleaseEvent = lambda _: self.toggle_worker() self.setup_ui_data(config) @@ -55,16 +55,13 @@ def setup_ui_data(self, config): self.set_worker_slider(50) @gui_error - def toggle_worker(self, args): + def toggle_worker(self, ): if self.horizontalLayout_5.alignment() != QtCore.Qt.AlignRight: - toggle_alignment = QtCore.Qt.AlignRight - toggle_label_text = "TURN WORKER OFF" self.start_worker() else: - toggle_alignment = QtCore.Qt.AlignLeft - toggle_label_text = "TURN WORKER ON" self.pause_worker() + def _toggle_worker(self, toggle_label_text, toggle_alignment): _translate = QtCore.QCoreApplication.translate self.toggle_label.setText(_translate("widget", toggle_label_text)) self.horizontalLayout_5.setAlignment(toggle_alignment) @@ -81,6 +78,7 @@ def start_worker(self): def _start_worker(self): self.running = True + self._toggle_worker('TURN WORKER OFF', QtCore.Qt.AlignRight) @gui_error def pause_worker(self): @@ -90,6 +88,7 @@ def pause_worker(self): def _pause_worker(self): self.running = False + self._toggle_worker('TURN WORKER ON', QtCore.Qt.AlignLeft) def set_worker_name(self, value): self.worker_name_label.setText(value) @@ -122,13 +121,12 @@ def set_worker_slider(self, value): total_padding = spacing + margin_left + margin_right usable_width = (bar_width - total_padding) - # So we keep the roundness of bars. # If bar width is less than 2 * border-radius, it squares the corners base_width = usable_width * (value / 100) - if (base_width < 20): + if base_width < 20: base_width = 20 - if (base_width > usable_width - 20): + if base_width > usable_width - 20: base_width = usable_width - 20 self.base_asset_label.setMaximumWidth(base_width) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index a7a8c8269..db2fcd8e8 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -142,4 +142,6 @@ def set_statusbar_message(self): def set_worker_status(self, worker_name, level, status): if worker_name != 'NONE': - self.worker_widgets[worker_name].set_status(status) + worker = self.worker_widgets.get(worker_name, None) + if worker: + worker.set_status(status) From e2c068f54a9f8b8f314c995dd4cafc4d909f0865 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 1 Jun 2018 14:09:13 +0300 Subject: [PATCH 0348/1846] Change dexbot version number to 0.2.8 --- dexbot/__init__.py | 2 +- dexbot/basestrategy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index bbe0ebdc1..ce1ae2d73 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.7' +VERSION = '0.2.8' AUTHOR = "codaone" __version__ = VERSION diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 33eeedaf8..d0632d471 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -178,7 +178,7 @@ def calculate_offset_center_price(self, spread, center_price=None, order_ids=Non total_balance = self.total_balance(order_ids) total = (total_balance['quote'] * calculated_center_price) + total_balance['base'] - if not total: + if not total: # Prevent division by zero balance = 0 else: # Returns a value between -1 and 1 From 0eab0a1df2ed2821bf2d092e2f0a7370fb582420 Mon Sep 17 00:00:00 2001 From: Petri Kanerva Date: Fri, 1 Jun 2018 14:27:01 +0300 Subject: [PATCH 0349/1846] Visual pixel tweaks --- dexbot/views/ui/edit_worker_window.ui | 21 ++++++++++++++------- dexbot/views/ui/worker_item_widget.ui | 14 ++++++++++---- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/dexbot/views/ui/edit_worker_window.ui b/dexbot/views/ui/edit_worker_window.ui index 7149be5b7..aa16adcea 100644 --- a/dexbot/views/ui/edit_worker_window.ui +++ b/dexbot/views/ui/edit_worker_window.ui @@ -243,14 +243,21 @@ PointingHandCursor - border: 1px solid #8f8f91; -border-radius: 4px; -background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + QPushButton { + border: 1px solid #8f8f91; + border-radius: 4px; + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #eb9994, stop: 1 #cc2522); -min-width: 80px; -min-height: 23px; -color: #fff; - + min-width: 80px; + min-height: 23px; + color: #fff; + } + + QPushButton:hover { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #ed9f9c, stop: 1 #db4240); + } + Delete diff --git a/dexbot/views/ui/worker_item_widget.ui b/dexbot/views/ui/worker_item_widget.ui index 10a0d43bf..2080cfe4d 100644 --- a/dexbot/views/ui/worker_item_widget.ui +++ b/dexbot/views/ui/worker_item_widget.ui @@ -84,6 +84,9 @@ border-radius: 20px; Bot name + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse @@ -94,17 +97,17 @@ border-radius: 20px; 10 - 9 + 16 10 - 9 + 16 - margin-top: 2px; + margin-top: 9px; @@ -142,6 +145,9 @@ border-radius: 20px; true + + margin-top: 9px; + SIMPLE STRATEGY @@ -556,7 +562,7 @@ margin: 0px; - MODIFY STRATEGY + MODIFY WORKER Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse From cd9fc4a3f064c455d00d21d46a3d2bb47c4c0591 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 1 Jun 2018 14:51:09 +0300 Subject: [PATCH 0350/1846] Change worker widget worker name font size --- dexbot/views/ui/worker_item_widget.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/views/ui/worker_item_widget.ui b/dexbot/views/ui/worker_item_widget.ui index 2080cfe4d..a511079f3 100644 --- a/dexbot/views/ui/worker_item_widget.ui +++ b/dexbot/views/ui/worker_item_widget.ui @@ -76,7 +76,7 @@ border-radius: 20px; Source Sans Pro - 20 + 18 75 true From 30b12416701bd62969a4bb50c1f0c08f69ea9b18 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 1 Jun 2018 14:52:46 +0300 Subject: [PATCH 0351/1846] Change dexbot version number to 0.3.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 11b5a922d..3f516ab49 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.6' +VERSION = '0.3.0' AUTHOR = "codaone" __version__ = VERSION From 8b14a7db4e358ebbb9e5348fd3f19853181758c6 Mon Sep 17 00:00:00 2001 From: Krister Viirsaar Date: Fri, 1 Jun 2018 12:51:00 +0300 Subject: [PATCH 0352/1846] Fix #164 permission error as normal user --- Makefile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6db5fbe90..ac2ac86ba 100644 --- a/Makefile +++ b/Makefile @@ -19,16 +19,22 @@ clean-pyc: pip: python3 -m pip install -r requirements.txt +pip-user: + python3 -m pip install --user -r requirements.txt + lint: flake8 dexbot/ build: pip python3 setup.py build +build-user: pip-user + python3 setup.py build + install: build python3 setup.py install -install-user: build +install-user: build-user python3 setup.py install --user git: From 792740535d3f7a42a88d53c5652477fe4f2d6512 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 1 Jun 2018 23:33:21 +0500 Subject: [PATCH 0353/1846] Do not cancel orders on error for staggered strategy Orders should not be cancelled on error because on next run reverse orders will be placed effectively selling all your funds. Closes: #170 --- dexbot/strategies/staggered_orders.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 172c8c0f1..19d5e96f3 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -41,7 +41,6 @@ def __init__(self, *args, **kwargs): self.update_gui_slider() def error(self, *args, **kwargs): - self.cancel_all() self.disabled = True def init_strategy(self): From 0654e0de9322231998c24cb984a2566166544829 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 2 Jun 2018 00:12:33 +0500 Subject: [PATCH 0354/1846] Fix logger verbosity initialization Closes: #172 --- dexbot/ui.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dexbot/ui.py b/dexbot/ui.py index 0b8e32557..4f4b71dc9 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -33,8 +33,8 @@ def new_func(ctx, *args, **kwargs): # Use special format for special workers logger logger = logging.getLogger("dexbot.per_worker") + logger.setLevel(getattr(logging, verbosity.upper())) ch = logging.StreamHandler() - ch.setLevel(getattr(logging, verbosity.upper())) ch.setFormatter(formatter2) logger.addHandler(ch) @@ -46,7 +46,6 @@ def new_func(ctx, *args, **kwargs): 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) logging.getLogger("dexbot").addHandler(ch) logging.getLogger("").handlers = [] From 8acb1df216be02c93e07cde3e65a600bfe0a9a46 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 2 Jun 2018 11:51:09 +0500 Subject: [PATCH 0355/1846] Set high order expiration time for staggered orders By default, dexbot uses python-bitshares order expiration time (7 days) which is quite low for staggered orders. Whether order expired, on the next run of the staggered orders worker it will treat expired order as filled and so a reverse order will be placed, effectively selling your funds (same as #170). Closes: #174 --- dexbot/basestrategy.py | 12 ++++++++---- dexbot/strategies/staggered_orders.py | 14 ++++++++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index d0632d471..6a88931b1 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -347,7 +347,7 @@ def pause(self): # By default, just call cancel_all(); strategies may override this method self.cancel_all() - def market_buy(self, amount, price, return_none=False): + def market_buy(self, amount, price, return_none=False, *args, **kwargs): symbol = self.market['base']['symbol'] precision = self.market['base']['precision'] base_amount = self.truncate(price * amount, precision) @@ -372,7 +372,9 @@ def market_buy(self, amount, price, return_none=False): price, Amount(amount=amount, asset=self.market["quote"]), account=self.account.name, - returnOrderId="head" + returnOrderId="head", + *args, + **kwargs ) self.log.debug('Placed buy order {}'.format(buy_transaction)) buy_order = self.get_order(buy_transaction['orderid'], return_none=return_none) @@ -384,7 +386,7 @@ def market_buy(self, amount, price, return_none=False): return buy_order - def market_sell(self, amount, price, return_none=False): + def market_sell(self, amount, price, return_none=False, *args, **kwargs): symbol = self.market['quote']['symbol'] precision = self.market['quote']['precision'] quote_amount = self.truncate(amount, precision) @@ -409,7 +411,9 @@ def market_sell(self, amount, price, return_none=False): price, Amount(amount=amount, asset=self.market["quote"]), account=self.account.name, - returnOrderId="head" + returnOrderId="head", + *args, + **kwargs ) self.log.debug('Placed sell order {}'.format(sell_transaction)) sell_order = self.get_order(sell_transaction['orderid'], return_none=return_none) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 172c8c0f1..e3d6bac2b 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -28,6 +28,8 @@ def __init__(self, *args, **kwargs): self.increment = self.worker['increment'] / 100 self.upper_bound = self.worker['upper_bound'] self.lower_bound = self.worker['lower_bound'] + # Order expiration time, should be high enough + self.expiration = 60*60*24*365*5 if self['setup_done']: self.check_orders() @@ -89,13 +91,13 @@ def init_strategy(self): # Place the buy orders for buy_order in buy_orders: - order = self.market_buy(buy_order['amount'], buy_order['price']) + order = self.market_buy(buy_order['amount'], buy_order['price'], expiration=self.expiration) 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']) + order = self.market_sell(sell_order['amount'], sell_order['price'], expiration=self.expiration) if order: self.save_order(order) @@ -114,11 +116,11 @@ def place_reverse_order(self, order): if order['base']['symbol'] == self.market['base']['symbol']: # Buy order price = order['price'] * (1 + self.spread) amount = order['quote']['amount'] - new_order = self.market_sell(amount, price) + new_order = self.market_sell(amount, price, expiration=self.expiration) else: # Sell order price = (order['price'] ** -1) / (1 + self.spread) amount = order['base']['amount'] - new_order = self.market_buy(amount, price) + new_order = self.market_buy(amount, price, expiration=self.expiration) if new_order: self.remove_order(order) @@ -130,11 +132,11 @@ def place_order(self, order): if order['base']['symbol'] == self.market['base']['symbol']: # Buy order price = order['price'] amount = order['quote']['amount'] - new_order = self.market_buy(amount, price) + new_order = self.market_buy(amount, price, expiration=self.expiration) else: # Sell order price = order['price'] ** -1 amount = order['base']['amount'] - new_order = self.market_sell(amount, price) + new_order = self.market_sell(amount, price, expiration=self.expiration) self.save_order(new_order) From 837c477bd34ceb728818293d47ad7a796d86dbb6 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 3 Jun 2018 12:08:17 +0500 Subject: [PATCH 0356/1846] Do not allow too often orders checking in staggered orders On every market update we're getting an event in on_market() callback inside worker.py which leads to calling self.workers[worker_name].onMarketUpdate(data) which then calls Strategy.check_orders() while using staggered oders. This leads to querying bitshares for each placed order every time market update comes which may take 3-4 seconds!!! On active market there are lots of market changes constantly, so the bot parses new notifications too slow! He just always stays beyond the actual market. In the active markets like BTS/USD there are lots of orders happening, but the bot is able to parse only one in 3-4 seconds (depending on number of orders and lag to the node). As a quick solution, just limit orders checking rate to once per 5 seconds. Closes: #169 --- dexbot/strategies/staggered_orders.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 172c8c0f1..bec3ff6d5 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1,5 +1,8 @@ import math +from datetime import datetime +from datetime import timedelta + from dexbot.basestrategy import BaseStrategy from dexbot.queue.idle_queue import idle_add @@ -13,7 +16,7 @@ def __init__(self, *args, **kwargs): self.log.info("Initializing Staggered Orders") # Define Callbacks - self.onMarketUpdate += self.check_orders + self.onMarketUpdate += self.onMarketUpdate_wrapper self.onAccount += self.check_orders self.ontick += self.tick @@ -149,6 +152,15 @@ def place_orders(self): self.log.info("Done placing orders") + def onMarketUpdate_wrapper(self, *args, **kwargs): + """ Handle market update callbacks + """ + delta = datetime.now() - self.last_check + + # Only allow to check orders whether minimal time passed + if delta > timedelta(seconds=5): + self.check_orders(*args, **kwargs) + def check_orders(self, *args, **kwargs): """ Tests if the orders need updating """ @@ -167,6 +179,8 @@ def check_orders(self, *args, **kwargs): self.update_gui_profit() self.update_gui_slider() + self.last_check = datetime.now() + @staticmethod def calculate_buy_prices(center_price, spread, increment, lower_bound): buy_prices = [] From ee7992c9c290be198b57604b0e9be2f51c56d33a Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 4 Jun 2018 08:23:54 +0300 Subject: [PATCH 0357/1846] Change dexbot version number to 0.3.1 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index bbe0ebdc1..c18410133 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.7' +VERSION = '0.3.1' AUTHOR = "codaone" __version__ = VERSION From 153962c1c0cca12123b9022bb23f71711ff9697c Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 4 Jun 2018 08:30:40 +0300 Subject: [PATCH 0358/1846] Change dexbot version number to 0.3.2 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index bbe0ebdc1..824e0414c 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.7' +VERSION = '0.3.2' AUTHOR = "codaone" __version__ = VERSION From 7ff5442edc659967fb6e9b219aca661a56bd78bb Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 4 Jun 2018 08:47:06 +0300 Subject: [PATCH 0359/1846] Change dexbot version number to 0.3.3 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 3f516ab49..c5924b133 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.3.0' +VERSION = '0.3.3' AUTHOR = "codaone" __version__ = VERSION From 149aa1961e5058d27ed392f09ae248ddb9553f67 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 4 Jun 2018 09:16:10 +0300 Subject: [PATCH 0360/1846] Change dexbot version number to 0.3.4 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index bbe0ebdc1..93ba1f37d 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.7' +VERSION = '0.3.4' AUTHOR = "codaone" __version__ = VERSION From f9c2e0f315687a9f738bc62634e207914082016b Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 4 Jun 2018 10:02:55 +0300 Subject: [PATCH 0361/1846] Change code styling in staggered_orders.py --- dexbot/strategies/staggered_orders.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index bec3ff6d5..0e2ccb648 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1,5 +1,4 @@ import math - from datetime import datetime from datetime import timedelta @@ -16,7 +15,7 @@ def __init__(self, *args, **kwargs): self.log.info("Initializing Staggered Orders") # Define Callbacks - self.onMarketUpdate += self.onMarketUpdate_wrapper + self.onMarketUpdate += self.on_market_update_wrapper self.onAccount += self.check_orders self.ontick += self.tick @@ -152,7 +151,7 @@ def place_orders(self): self.log.info("Done placing orders") - def onMarketUpdate_wrapper(self, *args, **kwargs): + def on_market_update_wrapper(self, *args, **kwargs): """ Handle market update callbacks """ delta = datetime.now() - self.last_check From 88362febc05f4b2e71d056677b031385694d24d8 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 4 Jun 2018 10:32:54 +0300 Subject: [PATCH 0362/1846] Fix undefined variable in staggered_orders.py --- dexbot/strategies/staggered_orders.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 0e2ccb648..05d7a9198 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -30,6 +30,7 @@ def __init__(self, *args, **kwargs): self.increment = self.worker['increment'] / 100 self.upper_bound = self.worker['upper_bound'] self.lower_bound = self.worker['lower_bound'] + self.last_check = datetime.now() if self['setup_done']: self.check_orders() From f55656b7f054b19338606114fa37912e6b2be2a1 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 4 Jun 2018 10:34:36 +0300 Subject: [PATCH 0363/1846] Change dexbot version number to 0.3.5 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index bbe0ebdc1..c10c6f046 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.7' +VERSION = '0.3.5' AUTHOR = "codaone" __version__ = VERSION From dd6ad97b5fe8100279551a982ba8a8974121a2ff Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 4 Jun 2018 13:48:16 +0300 Subject: [PATCH 0364/1846] Fix crash on empty markets --- dexbot/__init__.py | 2 +- dexbot/strategies/relative_orders.py | 5 ++++- dexbot/strategies/staggered_orders.py | 31 ++++++++++++++------------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index a5e04fb6f..33adf7e32 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.3.6' AUTHOR = "codaone" __version__ = VERSION diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 0540960b1..433b4db84 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -157,7 +157,10 @@ def update_gui_profit(self): def update_gui_slider(self): ticker = self.market.ticker() - latest_price = ticker.get('latest').get('price') + latest_price = ticker.get('latest', {}).get('latest', None) + if not latest_price: + return + total_balance = self.total_balance(self['order_ids']) total = (total_balance['quote'] * latest_price) + total_balance['base'] diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 354c370b8..71442d4f1 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -102,7 +102,6 @@ def place_reverse_order(self, order): """ Replaces an order with a reverse order buy orders become sell orders and sell orders become buy orders """ - assert order['base'], "order is deformed {}".format(dict(order)) if order['base']['symbol'] == self.market['base']['symbol']: # Buy order price = order['price'] * (1 + self.spread) amount = order['quote']['amount'] @@ -248,20 +247,22 @@ def update_gui_profit(self): pass def update_gui_slider(self): + ticker = self.market.ticker() + latest_price = ticker.get('latest', {}).get('price', None) + if not latest_price: + return orders = self.fetch_orders() if orders: - ticker = self.market.ticker() - if 'latest' in ticker and ticker['latest']: - latest_price = ticker['latest'].get('price') - if latest_price: - order_ids = 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 - percentage = 50 - else: - percentage = (total_balance['base'] / total) * 100 - idle_add(self.view.set_worker_slider, self.worker_name, percentage) - self['slider'] = percentage + order_ids = orders.keys() + else: + order_ids = None + total_balance = self.total_balance(order_ids) + 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 From 3733a1a8d85a31e1d5bc847048244a672621a49d Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 4 Jun 2018 13:50:59 +0300 Subject: [PATCH 0365/1846] Fix typo 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 433b4db84..9c5909038 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -157,7 +157,7 @@ def update_gui_profit(self): def update_gui_slider(self): ticker = self.market.ticker() - latest_price = ticker.get('latest', {}).get('latest', None) + latest_price = ticker.get('latest', {}).get('price', None) if not latest_price: return From 6b0c7e7993ee7e7d8ecf60a5a5bf65a498568ca1 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 4 Jun 2018 14:44:47 +0300 Subject: [PATCH 0366/1846] Fix stuff broken in the merge --- dexbot/views/worker_item.py | 9 ++++----- dexbot/views/worker_list.py | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 002be4e10..91f819432 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -14,7 +14,7 @@ class WorkerItemWidget(QtWidgets.QWidget, Ui_widget): - def __init__(self, worker_name, main_ctrl, view): + def __init__(self, worker_name, config, main_ctrl, view): super().__init__() self.main_ctrl = main_ctrl @@ -25,11 +25,7 @@ def __init__(self, worker_name, main_ctrl, view): self.setupUi(self) - 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.handle_remove_worker()) self.edit_button.clicked.connect(lambda: self.handle_edit_worker()) - self.toggle.mouseReleaseEvent = lambda _: self.toggle_worker() self.onoff.mouseReleaseEvent = lambda _: self.toggle_worker() @@ -173,3 +169,6 @@ def handle_edit_worker(self): edit_worker_dialog.worker_data) self.worker_name = new_worker_name self.reload_widget(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 af6f04fe6..cb1c51fb0 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -56,7 +56,8 @@ def __init__(self, main_ctrl): QtGui.QFontDatabase.addApplicationFont(":/bot_widget/font/SourceSansPro-Bold.ttf") def add_worker_widget(self, worker_name): - widget = WorkerItemWidget(worker_name, self.main_ctrl, self) + config = self.config.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 From 53f4c3fd50989d125b9b9dc13b829255bad396d4 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 4 Jun 2018 15:03:46 +0300 Subject: [PATCH 0367/1846] Fix method name in worker_item.py --- dexbot/views/worker_item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 91f819432..de2fe024f 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -133,7 +133,7 @@ def set_worker_slider(self, value): self.base_asset_label.setMinimumWidth(base_width) @gui_error - def handle_remove_worker(self): + def remove_widget_dialog(self): dialog = ConfirmationDialog( 'Are you sure you want to remove worker "{}"?'.format(self.worker_name)) return_value = dialog.exec_() From ad0063b7d0e9d1178f4633d194f384eb07fba1e6 Mon Sep 17 00:00:00 2001 From: Petri Kanerva Date: Tue, 5 Jun 2018 11:08:24 +0300 Subject: [PATCH 0368/1846] Add flow layout to worker list window --- dexbot/views/layouts/__init__.py | 0 dexbot/views/layouts/flow_layout.py | 87 ++++++++++++++++++++ dexbot/views/ui/worker_list_window.ui | 109 +++++++++++++------------- dexbot/views/worker_list.py | 8 +- 4 files changed, 146 insertions(+), 58 deletions(-) create mode 100644 dexbot/views/layouts/__init__.py create mode 100644 dexbot/views/layouts/flow_layout.py diff --git a/dexbot/views/layouts/__init__.py b/dexbot/views/layouts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dexbot/views/layouts/flow_layout.py b/dexbot/views/layouts/flow_layout.py new file mode 100644 index 000000000..c58a29adf --- /dev/null +++ b/dexbot/views/layouts/flow_layout.py @@ -0,0 +1,87 @@ +import sys + +from PyQt5 import Qt, QtCore, QtGui, QtWidgets + +class FlowLayout(QtWidgets.QLayout): + + def __init__(self, parent=None, margin=0, spacing=-1): + super(FlowLayout, self).__init__(parent) + + if parent is not None: + self.setContentsMargins(margin, margin, margin, margin) + + self.setSpacing(spacing) + + self.item_list = [] + + def __del__(self): + item = self.takeAt(0) + while item: + item = self.takeAt(0) + + def addItem(self, item): + self.item_list.append(item) + + def count(self): + return len(self.item_list) + + def itemAt(self, index): + if index >= 0 and index < len(self.item_list): + return self.item_list[index] + + return None + + def takeAt(self, index): + if index >= 0 and index < len(self.item_list): + return self.item_list.pop(index) + + return None + + def expandingDirections(self): + return QtCore.Qt.Orientations(QtCore.Qt.Orientation(0)) + + def hasHeightForWidth(self): + return True + + def heightForWidth(self, width): + height = self._doLayout(QtCore.QRect(0, 0, width, 0), True) + return height + + def setGeometry(self, rect): + super(FlowLayout, self).setGeometry(rect) + self._doLayout(rect, False) + + def sizeHint(self): + return self.minimumSize() + + def minimumSize(self): + size = QtCore.QSize() + + for item in self.item_list: + size = size.expandedTo(item.minimumSize()) + + margin, _, _, _ = self.getContentsMargins() + size += QtCore.QSize(2 * margin, 2 * margin) + return size + + def _doLayout(self, rect, test_only): + x = rect.x() + y = rect.y() + line_height = 0 + + for item in self.item_list: + next_x = x + item.sizeHint().width() + self.spacing() + if next_x - self.spacing() > rect.right() and line_height > 0: + x = rect.x() + y = y + line_height + self.spacing() + next_x = x + item.sizeHint().width() + self.spacing() + line_height = 0 + + if not test_only: + item.setGeometry( + QtCore.QRect(QtCore.QPoint(x, y), item.sizeHint())) + + x = next_x + line_height = max(line_height, item.sizeHint().height()) + + return y + line_height - rect.y() \ No newline at end of file diff --git a/dexbot/views/ui/worker_list_window.ui b/dexbot/views/ui/worker_list_window.ui index d924e5f85..a4d96293d 100644 --- a/dexbot/views/ui/worker_list_window.ui +++ b/dexbot/views/ui/worker_list_window.ui @@ -6,7 +6,7 @@ 0 0 - 809 + 1047 513 @@ -37,6 +37,59 @@ QScrollBar { + + + + + 1 + 1 + + + + false + + + + + + QFrame::NoFrame + + + Qt::ScrollBarAsNeeded + + + QAbstractScrollArea::AdjustToContents + + + true + + + Qt::AlignHCenter|Qt::AlignTop + + + + + 0 + 0 + 1029 + 336 + + + + + 0 + 0 + + + + -7 + + + color: white; + + + + @@ -142,60 +195,6 @@ QScrollBar { - - - - - 1 - 1 - - - - false - - - - - - QFrame::NoFrame - - - Qt::ScrollBarAsNeeded - - - QAbstractScrollArea::AdjustToContents - - - true - - - Qt::AlignHCenter|Qt::AlignTop - - - - - 0 - 0 - 791 - 18 - - - - - 0 - 0 - - - - -7 - - - color: white; - - - - - diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index db2fcd8e8..b2876c8d4 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -8,6 +8,7 @@ from dexbot.queue.queue_dispatcher import ThreadDispatcher from dexbot.queue.idle_queue import idle_add from .errors import gui_error +from .layouts.flow_layout import FlowLayout from PyQt5 import QtCore, QtGui, QtWidgets from bitsharesapi.bitsharesnoderpc import BitSharesNodeRPC @@ -20,7 +21,7 @@ def __init__(self, main_ctrl): super(MainView, self).__init__() self.ui = Ui_MainWindow() self.ui.setupUi(self) - self.worker_container = self.ui.verticalLayout + self.worker_container = self.ui.scrollAreaContent self.max_workers = 10 self.num_of_workers = 0 self.worker_widgets = {} @@ -30,7 +31,8 @@ def __init__(self, main_ctrl): self.main_ctrl.set_info_handler(self.set_worker_status) self.ui.add_worker_button.clicked.connect(lambda: self.handle_add_worker()) - + self.layout = FlowLayout(self.worker_container) + self.layout.setAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignTop) # Load worker widgets from config file workers = main_ctrl.get_workers_data() for worker_name in workers: @@ -58,7 +60,7 @@ 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.layout.addWidget(widget) self.worker_widgets[worker_name] = widget # Limit the max amount of workers so that the performance isn't greatly affected From 405f10cda3f067a216de31a50b1d9ce4605c8ca0 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 6 Jun 2018 10:03:17 +0300 Subject: [PATCH 0369/1846] Fix market asset split in the GUI --- dexbot/controllers/create_worker_controller.py | 3 ++- dexbot/views/worker_item.py | 4 ++-- dexbot/views/worker_list.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/dexbot/controllers/create_worker_controller.py b/dexbot/controllers/create_worker_controller.py index 26b132ad6..e53becc57 100644 --- a/dexbot/controllers/create_worker_controller.py +++ b/dexbot/controllers/create_worker_controller.py @@ -1,4 +1,5 @@ import collections +import re from dexbot.views.errors import gui_error from dexbot.config import Config @@ -125,7 +126,7 @@ def get_strategy_module(worker_data): @staticmethod def get_assets(worker_data): - return worker_data['market'].split('/') + return re.split("[/:]", worker_data['market']) def get_base_asset(self, worker_data): return self.get_assets(worker_data)[1] diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index de2fe024f..0082e7825 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -98,9 +98,9 @@ def set_worker_strategy(self, value): self.strategy_label.setText(value) def set_worker_market(self, value): - self.currency_label.setText(value) - values = re.split("[/:]", value) + market = '/'.join(values) + self.currency_label.setText(market) self.base_asset_label.setText(values[1]) self.quote_asset_label.setText(values[0]) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index cb1c51fb0..3ebfaf658 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -9,7 +9,7 @@ from dexbot.queue.idle_queue import idle_add from .errors import gui_error -from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5 import QtGui, QtWidgets from bitsharesapi.bitsharesnoderpc import BitSharesNodeRPC From 4062967d6c7a36982a6d979cca0170c8508d922f Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 6 Jun 2018 10:20:28 +0300 Subject: [PATCH 0370/1846] Change easyconfigs of staggered orders and relative orders --- dexbot/strategies/relative_orders.py | 15 ++++++++++----- dexbot/strategies/staggered_orders.py | 21 ++++++++++++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 497487e9c..96474d578 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -11,12 +11,17 @@ class Strategy(BaseStrategy): @classmethod def configure(cls): return BaseStrategy.configure() + [ - ConfigElement('center_price_dynamic', 'bool', False, "Dynamic centre price", None), - ConfigElement('center_price', 'float', 0.0, "Initial center price", (0, 0, None)), + ConfigElement('center_price_dynamic', + 'bool', False, 'Dynamic centre price', None), + ConfigElement('center_price', 'float', 0.0, + 'Initial center price', (0, 0, None)), ConfigElement('amount_relative', 'bool', False, - "Amount is expressed as a percentage of the account balance of quote/base asset", None), - ConfigElement('amount', 'float', 1.0, 'The amount of buy/sell orders', (0.0, None)), - ConfigElement('spread', 'float', 5.0, 'The percentage difference between buy and sell', (0.0, 100.0))] + 'Amount is expressed as a percentage of the account balance of quote/base asset', None), + ConfigElement('amount', 'float', 1.0, + 'The amount of buy/sell orders', (0.0, None)), + ConfigElement('spread', 'float', 5.0, + 'The percentage difference between buy and sell (Spread)', (0.0, 100.0)) + ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 2a5d9e460..f99b15d03 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -13,11 +13,22 @@ class Strategy(BaseStrategy): @classmethod def configure(cls): return BaseStrategy.configure() + [ - ConfigElement('upper_bound', 'float', 1.0, 'The top price in the range', (0.0, None)), - ConfigElement('lower_bound', 'float', 1.0, 'The bottom price in the range', (0.0, None)), - ConfigElement('increment', 'float', 1.0, 'The percentage difference between staggered orders', (0.0, 100.0)), - ConfigElement('amount', 'float', 1.0, 'The amount of buy/sell orders', (0.0, None)), - ConfigElement('spread', 'float', 5.0, 'The percentage difference between buy and sell', (0.0, 100.0))] + ConfigElement( + 'amount', 'float', 1.0, 'The amount of buy/sell orders', + (0.0, None)), + ConfigElement( + 'spread', 'float', 6.0, 'The percentage difference between buy and sell (Spread)', + (0.0, None)), + ConfigElement( + 'increment', 'float', 4.0, 'The percentage difference between staggered orders (Increment)', + (0.0, None)), + ConfigElement( + 'upper_bound', 'float', 1.0, 'The top price in the range', + (0.0, None)), + ConfigElement( + 'lower_bound', 'float', 1000.0, 'The bottom price in the range', + (0.0, None)) + ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) From fa4ab35d079d6912cb21f0c8848a7cee878c2472 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 6 Jun 2018 10:41:51 +0300 Subject: [PATCH 0371/1846] Fix CLI logic --- dexbot/cli.py | 3 +-- dexbot/cli_conf.py | 21 ++++++++------------- dexbot/worker.py | 2 -- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index e4ae7f89c..fd799e781 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -17,7 +17,6 @@ from . import errors from . import helper -from ruamel import yaml # We need to do this before importing click if "LANG" not in os.environ: os.environ['LANG'] = 'C.UTF-8' @@ -108,7 +107,7 @@ def run(ctx): def configure(ctx): """ Interactively configure dexbot """ - config = Config(ctx.obj['configfile']) + config = Config(path=ctx.obj['configfile']) configure_dexbot(config) config.save_config() diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index cf7a6da6c..89a7a8b60 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -19,10 +19,7 @@ import os.path import sys import re -import tempfile -import shutil -from dexbot.worker import STRATEGIES from dexbot.whiptail import get_whiptail from dexbot.basestrategy import BaseStrategy @@ -62,10 +59,9 @@ def select_choice(current, choices): def process_config_element(elem, d, config): - """ - Process an item of configuration metadata display a widget as appropriate - d: the Dialog object - config: the config dictionary for this worker + """ Process an item of configuration metadata display a widget as appropriate + d: the Dialog object + config: the config dictionary for this worker """ if elem.type == "string": txt = d.prompt(elem.description, config.get(elem.key, elem.default)) @@ -137,18 +133,17 @@ def setup_systemd(d, config): def configure_worker(d, worker): - strategy = worker.get('module', 'dexbot.strategies.echo') + default_strategy = worker.get('module', 'dexbot.strategies.relative_orders') for i in STRATEGIES: - if strategy == i['class']: - strategy = i['tag'] + if default_strategy == i['class']: + default_strategy = i['tag'] + worker['module'] = d.radiolist( "Choose a worker strategy", select_choice( - strategy, [(i['tag'], i['name']) for i in STRATEGIES])) + default_strategy, [(i['tag'], i['name']) for i in STRATEGIES])) for i in STRATEGIES: if i['tag'] == worker['module']: worker['module'] = i['class'] - # It's always Strategy now, for backwards compatibility only - worker['worker'] = 'Strategy' # Import the worker class but we don't __init__ it here klass = getattr( importlib.import_module(worker["module"]), diff --git a/dexbot/worker.py b/dexbot/worker.py index 1d6296f76..6d648fcf4 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -18,8 +18,6 @@ # 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. -STRATEGIES = [('dexbot.strategies.echo', 'Echo Test')] - class WorkerInfrastructure(threading.Thread): From cba780d7b4244f661f33bcf8a15863cb8d787cfc Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 6 Jun 2018 13:08:08 +0300 Subject: [PATCH 0372/1846] Separated CLI and GUI packages from the birany packages --- .travis.yml | 3 ++- appveyor.yml | 11 +++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 59d77a3dc..6628b07a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,8 @@ 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-cli-$TRAVIS_OS_NAME-$TRAVIS_TAG.tar.gz dist/$TRAVIS_OS_NAME/DEXBot-cli + - tar -czvf dist/DEXBot-gui-$TRAVIS_OS_NAME-$TRAVIS_TAG.tar.gz dist/$TRAVIS_OS_NAME/DEXBot-gui deploy: - provider: releases skip_cleanup: true diff --git a/appveyor.yml b/appveyor.yml index 7d04aabba..faabc242f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -24,15 +24,19 @@ install: after_test: - make package - - '7z a DEXBot-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-cli.exe' + - '7z a DEXBot-cli-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-cli.exe' + - '7z a DEXBot-gui-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe' # @TODO: Run tests.. test_script: - "echo tests..." artifacts: - - path: DEXBot-win64.zip - name: DEXBot-win64.zip + - path: DEXBot-cli-win64.zip + name: DEXBot-cli-win64.zip + + - path: DEXBot-gui-win64.zip + name: DEXBot-gui-win64.zip #---------------------------------# # deployment # @@ -44,7 +48,6 @@ clone_depth: 1 deploy: - provider: GitHub - artifact: DEXBot-win64.zip draft: false prerelease: false force_update: true From cf7611dd1bbb4e066cf1e26d831b59d0a63ad72e Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 7 Jun 2018 09:07:27 +0300 Subject: [PATCH 0373/1846] Change dexbot service config loading/creating logic --- dexbot/cli.py | 14 ++++----- dexbot/cli_conf.py | 74 ++++++++++++++++++++++++++++------------------ 2 files changed, 52 insertions(+), 36 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index fd799e781..e7df271fc 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -13,7 +13,7 @@ configfile ) from .worker import WorkerInfrastructure -from .cli_conf import configure_dexbot +from .cli_conf import configure_dexbot, dexbot_service_running from . import errors from . import helper @@ -107,17 +107,17 @@ def run(ctx): def configure(ctx): """ Interactively configure dexbot """ + # Make sure the dexbot service isn't running while we do the config edits + if dexbot_service_running(): + click.echo("Stopping dexbot daemon") + os.system('systemctl --user stop dexbot') + config = Config(path=ctx.obj['configfile']) configure_dexbot(config) config.save_config() click.echo("New configuration saved") - if config['systemd_status'] == 'installed': - # we are already installed - click.echo("Restarting dexbot daemon") - os.system("systemctl --user restart dexbot") - if config['systemd_status'] == 'install': - os.system("systemctl --user enable dexbot") + if config.get('systemd_status', 'disabled') == 'enabled': click.echo("Starting dexbot daemon") os.system("systemctl --user start dexbot") diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 89a7a8b60..e40b25c6a 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -2,7 +2,7 @@ A module to provide an interactive text-based tool for dexbot configuration The result is dexbot can be run without having to hand-edit config files. If systemd is detected it will offer to install a user service unit (under ~/.local/share/systemd -This requires a per-user systemd process to be runnng +This requires a per-user systemd process to be running Requires the 'whiptail' tool for text-based configuration (so UNIX only) if not available, falls back to a line-based configurator ("NoWhiptail") @@ -13,12 +13,13 @@ for each strategy class. """ - import importlib +import pathlib import os import os.path import sys import re +import subprocess from dexbot.whiptail import get_whiptail from dexbot.basestrategy import BaseStrategy @@ -97,39 +98,53 @@ def process_config_element(elem, d, config): config.get(elem.key, elem.default), elem.extra)) +def dexbot_service_running(): + """ Return True if dexbot service is running + """ + cmd = 'systemctl --user status dexbot' + output = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) + for line in output.stdout.readlines(): + if b'Active:' in line and b'(running)' in line: + return True + return False + + def setup_systemd(d, config): - if config.get("systemd_status", "install") == "reject": - return # Don't nag user if previously said no if not os.path.exists("/etc/systemd"): return # No working systemd - if os.path.exists(SYSTEMD_SERVICE_NAME): - # Dexbot already installed - # So just tell cli.py to quietly restart the daemon - config["systemd_status"] = "installed" + + if not d.confirm( + "Do you want to run dexbot as a background (daemon) process?"): + config['systemd_status'] = 'disabled' return - if d.confirm( - "Do you want to install dexbot as a background (daemon) process?"): - for i in ["~/.local", "~/.local/share", - "~/.local/share/systemd", "~/.local/share/systemd/user"]: - j = os.path.expanduser(i) - if not os.path.exists(j): - os.mkdir(j) - passwd = d.prompt("The wallet password\n" - "NOTE: this will be saved on disc so the worker can run unattended. " - "This means anyone with access to this computer's file can spend all your money", - password=True) + + redo_setup = False + if os.path.exists(SYSTEMD_SERVICE_NAME): + redo_setup = d.confirm('Redo systemd setup?', 'no') + + if not os.path.exists(SYSTEMD_SERVICE_NAME) or redo_setup: + path = '~/.local/share/systemd/user' + path = os.path.expanduser(path) + pathlib.Path(path).mkdir(parents=True, exist_ok=True) + password = d.prompt( + "The wallet password\n" + "NOTE: this will be saved on disc so the worker can run unattended. " + "This means anyone with access to this computer's files can spend all your money", + password=True) + # Because we hold password be restrictive fd = os.open(SYSTEMD_SERVICE_NAME, os.O_WRONLY | os.O_CREAT, 0o600) with open(fd, "w") as fp: fp.write( SYSTEMD_SERVICE_FILE.format( exe=sys.argv[0], - passwd=passwd, + passwd=password, homedir=os.path.expanduser("~"))) - # Signal cli.py to set the unit up after writing config file - config['systemd_status'] = 'install' - else: - config['systemd_status'] = 'reject' + # The dexbot service file was edited, reload the daemon configs + os.system('systemctl --user daemon-reload') + + # Signal cli.py to set the unit up after writing config file + config['systemd_status'] = 'enabled' def configure_worker(d, worker): @@ -145,12 +160,12 @@ def configure_worker(d, worker): if i['tag'] == worker['module']: worker['module'] = i['class'] # Import the worker class but we don't __init__ it here - klass = getattr( + strategy_class = getattr( importlib.import_module(worker["module"]), 'Strategy' ) # Use class metadata for per-worker configuration - configs = klass.configure() + configs = strategy_class.configure() if configs: for c in configs: process_config_element(c, d, worker) @@ -163,7 +178,7 @@ def configure_worker(d, worker): def configure_dexbot(config): d = get_whiptail() workers = config.get('workers', {}) - if len(workers) == 0: + if not workers: while True: txt = d.prompt("Your name for the worker") config['workers'] = {txt: configure_worker(d, {})} @@ -184,10 +199,11 @@ def configure_dexbot(config): del config['workers'][worker_name] strategy = BaseStrategy(worker_name) strategy.purge() # Cancel the orders of the bot - if action == 'NEW': + elif action == 'NEW': txt = d.prompt("Your name for the new worker") config['workers'][txt] = configure_worker(d, {}) - else: + elif action == 'CONF': config['node'] = d.prompt("BitShares node to use", default=config['node']) + setup_systemd(d, config) d.clear() return config From 4e6ebaccfcea8ebfe2530223bcf1c9eb2e7911f4 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 7 Jun 2018 09:12:31 +0300 Subject: [PATCH 0374/1846] Add get method to Config --- dexbot/config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dexbot/config.py b/dexbot/config.py index 5d96b75e2..2b8fca2f3 100644 --- a/dexbot/config.py +++ b/dexbot/config.py @@ -47,6 +47,9 @@ def __delitem__(self, key): def __contains__(self, key): return key in self._config + def get(self, key, default=None): + return self._config.get(key, default) + @property def default_data(self): return {'node': 'wss://status200.bitshares.apasia.tech/ws', 'workers': {}} From 318b4f9356a3b093e047121b91349b89e7124edb Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 7 Jun 2018 09:24:34 +0300 Subject: [PATCH 0375/1846] Change dexbot version number to 0.3.7 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 864d36c2f..950e5de09 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.3.7' AUTHOR = "codaone" __version__ = VERSION From a9997d58502ac9e2dd7e303dc32bda9938bd1c98 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 7 Jun 2018 09:44:16 +0300 Subject: [PATCH 0376/1846] Fix flow_layout.py code styling --- dexbot/views/layouts/flow_layout.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/dexbot/views/layouts/flow_layout.py b/dexbot/views/layouts/flow_layout.py index c58a29adf..393ad2c2b 100644 --- a/dexbot/views/layouts/flow_layout.py +++ b/dexbot/views/layouts/flow_layout.py @@ -1,6 +1,5 @@ -import sys +from PyQt5 import Qt, QtCore, QtWidgets -from PyQt5 import Qt, QtCore, QtGui, QtWidgets class FlowLayout(QtWidgets.QLayout): @@ -26,30 +25,28 @@ def count(self): return len(self.item_list) def itemAt(self, index): - if index >= 0 and index < len(self.item_list): + if 0 <= index < len(self.item_list): return self.item_list[index] - return None def takeAt(self, index): - if index >= 0 and index < len(self.item_list): + if 0 <= index < len(self.item_list): return self.item_list.pop(index) - return None def expandingDirections(self): - return QtCore.Qt.Orientations(QtCore.Qt.Orientation(0)) + return Qt.Orientations(Qt.Orientation(0)) def hasHeightForWidth(self): return True def heightForWidth(self, width): - height = self._doLayout(QtCore.QRect(0, 0, width, 0), True) + height = self._do_layout(QtCore.QRect(0, 0, width, 0), True) return height def setGeometry(self, rect): super(FlowLayout, self).setGeometry(rect) - self._doLayout(rect, False) + self._do_layout(rect, False) def sizeHint(self): return self.minimumSize() @@ -64,7 +61,7 @@ def minimumSize(self): size += QtCore.QSize(2 * margin, 2 * margin) return size - def _doLayout(self, rect, test_only): + def _do_layout(self, rect, test_only): x = rect.x() y = rect.y() line_height = 0 @@ -84,4 +81,4 @@ def _doLayout(self, rect, test_only): x = next_x line_height = max(line_height, item.sizeHint().height()) - return y + line_height - rect.y() \ No newline at end of file + return y + line_height - rect.y() From 91df70be4d8d0e8434db4ebdbf1f96332c6debf3 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 7 Jun 2018 10:01:39 +0300 Subject: [PATCH 0377/1846] Change code layout of worker_list.py --- dexbot/views/worker_list.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index b2876c8d4..184b1d7ef 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -2,26 +2,25 @@ from threading import Thread from dexbot import __version__ +from dexbot.queue.queue_dispatcher import ThreadDispatcher +from dexbot.queue.idle_queue import idle_add from .ui.worker_list_window_ui import Ui_MainWindow from .create_worker import CreateWorkerView from .worker_item import WorkerItemWidget -from dexbot.queue.queue_dispatcher import ThreadDispatcher -from dexbot.queue.idle_queue import idle_add from .errors import gui_error from .layouts.flow_layout import FlowLayout -from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5 import QtGui, QtWidgets from bitsharesapi.bitsharesnoderpc import BitSharesNodeRPC -class MainView(QtWidgets.QMainWindow): +class MainView(QtWidgets.QMainWindow, Ui_MainWindow): def __init__(self, main_ctrl): + super().__init__(flags=0) + self.setupUi(self) self.main_ctrl = main_ctrl - super(MainView, self).__init__() - self.ui = Ui_MainWindow() - self.ui.setupUi(self) - self.worker_container = self.ui.scrollAreaContent + self.max_workers = 10 self.num_of_workers = 0 self.worker_widgets = {} @@ -29,10 +28,10 @@ def __init__(self, main_ctrl): self.statusbar_updater = None self.statusbar_updater_first_run = True self.main_ctrl.set_info_handler(self.set_worker_status) + self.layout = FlowLayout(self.scrollAreaContent) + + self.add_worker_button.clicked.connect(lambda: self.handle_add_worker()) - self.ui.add_worker_button.clicked.connect(lambda: self.handle_add_worker()) - self.layout = FlowLayout(self.worker_container) - self.layout.setAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignTop) # Load worker widgets from config file workers = main_ctrl.get_workers_data() for worker_name in workers: @@ -41,14 +40,14 @@ def __init__(self, main_ctrl): # 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) + self.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() - self.ui.status_bar.showMessage("ver {} - Node delay: - ms".format(__version__)) + self.status_bar.showMessage("ver {} - Node delay: - ms".format(__version__)) self.statusbar_updater = Thread( target=self._update_statusbar_message ) @@ -66,14 +65,14 @@ def add_worker_widget(self, 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) + self.add_worker_button.setEnabled(False) def remove_worker_widget(self, worker_name): self.worker_widgets.pop(worker_name, None) self.num_of_workers -= 1 if self.num_of_workers < self.max_workers: - self.ui.add_worker_button.setEnabled(True) + self.add_worker_button.setEnabled(True) @gui_error def handle_add_worker(self): @@ -107,7 +106,7 @@ def customEvent(self, event): def closeEvent(self, event): self.closing = True - self.ui.status_bar.showMessage("Closing app...") + self.status_bar.showMessage("Closing app...") if self.statusbar_updater and self.statusbar_updater.is_alive(): self.statusbar_updater.join() @@ -138,9 +137,9 @@ def set_statusbar_message(self): latency = -1 if latency != -1: - self.ui.status_bar.showMessage("ver {} - Node delay: {:.2f}ms".format(__version__, latency)) + self.status_bar.showMessage("ver {} - Node delay: {:.2f}ms".format(__version__, latency)) else: - self.ui.status_bar.showMessage("ver {} - Node disconnected".format(__version__)) + self.status_bar.showMessage("ver {} - Node disconnected".format(__version__)) def set_worker_status(self, worker_name, level, status): if worker_name != 'NONE': From fd9d2d387fb2fb9df106f95afcdc92833334f08b Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 7 Jun 2018 10:02:17 +0300 Subject: [PATCH 0378/1846] Change dexbot version number 0.3.8 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 33adf7e32..6143b7c97 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.3.6' +VERSION = '0.3.8' AUTHOR = "codaone" __version__ = VERSION From 3d2da7dfe078ff3a8efa203fe982955afed864d5 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 7 Jun 2018 10:04:02 +0300 Subject: [PATCH 0379/1846] Fix parameter in worker_list.py --- 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 184b1d7ef..42c265a0a 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -17,7 +17,7 @@ class MainView(QtWidgets.QMainWindow, Ui_MainWindow): def __init__(self, main_ctrl): - super().__init__(flags=0) + super().__init__() self.setupUi(self) self.main_ctrl = main_ctrl From eee95f61253d47a0370a2147467b727dd91b8381 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 7 Jun 2018 10:18:52 +0300 Subject: [PATCH 0380/1846] Change dexbot version number to 0.3.9 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 33adf7e32..278855d6b 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -3,7 +3,7 @@ from appdirs import user_config_dir APP_NAME = "dexbot" -VERSION = '0.3.6' +VERSION = '0.3.9' AUTHOR = "codaone" __version__ = VERSION From e168bee7f6c2a2ab16aeed91d502a3c7b9f14b24 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 7 Jun 2018 10:41:25 +0300 Subject: [PATCH 0381/1846] Fix missing default arugment for boolean in cli_conf.py --- dexbot/cli_conf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index e40b25c6a..bdc79c2c4 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -74,7 +74,11 @@ def process_config_element(elem, d, config): elem.key, elem.default)) config[elem.key] = txt if elem.type == "bool": - config[elem.key] = d.confirm(elem.description) + if elem.default: + default = 'yes' + else: + default = 'no' + config[elem.key] = d.confirm(elem.description, default) if elem.type in ("float", "int"): txt = d.prompt(elem.description, str(config.get(elem.key, elem.default))) while True: From 43a568d52e8824f60c1eba19a0c6e8c10e61eae7 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 7 Jun 2018 10:42:00 +0300 Subject: [PATCH 0382/1846] Change relative orders easyconfig order and add missing option --- dexbot/strategies/relative_orders.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 96474d578..d3d3d593c 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -11,14 +11,16 @@ class Strategy(BaseStrategy): @classmethod def configure(cls): return BaseStrategy.configure() + [ - ConfigElement('center_price_dynamic', - 'bool', False, 'Dynamic centre price', None), - ConfigElement('center_price', 'float', 0.0, - 'Initial center price', (0, 0, None)), ConfigElement('amount_relative', 'bool', False, 'Amount is expressed as a percentage of the account balance of quote/base asset', None), ConfigElement('amount', 'float', 1.0, 'The amount of buy/sell orders', (0.0, None)), + ConfigElement('center_price_dynamic', 'bool', True, + 'Dynamic centre price', None), + ConfigElement('center_price', 'float', 0.0, + 'Initial center price', (0, 0, None)), + ConfigElement('center_price_offset', 'bool', False, + 'Center price offset based on asset balances', None), ConfigElement('spread', 'float', 5.0, 'The percentage difference between buy and sell (Spread)', (0.0, 100.0)) ] From 087d61aae0b3c11759183ec8aae75c25f64f3e9d Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 7 Jun 2018 10:48:25 +0300 Subject: [PATCH 0383/1846] Change staggered orders easyconfig layout --- dexbot/strategies/staggered_orders.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index f99b15d03..d40e795f3 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -14,20 +14,20 @@ class Strategy(BaseStrategy): def configure(cls): return BaseStrategy.configure() + [ ConfigElement( - 'amount', 'float', 1.0, 'The amount of buy/sell orders', - (0.0, None)), + 'amount', 'float', 1.0, + 'The amount of buy/sell orders', (0.0, None)), ConfigElement( - 'spread', 'float', 6.0, 'The percentage difference between buy and sell (Spread)', - (0.0, None)), + 'spread', 'float', 6.0, + 'The percentage difference between buy and sell (Spread)', (0.0, None)), ConfigElement( - 'increment', 'float', 4.0, 'The percentage difference between staggered orders (Increment)', - (0.0, None)), + 'increment', 'float', 4.0, + 'The percentage difference between staggered orders (Increment)', (0.0, None)), ConfigElement( - 'upper_bound', 'float', 1.0, 'The top price in the range', - (0.0, None)), + 'upper_bound', 'float', 1.0, + 'The top price in the range', (0.0, None)), ConfigElement( - 'lower_bound', 'float', 1000.0, 'The bottom price in the range', - (0.0, None)) + 'lower_bound', 'float', 1000.0, + 'The bottom price in the range', (0.0, None)) ] def __init__(self, *args, **kwargs): From 174ec9c37492fc48084a7bbf06417098d6959da3 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 7 Jun 2018 11:41:21 +0300 Subject: [PATCH 0384/1846] Add "/" as a separator symbol for the cli market parser --- dexbot/basestrategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index eab90d2b0..e1e435917 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -100,7 +100,7 @@ def configure(cls): ConfigElement("account", "string", "", "BitShares account name for the bot to operate with", ""), ConfigElement("market", "string", "USD:BTS", "BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"", - "[A-Z]+:[A-Z]+") + "[A-Z]+[:\/][A-Z]+") ] def __init__( From 82b1ec70a8c9060c2e79dcb2d476a32fdc5d41be Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 7 Jun 2018 12:41:28 +0300 Subject: [PATCH 0385/1846] Change queue package name to qt_queue --- dexbot/controllers/strategy_controller.py | 2 +- dexbot/{queue => qt_queue}/__init__.py | 0 dexbot/{queue => qt_queue}/idle_queue.py | 0 dexbot/{queue => qt_queue}/queue_dispatcher.py | 2 +- dexbot/strategies/relative_orders.py | 2 +- dexbot/strategies/staggered_orders.py | 2 +- dexbot/views/errors.py | 2 +- dexbot/views/worker_list.py | 4 ++-- 8 files changed, 7 insertions(+), 7 deletions(-) rename dexbot/{queue => qt_queue}/__init__.py (100%) rename dexbot/{queue => qt_queue}/idle_queue.py (100%) rename dexbot/{queue => qt_queue}/queue_dispatcher.py (93%) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 3988168f9..e11131fa3 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -1,4 +1,4 @@ -from dexbot.queue.idle_queue import idle_add +from dexbot.qt_queue.idle_queue import idle_add from dexbot.views.errors import gui_error from dexbot.strategies.staggered_orders import Strategy as StaggeredOrdersStrategy diff --git a/dexbot/queue/__init__.py b/dexbot/qt_queue/__init__.py similarity index 100% rename from dexbot/queue/__init__.py rename to dexbot/qt_queue/__init__.py diff --git a/dexbot/queue/idle_queue.py b/dexbot/qt_queue/idle_queue.py similarity index 100% rename from dexbot/queue/idle_queue.py rename to dexbot/qt_queue/idle_queue.py diff --git a/dexbot/queue/queue_dispatcher.py b/dexbot/qt_queue/queue_dispatcher.py similarity index 93% rename from dexbot/queue/queue_dispatcher.py rename to dexbot/qt_queue/queue_dispatcher.py index 7a3e028da..4fce8b82f 100644 --- a/dexbot/queue/queue_dispatcher.py +++ b/dexbot/qt_queue/queue_dispatcher.py @@ -1,7 +1,7 @@ from PyQt5.Qt import QApplication from PyQt5.QtCore import QThread, QEvent -from dexbot.queue.idle_queue import idle_loop +from dexbot.qt_queue.idle_queue import idle_loop class ThreadDispatcher(QThread): diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index d3d3d593c..366d8e7e5 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -1,7 +1,7 @@ import math from dexbot.basestrategy import BaseStrategy, ConfigElement -from dexbot.queue.idle_queue import idle_add +from dexbot.qt_queue.idle_queue import idle_add class Strategy(BaseStrategy): diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index d40e795f3..afa9b790f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -3,7 +3,7 @@ from datetime import timedelta from dexbot.basestrategy import BaseStrategy, ConfigElement -from dexbot.queue.idle_queue import idle_add +from dexbot.qt_queue.idle_queue import idle_add class Strategy(BaseStrategy): diff --git a/dexbot/views/errors.py b/dexbot/views/errors.py index 4d497dfa9..539fc3a28 100644 --- a/dexbot/views/errors.py +++ b/dexbot/views/errors.py @@ -2,7 +2,7 @@ import traceback from dexbot.ui import translate_error -from dexbot.queue.idle_queue import idle_add +from dexbot.qt_queue.idle_queue import idle_add from PyQt5 import QtWidgets diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 3ebfaf658..33198dfd3 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -5,8 +5,8 @@ from .ui.worker_list_window_ui import Ui_MainWindow from .create_worker import CreateWorkerView from .worker_item import WorkerItemWidget -from dexbot.queue.queue_dispatcher import ThreadDispatcher -from dexbot.queue.idle_queue import idle_add +from dexbot.qt_queue.queue_dispatcher import ThreadDispatcher +from dexbot.qt_queue.idle_queue import idle_add from .errors import gui_error from PyQt5 import QtGui, QtWidgets From ec4dff240c173f68d9292b15f19de3c1d30926ca Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 7 Jun 2018 12:42:09 +0300 Subject: [PATCH 0386/1846] Fix worker purge on cli edit/remove --- dexbot/cli_conf.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index bdc79c2c4..0103ce667 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -24,6 +24,8 @@ from dexbot.whiptail import get_whiptail from dexbot.basestrategy import BaseStrategy +from bitshares import BitShares + # FIXME: auto-discovery of strategies would be cool but can't figure out a way STRATEGIES = [ {'tag': 'relative', @@ -198,11 +200,15 @@ def configure_dexbot(config): if action == 'EDIT': worker_name = d.menu("Select worker to edit", [(i, i) for i in workers]) config['workers'][worker_name] = configure_worker(d, config['workers'][worker_name]) + bitshares_instance = BitShares(config['node']) + strategy = BaseStrategy(worker_name, bitshares_instance=bitshares_instance) + strategy.purge() elif action == 'DEL': worker_name = d.menu("Select worker to delete", [(i, i) for i in workers]) del config['workers'][worker_name] - strategy = BaseStrategy(worker_name) - strategy.purge() # Cancel the orders of the bot + bitshares_instance = BitShares(config['node']) + strategy = BaseStrategy(worker_name, bitshares_instance=bitshares_instance) + strategy.purge() elif action == 'NEW': txt = d.prompt("Your name for the new worker") config['workers'][txt] = configure_worker(d, {}) From 2464829be2841b916cede18e9b200e49f5e7f6af Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 7 Jun 2018 12:49:51 +0300 Subject: [PATCH 0387/1846] Fix cli config boolean value parsing --- dexbot/cli_conf.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 0103ce667..6215b8c91 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -76,11 +76,9 @@ def process_config_element(elem, d, config): elem.key, elem.default)) config[elem.key] = txt if elem.type == "bool": - if elem.default: - default = 'yes' - else: - default = 'no' - config[elem.key] = d.confirm(elem.description, default) + value = config.get(elem.key, elem.default) + value = 'yes' if value else 'no' + config[elem.key] = d.confirm(elem.description, value) if elem.type in ("float", "int"): txt = d.prompt(elem.description, str(config.get(elem.key, elem.default))) while True: From d31d6372f290645520ca9fa42e93bb0a26993c90 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 7 Jun 2018 13:21:24 +0300 Subject: [PATCH 0388/1846] Change dexbot version number to 0.4.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 162a03ff0..a16cb28f8 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = "dexbot" -VERSION = '0.3.9' +VERSION = '0.4.0' AUTHOR = "codaone" __version__ = VERSION From 626747578d6b43abebb49307803a2368ae4f738a Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 8 Jun 2018 08:53:31 +0300 Subject: [PATCH 0389/1846] Created custom error dialog --- dexbot/resources/icons.qrc | 13 +- dexbot/resources/img/bin.png | Bin 768 -> 0 bytes dexbot/resources/img/error.png | Bin 0 -> 6344 bytes dexbot/resources/img/pause.png | Bin 1886 -> 0 bytes dexbot/resources/img/pen.png | Bin 430 -> 0 bytes dexbot/resources/img/play.png | Bin 2216 -> 0 bytes dexbot/views/errors.py | 60 +++++-- dexbot/views/notice.py | 9 +- dexbot/views/ui/error_dialog.ui | 221 ++++++++++++++++++++++++++ dexbot/views/ui/worker_item_widget.ui | 4 +- dexbot/views/ui/worker_list_window.ui | 2 +- 11 files changed, 283 insertions(+), 26 deletions(-) delete mode 100644 dexbot/resources/img/bin.png create mode 100644 dexbot/resources/img/error.png delete mode 100644 dexbot/resources/img/pause.png delete mode 100644 dexbot/resources/img/pen.png delete mode 100644 dexbot/resources/img/play.png create mode 100644 dexbot/views/ui/error_dialog.ui diff --git a/dexbot/resources/icons.qrc b/dexbot/resources/icons.qrc index 1ce322b8e..effdadc5b 100644 --- a/dexbot/resources/icons.qrc +++ b/dexbot/resources/icons.qrc @@ -1,12 +1,13 @@ - + svg/modifystrategy.svg + svg/simplestrategy.svg + + img/dexbot.png svg/dexbot.svg - svg/simplestrategy.svg - img/bin.png - img/pause.png - img/pen.png - img/play.png + + + img/error.png diff --git a/dexbot/resources/img/bin.png b/dexbot/resources/img/bin.png deleted file mode 100644 index 006236bb79431d6610686de34bad19a7b3f280d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 768 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 Y2P-k~C$h(-f&c&j07*qoM6N<$f((LoX#fBK diff --git a/dexbot/resources/img/pen.png b/dexbot/resources/img/pen.png deleted file mode 100644 index 1732eb6730d9db6c7fda8063e038272f13dd1e58..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 430 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!24_zf$B+p3lN}dp`_BCL^Jd@j_@m_+LHmrC z3~CD=+Oo(P&cDU?fkA2qZ-?kao@1gy`i)s4D_%0NFGyU!&~9JPv_sMtotUN=eQ`g)O9%b+BCp@&(ij%2JUxQqLwv2xy+<+d+Iei_w!}tjbc)PmxE?RE^uNKd;a5- zmB%|Cfz5%J1zWr@>Oo|R)uk_{YRG9FYub(+1%kqSx zpM}nUZV`4O;}QC*<>!eOTrYX<9L Wl^GMOR$c*yF@vY8pUXO@geCwOT%0-p diff --git a/dexbot/resources/img/play.png b/dexbot/resources/img/play.png deleted file mode 100644 index 42756bb7e3c9d3a63e0e825e95125d8e20155ade..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2216 zcmV;Z2v_%sP)eN%Ck^$rTuB z^<*vXtR%!Z<8?|S*0%3OgK`-@tp^M&8%g)WPHh-tEyAwihTq*IcRm>e^PH3MY1xYqshBjcNuL157_V2F_)nU)-ou#(0FFb!Q`q491W`Sf z^hro+K$xlct#2Qi)HY+|Gl{L8hft?9COwzbfRjoCJ`U6Y7_bhxCuq~UvB9@L>A71B z2x$?%w)K7J26ScylLMhM*w=IBmOFZj0dIF7$Cr`I=0s&OzqV)iAwOct21F#u%pn@cDH&%)l+=;KPxtv8U{1EObtWG)P9& z_Fh1ksSp9+VywedaVrt=xN|(F%A>I_cm~h6?LpM!A+{KcpiS!mWsLEbnBP?K@9>DZ zsCLgpd*w!~E_f9Ag3~nZ7}wFR^?)ioh6qzk6HbOxu;ca8W%#&!4W4n&gq_NUYsR^b z4lM*4W>F~2ybl_;!%9R<&l!U)r9a0%%U(dGG|EstjqB*r70_;7K~Nt$gB}h+!sCwd zs2}?ZUMYSOUW*6RfN!`e*U_v6K?9nC5Q}DAAXDtriuJ{du&;bAR(j?F)ShJp22~G( zQbItC{YsuTln^{*ABn}T$;PA)0#PCs*obI|Kqly%;av18w)qd^uidqP0rixHS|N&r zLL>u@j+;tLybFKs`U3BD)#AS~W4h-A6e6`jbWl5FYBNM69gg>2!2B!Qpv1cjnPc!B zBB97562u~l10K;~m{2NcT*sHuW^D4+;Df*yrf5pUf(ok#qC~Ca+2);tHvD@*ZNo-i z4UUA*;);n)avKrhwdVKr1;*KinI?QAJGyGIuJs@AYZ=ZXN&_3UV1&ifXTUUhv}wXo z7RT}4i+HW`AU+MAHcj_tywXVIQG1^O6YZm57xEC%1BQxTQrq#%w%s@x`U(L(WT@6a zROeNJQrh?avkO+T%r(g{F^x0)q2nONU;H%=1WpWk<^hW3uBz+5uJdx?JXj4Hobc-* zyxCcU$Pnms0l*U16y%5!0d6p$!diqWa>d|9Wfnxl)A`dAy9opA#Dc|nQ$dGdvpAbP zB9Fmjds$*XVF0C+EOk!A{r2*~h|3g22w0jw9V0C6#Ib&z0I$W3=RC7b56moNYXP2i zRrQ-t+0OtLGjQ^~yOOkl2a0-kV< zzh&SfX+L;I&V5*tHx&jiCom1Kr2wxMFHU+cX_YSvHuA07!5gAiaZG9V@Z{b7iF>>CqG}a*p-w z|3Yk#zapi=?A-gYaoAHBEfo(~i>Gv3U7}2Mrt^pFqfp)a7FvejT7flp54Euc}9S&Oh~*m5j#O@NKwxy;{8S~c5fTM-HCQC=mN>LM)1)9z{2cf + + Dialog + + + + 0 + 0 + 400 + 224 + + + + + 400 + 400 + + + + + 0 + 0 + + + + DEXBot - Error Dialog + + + + + + + 0 + 0 + + + + + 401 + 74 + + + + + + + + 0 + 0 + + + + + 48 + 48 + + + + + 48 + 48 + + + + + + + :/dialog/img/error.png + + + true + + + + + + + + + + + 400 + 25 + + + + ArrowCursor + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + 400 + 25 + + + + ArrowCursor + + + + + + true + + + + + + + + + + + + + + 0 + 0 + + + + + + + + + 400 + 45 + + + + + + + Qt::Horizontal + + + + 20 + 20 + + + + + + + + PointingHandCursor + + + Show details + + + false + + + true + + + + + + + PointingHandCursor + + + Hide details + + + + + + + + 0 + 0 + + + + PointingHandCursor + + + OK + + + + + + + + + + + 400 + 300 + + + + Qt::DefaultContextMenu + + + + + + + + + + diff --git a/dexbot/views/ui/worker_item_widget.ui b/dexbot/views/ui/worker_item_widget.ui index a511079f3..b06470df5 100644 --- a/dexbot/views/ui/worker_item_widget.ui +++ b/dexbot/views/ui/worker_item_widget.ui @@ -113,7 +113,7 @@ border-radius: 20px; - :/bot_widget/svg/simplestrategy.svg + :/worker_widget/svg/simplestrategy.svg true @@ -535,7 +535,7 @@ margin: 0px; - :/bot_widget/svg/modifystrategy.svg:/bot_widget/svg/modifystrategy.svg + :/worker_widget/svg/modifystrategy.svg:/worker_widget/svg/modifystrategy.svg diff --git a/dexbot/views/ui/worker_list_window.ui b/dexbot/views/ui/worker_list_window.ui index d924e5f85..02fbb9f40 100644 --- a/dexbot/views/ui/worker_list_window.ui +++ b/dexbot/views/ui/worker_list_window.ui @@ -92,7 +92,7 @@ QScrollBar { - :/bot_widget/img/dexbot.png + :/general/img/dexbot.png true From 2674de0225aa5a638cff7e93a9904bbe542ffc4a Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 8 Jun 2018 13:59:37 +0300 Subject: [PATCH 0390/1846] Add manual center price offset option --- dexbot/basestrategy.py | 6 +- dexbot/controllers/strategy_controller.py | 4 +- dexbot/strategies/relative_orders.py | 4 +- .../views/ui/forms/relative_orders_widget.ui | 57 ++++++++++++++++++- dexbot/worker.py | 4 +- 5 files changed, 69 insertions(+), 6 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index e1e435917..59ea7aafd 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -202,7 +202,7 @@ def calculate_center_price(self, suppress_errors=False): 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): + def calculate_offset_center_price(self, spread, center_price=None, order_ids=None, manual_offset=0): """ Calculate center price which shifts based on available funds """ if center_price is None: @@ -234,6 +234,10 @@ def calculate_offset_center_price(self, spread, center_price=None, order_ids=Non else: offset_center_price = calculated_center_price + # Calculate final_offset_price if manual center price offset is given + if manual_offset: + offset_center_price = center_price + (center_price * manual_offset) + return offset_center_price @property diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index e11131fa3..7b738f846 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -52,6 +52,7 @@ def set_config_values(self, worker_data): 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)) + self.view.strategy_widget.manual_offset_input.setValue(worker_data.get('manual_offset', 0)) if worker_data.get('center_price_dynamic', True): self.view.strategy_widget.center_price_dynamic_checkbox.setChecked(True) @@ -80,7 +81,8 @@ def values(self): '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() + 'spread': self.view.strategy_widget.spread_input.value(), + 'manual_offset': self.view.strategy_widget.manual_offset_input.value() } return data diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 366d8e7e5..fee20e99a 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -22,7 +22,9 @@ def configure(cls): ConfigElement('center_price_offset', 'bool', False, 'Center price offset based on asset balances', None), ConfigElement('spread', 'float', 5.0, - 'The percentage difference between buy and sell (Spread)', (0.0, 100.0)) + 'The percentage difference between buy and sell (Spread)', (0.0, 100.0)), + ConfigElement('manual_offset', 'float', 0.0, + 'Manual center price offset', (-50.0, 100.0)) ] def __init__(self, *args, **kwargs): diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index e43a8db81..079d4fc73 100644 --- a/dexbot/views/ui/forms/relative_orders_widget.ui +++ b/dexbot/views/ui/forms/relative_orders_widget.ui @@ -6,8 +6,8 @@ 0 0 - 446 - 225 + 416 + 258 @@ -245,6 +245,59 @@ + + + + + 110 + 31 + + + + + 110 + 16777215 + + + + Manual center price offset + + + true + + + manual_offset_input + + + + + + + + 0 + 0 + + + + + 151 + 0 + + + + + + + % + + + -50.000000000000000 + + + 100.000000000000000 + + + diff --git a/dexbot/worker.py b/dexbot/worker.py index 6d648fcf4..2372a7d12 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -8,6 +8,7 @@ import dexbot.errors as errors from dexbot.basestrategy import BaseStrategy +from bitshares import BitShares from bitshares.notify import Notify from bitshares.instance import shared_bitshares_instance @@ -224,7 +225,8 @@ def remove_market(self, worker_name): @staticmethod def remove_offline_worker(config, worker_name): # Initialize the base strategy to get control over the data - strategy = BaseStrategy(worker_name, config) + bitshares_instance = BitShares(config['node']) + strategy = BaseStrategy(worker_name, config, bitshares_instance=bitshares_instance) strategy.purge() def do_next_tick(self, job): From 2612b61cd624031cf6ec6aa546f1740f51208269 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 11 Jun 2018 09:55:57 +0300 Subject: [PATCH 0391/1846] Fix crash when deleting worker caused by missing private key --- dexbot/basestrategy.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index e1e435917..4d3e3c8b0 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -10,6 +10,7 @@ from events import Events import bitsharesapi import bitsharesapi.exceptions +import bitshares.exceptions from bitshares.amount import Amount from bitshares.market import Market from bitshares.account import Account @@ -358,6 +359,9 @@ def _cancel(self, orders): return False else: self.log.exception("Unable to cancel order") + except bitshares.exceptions.MissingKeyError: + self.log.exception('Unable to cancel order(s), private key missing.') + return True def cancel(self, orders): From 80e5ba521ea0692f458c67dbefe09504d85852c8 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 11 Jun 2018 12:36:27 +0300 Subject: [PATCH 0392/1846] Add manual center price to Staggered Orders configuration --- dexbot/controllers/strategy_controller.py | 10 ++ dexbot/strategies/staggered_orders.py | 11 ++ .../views/ui/forms/relative_orders_widget.ui | 2 +- .../views/ui/forms/staggered_orders_widget.ui | 154 ++++++++++++++---- 4 files changed, 145 insertions(+), 32 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index e11131fa3..fc6914c6a 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -113,6 +113,14 @@ def set_config_values(self, worker_data): widget.lower_bound_input.setValue(worker_data.get('lower_bound', 0.000001)) widget.upper_bound_input.setValue(worker_data.get('upper_bound', 1000000)) + self.view.strategy_widget.center_price_input.setValue(worker_data.get('center_price', 0)) + + 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) + self.view.strategy_widget.center_price_input.setDisabled(False) + @gui_error def on_value_change(self): base_asset = self.worker_controller.view.base_asset_input.currentText() @@ -171,6 +179,8 @@ def values(self): data = { 'amount': self.view.strategy_widget.amount_input.value(), 'spread': self.view.strategy_widget.spread_input.value(), + 'center_price': self.view.strategy_widget.center_price_input.value(), + 'center_price_dynamic': self.view.strategy_widget.center_price_dynamic_checkbox.isChecked(), '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() diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index afa9b790f..47eb35042 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -16,6 +16,12 @@ def configure(cls): ConfigElement( 'amount', 'float', 1.0, 'The amount of buy/sell orders', (0.0, None)), + ConfigElement( + 'center_price_dynamic', 'bool', True, + 'Dynamic centre price', None), + ConfigElement( + 'center_price', 'float', 0.0, + 'Initial center price', (0, 0, None)), ConfigElement( 'spread', 'float', 6.0, 'The percentage difference between buy and sell (Spread)', (0.0, None)), @@ -73,7 +79,12 @@ def init_strategy(self): self.cancel_all() self.clear_orders() + # Dynamic / Manual center price center_price = self.calculate_center_price() + + if self.worker['center_price_dynamic']: + center_price = self.worker['center_price'] + amount = self.amount spread = self.spread increment = self.increment diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index e43a8db81..e465806e5 100644 --- a/dexbot/views/ui/forms/relative_orders_widget.ui +++ b/dexbot/views/ui/forms/relative_orders_widget.ui @@ -158,7 +158,7 @@ 8 - -999999999.998999953269958 + 0.000000000000000 999999999.998999953269958 diff --git a/dexbot/views/ui/forms/staggered_orders_widget.ui b/dexbot/views/ui/forms/staggered_orders_widget.ui index 33ad529da..4d02c511f 100644 --- a/dexbot/views/ui/forms/staggered_orders_widget.ui +++ b/dexbot/views/ui/forms/staggered_orders_widget.ui @@ -32,6 +32,65 @@ Worker Parameters + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Amount + + + spread_input + + + + + + + + 0 + 0 + + + + + 151 + 0 + + + + + + + + + + 8 + + + 100000.000000000000000 + + + 0.000000000000000 + + + @@ -200,7 +259,7 @@ - + @@ -228,7 +287,7 @@ - + @@ -256,62 +315,78 @@ - - + + - + 0 0 - 151 + 110 0 - - - - - - - - 8 + + + 110 + 16777215 + - - 100000.000000000000000 + + Center Price - - 0.000000000000000 + + center_price_input - - + + + + false + - + 0 0 - 110 + 140 0 - - - 110 - 16777215 - + + + + + false + + false + + + 8 + + + 0.000000000000000 + + + 999999999.998999953269958 + + + + + - Amount + Calculate center price dynamically - - spread_input + + true @@ -376,5 +451,22 @@ - + + + center_price_dynamic_checkbox + clicked(bool) + center_price_input + setDisabled(bool) + + + 252 + 165 + + + 208 + 136 + + + + From 74d59bc9cf786ee8d66738cf3e852897198eb36b Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 11 Jun 2018 12:47:46 +0300 Subject: [PATCH 0393/1846] Fix the center price logic --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 47eb35042..7fd4b179d 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -80,9 +80,9 @@ def init_strategy(self): self.clear_orders() # Dynamic / Manual center price - center_price = self.calculate_center_price() - if self.worker['center_price_dynamic']: + center_price = self.calculate_center_price() + else: center_price = self.worker['center_price'] amount = self.amount From 39f7711ec44b30516dca8502ec97f701e33466b6 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 11 Jun 2018 13:26:27 +0300 Subject: [PATCH 0394/1846] Fix window height --- dexbot/views/ui/forms/staggered_orders_widget.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/views/ui/forms/staggered_orders_widget.ui b/dexbot/views/ui/forms/staggered_orders_widget.ui index 4d02c511f..2eda84934 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 - 283 + 350 From a284df030681a01e488c55bc44106ddb45a1d3cf Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 12 Jun 2018 08:36:48 +0300 Subject: [PATCH 0395/1846] Change the name of the worker controller --- .../{create_worker_controller.py => worker_controller.py} | 4 ++-- dexbot/views/create_worker.py | 4 ++-- dexbot/views/edit_worker.py | 4 ++-- dexbot/views/worker_item.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) rename dexbot/controllers/{create_worker_controller.py => worker_controller.py} (98%) diff --git a/dexbot/controllers/create_worker_controller.py b/dexbot/controllers/worker_controller.py similarity index 98% rename from dexbot/controllers/create_worker_controller.py rename to dexbot/controllers/worker_controller.py index e53becc57..21a324d9c 100644 --- a/dexbot/controllers/create_worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -14,7 +14,7 @@ from bitsharesbase.account import PrivateKey -class CreateWorkerController: +class WorkerController: def __init__(self, view, bitshares_instance, mode): self.view = view @@ -38,7 +38,7 @@ def strategies(self): def get_strategies(): """ Static method for getting the strategies """ - controller = CreateWorkerController(None, None, None) + controller = WorkerController(None, None, None) return controller.strategies @property diff --git a/dexbot/views/create_worker.py b/dexbot/views/create_worker.py index fa5f6d40c..8ef01a42e 100644 --- a/dexbot/views/create_worker.py +++ b/dexbot/views/create_worker.py @@ -1,5 +1,5 @@ from .ui.create_worker_window_ui import Ui_Dialog -from dexbot.controllers.create_worker_controller import CreateWorkerController +from dexbot.controllers.worker_controller import WorkerController from PyQt5 import QtWidgets @@ -9,7 +9,7 @@ class CreateWorkerView(QtWidgets.QDialog, Ui_Dialog): def __init__(self, bitshares_instance): super().__init__() self.strategy_widget = None - controller = CreateWorkerController(self, bitshares_instance, 'add') + controller = WorkerController(self, bitshares_instance, 'add') self.controller = controller self.setupUi(self) diff --git a/dexbot/views/edit_worker.py b/dexbot/views/edit_worker.py index fa0306d00..24747e4b3 100644 --- a/dexbot/views/edit_worker.py +++ b/dexbot/views/edit_worker.py @@ -1,5 +1,5 @@ from .ui.edit_worker_window_ui import Ui_Dialog -from dexbot.controllers.create_worker_controller import CreateWorkerController +from dexbot.controllers.worker_controller import WorkerController from PyQt5 import QtWidgets @@ -10,7 +10,7 @@ def __init__(self, parent_widget, bitshares_instance, worker_name, config): super().__init__() self.worker_name = worker_name self.strategy_widget = None - controller = CreateWorkerController(self, bitshares_instance, 'edit') + controller = WorkerController(self, bitshares_instance, 'edit') self.controller = controller self.parent_widget = parent_widget diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 0082e7825..d4ed114c4 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -5,7 +5,7 @@ from .edit_worker import EditWorkerView from .errors import gui_error from dexbot.storage import db_worker -from dexbot.controllers.create_worker_controller import CreateWorkerController +from dexbot.controllers.worker_controller import WorkerController from dexbot.views.errors import gui_error from dexbot.resources import icons_rc @@ -39,7 +39,7 @@ def setup_ui_data(self, config): self.set_worker_market(market) module = config['workers'][worker_name]['module'] - strategies = CreateWorkerController.get_strategies() + strategies = WorkerController.get_strategies() self.set_worker_strategy(strategies[module]['name']) profit = db_worker.get_item(worker_name, 'profit') From 76356f6f4eb8f1da94c1eab2e5e03ff4e15a0ed8 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 12 Jun 2018 08:44:31 +0300 Subject: [PATCH 0396/1846] Add uppercase validator to worker's assets input fields --- dexbot/controllers/worker_controller.py | 8 ++++++++ dexbot/views/create_worker.py | 8 +++++++- dexbot/views/edit_worker.py | 8 +++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index 21a324d9c..e5e0db42c 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -12,6 +12,7 @@ from bitshares.asset import Asset from bitshares.account import Account from bitsharesbase.account import PrivateKey +from PyQt5 import QtGui class WorkerController: @@ -246,3 +247,10 @@ def handle_save(self): } self.view.worker_name = self.view.worker_name_input.text() self.view.accept() + + +class UppercaseValidator(QtGui.QValidator): + + @staticmethod + def validate(string, pos): + return QtGui.QValidator.Acceptable, string.upper(), pos diff --git a/dexbot/views/create_worker.py b/dexbot/views/create_worker.py index 8ef01a42e..39d488991 100644 --- a/dexbot/views/create_worker.py +++ b/dexbot/views/create_worker.py @@ -1,5 +1,5 @@ from .ui.create_worker_window_ui import Ui_Dialog -from dexbot.controllers.worker_controller import WorkerController +from dexbot.controllers.worker_controller import WorkerController, UppercaseValidator from PyQt5 import QtWidgets @@ -14,6 +14,8 @@ def __init__(self, bitshares_instance): self.setupUi(self) + validator = UppercaseValidator(self) + # Todo: Using a model here would be more Qt like # Populate the comboboxes strategies = self.controller.strategies @@ -25,6 +27,10 @@ def __init__(self, bitshares_instance): self.worker_name = controller.get_unique_worker_name() self.worker_name_input.setText(self.worker_name) + # Validating assets fields + self.base_asset_input.setValidator(validator) + self.quote_asset_input.setValidator(validator) + # Set signals self.strategy_input.currentTextChanged.connect(lambda: controller.change_strategy_form()) self.save_button.clicked.connect(lambda: controller.handle_save()) diff --git a/dexbot/views/edit_worker.py b/dexbot/views/edit_worker.py index 24747e4b3..c48b442c5 100644 --- a/dexbot/views/edit_worker.py +++ b/dexbot/views/edit_worker.py @@ -1,5 +1,5 @@ from .ui.edit_worker_window_ui import Ui_Dialog -from dexbot.controllers.worker_controller import WorkerController +from dexbot.controllers.worker_controller import WorkerController, UppercaseValidator from PyQt5 import QtWidgets @@ -17,6 +17,8 @@ def __init__(self, parent_widget, bitshares_instance, worker_name, config): self.setupUi(self) worker_data = config['workers'][worker_name] + validator = UppercaseValidator(self) + # Todo: Using a model here would be more Qt like # Populate the comboboxes strategies = self.controller.strategies @@ -32,6 +34,10 @@ def __init__(self, parent_widget, bitshares_instance, 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)) + # Validating assets fields + self.base_asset_input.setValidator(validator) + self.quote_asset_input.setValidator(validator) + # Set signals self.strategy_input.currentTextChanged.connect(lambda: controller.change_strategy_form()) self.save_button.clicked.connect(lambda: self.controller.handle_save()) From 960d80447b86d406355c91f919728fe13611bf27 Mon Sep 17 00:00:00 2001 From: joelvai <10694041+joelvai@users.noreply.github.com> Date: Wed, 13 Jun 2018 08:42:40 +0300 Subject: [PATCH 0397/1846] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 22 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 7 +++++++ 2 files changed, 29 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..f26d68726 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,22 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +## Expected Behavior + + +## Actual Behavior + + +## Steps to Reproduce the Problem + + 1. + 2. + 3. + +## Specifications + + - Version: + - OS: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..7ebfb7420 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,7 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + + From 9553da4db50e9bb5da97df8b920d75c4c89a9f13 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 13 Jun 2018 08:45:10 +0300 Subject: [PATCH 0398/1846] Remove old issue template file --- docs/issue_template.md | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 docs/issue_template.md diff --git a/docs/issue_template.md b/docs/issue_template.md deleted file mode 100644 index 4054600ea..000000000 --- a/docs/issue_template.md +++ /dev/null @@ -1,16 +0,0 @@ -## Expected Behavior - - -## Actual Behavior - - -## Steps to Reproduce the Problem - - 1. - 2. - 3. - -## Specifications - - - Version: - - OS: From a66e25d709e7d1fbb125f78bc657f8bc1d5f336d Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 13 Jun 2018 12:44:17 +0300 Subject: [PATCH 0399/1846] Change relative orders --- dexbot/strategies/relative_orders.py | 34 +++++++++++++++++++--------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 366d8e7e5..f6f4a9633 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -104,10 +104,11 @@ def update_orders(self): # Cancel the orders before redoing them self.cancel_all() + self.clear_orders() # Mark the orders empty - self['buy_order'] = {} - self['sell_order'] = {} + # self['buy_order'] = {} + # self['sell_order'] = {} order_ids = [] @@ -117,13 +118,15 @@ def update_orders(self): # Buy Side buy_order = self.market_buy(amount_base, self.buy_price, True) if buy_order: - self['buy_order'] = buy_order + # self['buy_order'] = sell_order + self.save_order(buy_order) order_ids.append(buy_order['id']) # Sell Side sell_order = self.market_sell(amount_quote, self.sell_price, True) if sell_order: - self['sell_order'] = sell_order + # self['sell_order'] = sell_order + self.save_order(sell_order) order_ids.append(sell_order['id']) self['order_ids'] = order_ids @@ -137,16 +140,19 @@ def update_orders(self): def check_orders(self, *args, **kwargs): """ Tests if the orders need updating """ - stored_sell_order = self['sell_order'] - stored_buy_order = self['buy_order'] - current_sell_order = self.get_order(stored_sell_order) - current_buy_order = self.get_order(stored_buy_order) + orders = self.fetch_orders() - if not current_sell_order or not current_buy_order: - # Either buy or sell order is missing, update both orders + if not orders: self.update_orders() else: self.log.info("Orders correct on market") + for order_id, order in orders.items(): + # Looks up order from BitShares + current_order = self.get_order(order_id) + + if not current_order: + self.update_orders() + break if self.view: self.update_gui_profit() @@ -168,7 +174,13 @@ def update_gui_slider(self): if not latest_price: return - total_balance = self.total_balance(self['order_ids']) + order_ids = None + orders = self.fetch_orders() + + if orders: + order_ids = 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 655ee62bcd7b3f108256a8710b49bfe4e193648a Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 13 Jun 2018 14:23:46 +0300 Subject: [PATCH 0400/1846] Revert "Add manual center price to Staggered Orders configuration" This reverts commit 80e5ba521ea0692f458c67dbefe09504d85852c8. --- dexbot/controllers/strategy_controller.py | 10 -- dexbot/strategies/staggered_orders.py | 13 +- .../views/ui/forms/relative_orders_widget.ui | 2 +- .../views/ui/forms/staggered_orders_widget.ui | 154 ++++-------------- 4 files changed, 33 insertions(+), 146 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index fc6914c6a..e11131fa3 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -113,14 +113,6 @@ def set_config_values(self, worker_data): widget.lower_bound_input.setValue(worker_data.get('lower_bound', 0.000001)) widget.upper_bound_input.setValue(worker_data.get('upper_bound', 1000000)) - self.view.strategy_widget.center_price_input.setValue(worker_data.get('center_price', 0)) - - 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) - self.view.strategy_widget.center_price_input.setDisabled(False) - @gui_error def on_value_change(self): base_asset = self.worker_controller.view.base_asset_input.currentText() @@ -179,8 +171,6 @@ def values(self): data = { 'amount': self.view.strategy_widget.amount_input.value(), 'spread': self.view.strategy_widget.spread_input.value(), - 'center_price': self.view.strategy_widget.center_price_input.value(), - 'center_price_dynamic': self.view.strategy_widget.center_price_dynamic_checkbox.isChecked(), '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() diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 7fd4b179d..afa9b790f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -16,12 +16,6 @@ def configure(cls): ConfigElement( 'amount', 'float', 1.0, 'The amount of buy/sell orders', (0.0, None)), - ConfigElement( - 'center_price_dynamic', 'bool', True, - 'Dynamic centre price', None), - ConfigElement( - 'center_price', 'float', 0.0, - 'Initial center price', (0, 0, None)), ConfigElement( 'spread', 'float', 6.0, 'The percentage difference between buy and sell (Spread)', (0.0, None)), @@ -79,12 +73,7 @@ def init_strategy(self): self.cancel_all() self.clear_orders() - # Dynamic / Manual center price - if self.worker['center_price_dynamic']: - center_price = self.calculate_center_price() - else: - center_price = self.worker['center_price'] - + center_price = self.calculate_center_price() amount = self.amount spread = self.spread increment = self.increment diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index e465806e5..e43a8db81 100644 --- a/dexbot/views/ui/forms/relative_orders_widget.ui +++ b/dexbot/views/ui/forms/relative_orders_widget.ui @@ -158,7 +158,7 @@ 8 - 0.000000000000000 + -999999999.998999953269958 999999999.998999953269958 diff --git a/dexbot/views/ui/forms/staggered_orders_widget.ui b/dexbot/views/ui/forms/staggered_orders_widget.ui index 2eda84934..b2e7cb3b3 100644 --- a/dexbot/views/ui/forms/staggered_orders_widget.ui +++ b/dexbot/views/ui/forms/staggered_orders_widget.ui @@ -32,65 +32,6 @@ Worker Parameters - - - - - 0 - 0 - - - - - 110 - 0 - - - - - 110 - 16777215 - - - - Amount - - - spread_input - - - - - - - - 0 - 0 - - - - - 151 - 0 - - - - - - - - - - 8 - - - 100000.000000000000000 - - - 0.000000000000000 - - - @@ -259,7 +200,7 @@ - + @@ -287,7 +228,7 @@ - + @@ -315,78 +256,62 @@ - - + + - + 0 0 - 110 + 151 0 - - - 110 - 16777215 - + + - - Center Price + + - - center_price_input + + 8 + + + 100000.000000000000000 + + + 0.000000000000000 - - - - false - + + - + 0 0 - 140 + 110 0 - - - - - false - - - false - - - 8 - - - 0.000000000000000 - - - 999999999.998999953269958 + + + 110 + 16777215 + - - - - - Calculate center price dynamically + Amount - - true + + spread_input @@ -451,22 +376,5 @@ - - - center_price_dynamic_checkbox - clicked(bool) - center_price_input - setDisabled(bool) - - - 252 - 165 - - - 208 - 136 - - - - + From 26d876c7cdbd9410f944b271f7b61cb19bc5be2a Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 13 Jun 2018 14:34:55 +0300 Subject: [PATCH 0401/1846] Change dexbot version number to 0.4.1 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index a16cb28f8..ff9eac418 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = "dexbot" -VERSION = '0.4.0' +VERSION = '0.4.1' AUTHOR = "codaone" __version__ = VERSION From d2ccbaf41f416b3a325a2021f707beb76b321122 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 13 Jun 2018 14:49:11 +0300 Subject: [PATCH 0402/1846] Change dexbot version number to 0.4.2 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index a16cb28f8..c18e15f57 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = "dexbot" -VERSION = '0.4.0' +VERSION = '0.4.2' AUTHOR = "codaone" __version__ = VERSION From da9772e2d8a79eee7347e75f6c6f8dd135537344 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 11 Jun 2018 12:36:27 +0300 Subject: [PATCH 0403/1846] Add manual center price to Staggered Orders configuration --- dexbot/controllers/strategy_controller.py | 10 ++ dexbot/strategies/staggered_orders.py | 11 ++ .../views/ui/forms/relative_orders_widget.ui | 2 +- .../views/ui/forms/staggered_orders_widget.ui | 154 ++++++++++++++---- 4 files changed, 145 insertions(+), 32 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index e11131fa3..fc6914c6a 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -113,6 +113,14 @@ def set_config_values(self, worker_data): widget.lower_bound_input.setValue(worker_data.get('lower_bound', 0.000001)) widget.upper_bound_input.setValue(worker_data.get('upper_bound', 1000000)) + self.view.strategy_widget.center_price_input.setValue(worker_data.get('center_price', 0)) + + 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) + self.view.strategy_widget.center_price_input.setDisabled(False) + @gui_error def on_value_change(self): base_asset = self.worker_controller.view.base_asset_input.currentText() @@ -171,6 +179,8 @@ def values(self): data = { 'amount': self.view.strategy_widget.amount_input.value(), 'spread': self.view.strategy_widget.spread_input.value(), + 'center_price': self.view.strategy_widget.center_price_input.value(), + 'center_price_dynamic': self.view.strategy_widget.center_price_dynamic_checkbox.isChecked(), '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() diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index afa9b790f..47eb35042 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -16,6 +16,12 @@ def configure(cls): ConfigElement( 'amount', 'float', 1.0, 'The amount of buy/sell orders', (0.0, None)), + ConfigElement( + 'center_price_dynamic', 'bool', True, + 'Dynamic centre price', None), + ConfigElement( + 'center_price', 'float', 0.0, + 'Initial center price', (0, 0, None)), ConfigElement( 'spread', 'float', 6.0, 'The percentage difference between buy and sell (Spread)', (0.0, None)), @@ -73,7 +79,12 @@ def init_strategy(self): self.cancel_all() self.clear_orders() + # Dynamic / Manual center price center_price = self.calculate_center_price() + + if self.worker['center_price_dynamic']: + center_price = self.worker['center_price'] + amount = self.amount spread = self.spread increment = self.increment diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index e43a8db81..e465806e5 100644 --- a/dexbot/views/ui/forms/relative_orders_widget.ui +++ b/dexbot/views/ui/forms/relative_orders_widget.ui @@ -158,7 +158,7 @@ 8 - -999999999.998999953269958 + 0.000000000000000 999999999.998999953269958 diff --git a/dexbot/views/ui/forms/staggered_orders_widget.ui b/dexbot/views/ui/forms/staggered_orders_widget.ui index 33ad529da..4d02c511f 100644 --- a/dexbot/views/ui/forms/staggered_orders_widget.ui +++ b/dexbot/views/ui/forms/staggered_orders_widget.ui @@ -32,6 +32,65 @@ Worker Parameters + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + Amount + + + spread_input + + + + + + + + 0 + 0 + + + + + 151 + 0 + + + + + + + + + + 8 + + + 100000.000000000000000 + + + 0.000000000000000 + + + @@ -200,7 +259,7 @@ - + @@ -228,7 +287,7 @@ - + @@ -256,62 +315,78 @@ - - + + - + 0 0 - 151 + 110 0 - - - - - - - - 8 + + + 110 + 16777215 + - - 100000.000000000000000 + + Center Price - - 0.000000000000000 + + center_price_input - - + + + + false + - + 0 0 - 110 + 140 0 - - - 110 - 16777215 - + + + + + false + + false + + + 8 + + + 0.000000000000000 + + + 999999999.998999953269958 + + + + + - Amount + Calculate center price dynamically - - spread_input + + true @@ -376,5 +451,22 @@ - + + + center_price_dynamic_checkbox + clicked(bool) + center_price_input + setDisabled(bool) + + + 252 + 165 + + + 208 + 136 + + + + From ba5c625e1d3862ce60481d733b0d2ece63094ff8 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 13 Jun 2018 15:16:37 +0300 Subject: [PATCH 0404/1846] Change dexbot version number to 0.4.3 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index a16cb28f8..d03f3371e 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = "dexbot" -VERSION = '0.4.0' +VERSION = '0.4.3' AUTHOR = "codaone" __version__ = VERSION From 683abc4efd40408928c37e2b42f72a5468c9d709 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 11 Jun 2018 12:47:46 +0300 Subject: [PATCH 0405/1846] Fix the center price logic --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 47eb35042..7fd4b179d 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -80,9 +80,9 @@ def init_strategy(self): self.clear_orders() # Dynamic / Manual center price - center_price = self.calculate_center_price() - if self.worker['center_price_dynamic']: + center_price = self.calculate_center_price() + else: center_price = self.worker['center_price'] amount = self.amount From 8c9cc494a12695a455ed5969c6c9918f5c858e58 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 14 Jun 2018 14:41:54 +0300 Subject: [PATCH 0406/1846] Add logging to sold orders --- dexbot/basestrategy.py | 40 +++++++++++++++++++++++++++ dexbot/controllers/main_controller.py | 5 +++- dexbot/helper.py | 20 ++++++++++++++ dexbot/strategies/relative_orders.py | 14 +++++++--- dexbot/strategies/staggered_orders.py | 2 ++ 5 files changed, 76 insertions(+), 5 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 4d3e3c8b0..1fe278413 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -1,3 +1,4 @@ +import datetime import logging import collections import time @@ -181,6 +182,10 @@ def __init__( 'is_disabled': lambda: self.disabled} ) + self.orders_log = logging.LoggerAdapter( + logging.getLogger('dexbot.orders_log'), {} + ) + def calculate_center_price(self, suppress_errors=False): ticker = self.market.ticker() highest_bid = ticker.get("highestBid") @@ -391,6 +396,7 @@ def pause(self): """ # By default, just call cancel_all(); strategies may override this method self.cancel_all() + self.clear_orders() def market_buy(self, amount, price, return_none=False, *args, **kwargs): symbol = self.market['base']['symbol'] @@ -586,3 +592,37 @@ def truncate(number, decimals): """ Change the decimal point of a number without rounding """ return math.floor(number * 10 ** decimals) / 10 ** decimals + + def write_order_log(self, order): + # ID; + # operation_type; + # base_asset; + # base_amount; + # quote_asset; + # quote_amount; + # timestamp + + if order['base']['symbol'] == self.market['base']['symbol']: + operation_type = 'BUY' + base_symbol = order['base']['symbol'] + base_amount = -order['base']['amount'] + quote_symbol = order['quote']['symbol'] + quote_amount = order['quote']['amount'] + else: + operation_type = 'SELL' + base_symbol = order['quote']['symbol'] + base_amount = order['quote']['amount'] + quote_symbol = order['base']['symbol'] + quote_amount = -order['base']['amount'] + + message = '{};{};{};{};{};{};{}'.format( + order['id'], + operation_type, + base_symbol, + base_amount, + quote_symbol, + quote_amount, + datetime.datetime.now().isoformat() + ) + + self.orders_log.info(message) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 7cfa51b0e..ab27eb1ea 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -1,7 +1,7 @@ import logging import sys -from dexbot import VERSION +from dexbot import VERSION, helper from dexbot.worker import WorkerInfrastructure from dexbot.views.errors import PyQtHandler @@ -30,6 +30,9 @@ def __init__(self, bitshares_instance, config): logger.info("DEXBot {} on python {} {}".format(VERSION, sys.version[:6], sys.platform), extra={ 'worker_name': 'NONE', 'account': 'NONE', 'market': 'NONE'}) + # Configure orders logging + helper.initialize_orders_log() + def set_info_handler(self, handler): self.pyqt_handler.set_info_handler(handler) diff --git a/dexbot/helper.py b/dexbot/helper.py index 67b28c466..b57075506 100644 --- a/dexbot/helper.py +++ b/dexbot/helper.py @@ -1,6 +1,7 @@ import os import shutil import errno +import logging def mkdir(d): @@ -26,3 +27,22 @@ def remove(path): shutil.rmtree(path) except FileNotFoundError: return + + +def initialize_orders_log(): + """ Creates .csv log file, adds the headers first time only + """ + filename = 'orders.csv' + file = os.path.isfile(filename) + + formatter = logging.Formatter('%(message)s') + logger = logging.getLogger("dexbot.orders_log") + + file_handler = logging.FileHandler(filename) + file_handler.setFormatter(formatter) + + logger.addHandler(file_handler) + logger.setLevel(logging.INFO) + + if not file: + logger.info("ID;operation_type;base_asset;base_amount;quote_asset;quote_amount;timestamp") diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index f6f4a9633..2a6f76b8e 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -131,6 +131,7 @@ def update_orders(self): self['order_ids'] = order_ids + # Logger here as well self.log.info("Done placing orders") # Some orders weren't successfully created, redo them @@ -141,6 +142,7 @@ def check_orders(self, *args, **kwargs): """ Tests if the orders need updating """ orders = self.fetch_orders() + order_check_flag = False if not orders: self.update_orders() @@ -148,11 +150,15 @@ def check_orders(self, *args, **kwargs): self.log.info("Orders correct on market") for order_id, order in orders.items(): # Looks up order from BitShares - current_order = self.get_order(order_id) + current_bitshares_order = self.get_order(order_id) - if not current_order: - self.update_orders() - break + if not current_bitshares_order: + if not order_check_flag: + order_check_flag = True + self.write_order_log(order) + + if order_check_flag: + self.update_orders() if self.view: self.update_gui_profit() diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index afa9b790f..ae9fa291e 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -190,6 +190,8 @@ def check_orders(self, *args, **kwargs): for order_id, order in orders.items(): current_order = self.get_order(order_id) if not current_order: + # Write order to .csv log + self.write_order_log(order) self.place_reverse_order(order) order_placed = True From 1a1b4d5a8c9a70352460b24b0fea7e0e4ec98503 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 14 Jun 2018 15:30:14 +0300 Subject: [PATCH 0407/1846] Change the location of the orders log file --- dexbot/__init__.py | 4 ++-- dexbot/helper.py | 6 +++++- dexbot/storage.py | 9 ++++----- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index d03f3371e..402b1b402 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ -APP_NAME = "dexbot" +APP_NAME = 'dexbot' VERSION = '0.4.3' -AUTHOR = "codaone" +AUTHOR = 'Codaone Oy' __version__ = VERSION diff --git a/dexbot/helper.py b/dexbot/helper.py index b57075506..785338554 100644 --- a/dexbot/helper.py +++ b/dexbot/helper.py @@ -2,6 +2,9 @@ import shutil import errno import logging +from appdirs import user_data_dir + +from dexbot import APP_NAME, AUTHOR def mkdir(d): @@ -32,7 +35,8 @@ def remove(path): def initialize_orders_log(): """ Creates .csv log file, adds the headers first time only """ - filename = 'orders.csv' + data_dir = user_data_dir(APP_NAME, AUTHOR) + filename = data_dir + '/orders.csv' file = os.path.isfile(filename) formatter = logging.Formatter('%(message)s') diff --git a/dexbot/storage.py b/dexbot/storage.py index b454259dd..263678a83 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -5,17 +5,16 @@ import uuid from appdirs import user_data_dir +from . import helper +from dexbot import APP_NAME, AUTHOR + from sqlalchemy import create_engine, Column, String, Integer from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -from . import helper - Base = declarative_base() # For dexbot.sqlite file -appname = "dexbot" -appauthor = "Codaone Oy" storageDatabase = "dexbot.sqlite" @@ -278,7 +277,7 @@ def _fetch_orders(self, worker, token): # Derive sqlite file directory -data_dir = user_data_dir(appname, appauthor) +data_dir = user_data_dir(APP_NAME, AUTHOR) sqlDataBaseFile = os.path.join(data_dir, storageDatabase) # Create directory for sqlite file From 30e53746090324a099d2dd0feb929d7affe8608a Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 15 Jun 2018 10:21:35 +0300 Subject: [PATCH 0408/1846] Fix worker creation after deleting config --- dexbot/basestrategy.py | 4 ++++ dexbot/controllers/main_controller.py | 7 ++++++- dexbot/storage.py | 5 +++++ dexbot/views/worker_item.py | 2 +- dexbot/views/worker_list.py | 2 ++ dexbot/worker.py | 4 ++++ 6 files changed, 22 insertions(+), 2 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 1fe278413..206c7c542 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -492,6 +492,10 @@ def purge(self): self.clear_orders() self.clear() + @staticmethod + def purge_database_only(worker_name): + Storage.clear_worker_data(worker_name) + @staticmethod def get_order_amount(order, asset_type): try: diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index ab27eb1ea..4b68b390f 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -36,7 +36,7 @@ def __init__(self, bitshares_instance, config): def set_info_handler(self, handler): self.pyqt_handler.set_info_handler(handler) - def create_worker(self, worker_name, config, view): + def start_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(): self.worker_manager.add_worker(worker_name, config) @@ -64,3 +64,8 @@ def remove_worker(self, worker_name): config = self.config.get_worker_config(worker_name) WorkerInfrastructure.remove_offline_worker(config, worker_name) + @staticmethod + def create_worker(worker_name): + # Deletes old worker's data + WorkerInfrastructure.remove_offline_worker_data_only(worker_name) + diff --git a/dexbot/storage.py b/dexbot/storage.py index 263678a83..32305f267 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -98,6 +98,11 @@ def fetch_orders(self, worker=None): worker = self.category return db_worker.fetch_orders(worker) + @staticmethod + def clear_worker_data(worker): + db_worker.clear_orders(worker) + db_worker.clear(worker) + class DatabaseWorker(threading.Thread): """ Thread safe database worker diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index d4ed114c4..d412b02a9 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -74,7 +74,7 @@ def _toggle_worker(self, toggle_label_text, toggle_alignment): 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) + self.main_ctrl.start_worker(self.worker_name, self.worker_config, self.view) def _start_worker(self): self.running = True diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index c539e5371..ac1dd8002 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -87,6 +87,8 @@ def handle_add_worker(self): # User clicked save if return_value == 1: worker_name = create_worker_dialog.worker_name + self.main_ctrl.create_worker(worker_name) + self.config.add_worker_config(worker_name, create_worker_dialog.worker_data) self.add_worker_widget(worker_name) diff --git a/dexbot/worker.py b/dexbot/worker.py index 6d648fcf4..545f2f38d 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -227,6 +227,10 @@ def remove_offline_worker(config, worker_name): strategy = BaseStrategy(worker_name, config) strategy.purge() + @staticmethod + def remove_offline_worker_data_only(worker_name): + BaseStrategy.purge_database_only(worker_name) + def do_next_tick(self, job): """ Add a callable to be executed on the next tick """ self.jobs.add(job) From 8816debd37622ced6248385200ee932c5d6e11e7 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 15 Jun 2018 10:32:24 +0300 Subject: [PATCH 0409/1846] Change operation type in orders log from sell/buy to trade --- dexbot/basestrategy.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 206c7c542..6fb1825d7 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -598,22 +598,14 @@ def truncate(number, decimals): return math.floor(number * 10 ** decimals) / 10 ** decimals def write_order_log(self, order): - # ID; - # operation_type; - # base_asset; - # base_amount; - # quote_asset; - # quote_amount; - # timestamp + operation_type = 'TRADE' if order['base']['symbol'] == self.market['base']['symbol']: - operation_type = 'BUY' base_symbol = order['base']['symbol'] base_amount = -order['base']['amount'] quote_symbol = order['quote']['symbol'] quote_amount = order['quote']['amount'] else: - operation_type = 'SELL' base_symbol = order['quote']['symbol'] base_amount = order['quote']['amount'] quote_symbol = order['base']['symbol'] From 5dd1bfc32d042d5dcb054189a71e60cac4876cf5 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 15 Jun 2018 11:56:38 +0300 Subject: [PATCH 0410/1846] Add worker name field to orders log --- dexbot/basestrategy.py | 5 +++-- dexbot/helper.py | 2 +- dexbot/strategies/relative_orders.py | 2 +- dexbot/strategies/staggered_orders.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 6fb1825d7..9b5248536 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -597,7 +597,7 @@ def truncate(number, decimals): """ return math.floor(number * 10 ** decimals) / 10 ** decimals - def write_order_log(self, order): + def write_order_log(self, worker_name, order): operation_type = 'TRADE' if order['base']['symbol'] == self.market['base']['symbol']: @@ -611,7 +611,8 @@ def write_order_log(self, order): quote_symbol = order['base']['symbol'] quote_amount = -order['base']['amount'] - message = '{};{};{};{};{};{};{}'.format( + message = '{};{};{};{};{};{};{};{}'.format( + worker_name, order['id'], operation_type, base_symbol, diff --git a/dexbot/helper.py b/dexbot/helper.py index 785338554..4f5ce9595 100644 --- a/dexbot/helper.py +++ b/dexbot/helper.py @@ -49,4 +49,4 @@ def initialize_orders_log(): logger.setLevel(logging.INFO) if not file: - logger.info("ID;operation_type;base_asset;base_amount;quote_asset;quote_amount;timestamp") + logger.info("worker_name;ID;operation_type;base_asset;base_amount;quote_asset;quote_amount;timestamp") diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 2a6f76b8e..4a8b47edd 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -155,7 +155,7 @@ def check_orders(self, *args, **kwargs): if not current_bitshares_order: if not order_check_flag: order_check_flag = True - self.write_order_log(order) + self.write_order_log(self.worker_name, order) if order_check_flag: self.update_orders() diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index ae9fa291e..a778aab2b 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -191,7 +191,7 @@ def check_orders(self, *args, **kwargs): current_order = self.get_order(order_id) if not current_order: # Write order to .csv log - self.write_order_log(order) + self.write_order_log(self.worker_name, order) self.place_reverse_order(order) order_placed = True From 03b3671d9c7a8b8b433163867a995f80c2fb91e6 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 15 Jun 2018 12:14:45 +0300 Subject: [PATCH 0411/1846] Remove comments and rename variables --- dexbot/controllers/main_controller.py | 4 ++-- dexbot/helper.py | 2 +- dexbot/strategies/relative_orders.py | 12 ++---------- dexbot/worker.py | 2 +- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 4b68b390f..f9a005999 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -1,7 +1,7 @@ import logging import sys -from dexbot import VERSION, helper +from dexbot import VERSION, helper #### from dexbot.worker import WorkerInfrastructure from dexbot.views.errors import PyQtHandler @@ -67,5 +67,5 @@ def remove_worker(self, worker_name): @staticmethod def create_worker(worker_name): # Deletes old worker's data - WorkerInfrastructure.remove_offline_worker_data_only(worker_name) + WorkerInfrastructure.remove_offline_worker_data(worker_name) diff --git a/dexbot/helper.py b/dexbot/helper.py index 4f5ce9595..ccac91219 100644 --- a/dexbot/helper.py +++ b/dexbot/helper.py @@ -36,7 +36,7 @@ def initialize_orders_log(): """ Creates .csv log file, adds the headers first time only """ data_dir = user_data_dir(APP_NAME, AUTHOR) - filename = data_dir + '/orders.csv' + filename = os.path.join(data_dir, 'orders.csv') file = os.path.isfile(filename) formatter = logging.Formatter('%(message)s') diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 4a8b47edd..3cb136fcc 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -106,10 +106,6 @@ def update_orders(self): self.cancel_all() self.clear_orders() - # Mark the orders empty - # self['buy_order'] = {} - # self['sell_order'] = {} - order_ids = [] amount_base = self.amount_base @@ -118,20 +114,17 @@ def update_orders(self): # Buy Side buy_order = self.market_buy(amount_base, self.buy_price, True) if buy_order: - # self['buy_order'] = sell_order self.save_order(buy_order) order_ids.append(buy_order['id']) # Sell Side sell_order = self.market_sell(amount_quote, self.sell_price, True) if sell_order: - # self['sell_order'] = sell_order self.save_order(sell_order) order_ids.append(sell_order['id']) self['order_ids'] = order_ids - # Logger here as well self.log.info("Done placing orders") # Some orders weren't successfully created, redo them @@ -149,10 +142,9 @@ def check_orders(self, *args, **kwargs): else: self.log.info("Orders correct on market") for order_id, order in orders.items(): - # Looks up order from BitShares - current_bitshares_order = self.get_order(order_id) + current_order = self.get_order(order_id) - if not current_bitshares_order: + if not current_order: if not order_check_flag: order_check_flag = True self.write_order_log(self.worker_name, order) diff --git a/dexbot/worker.py b/dexbot/worker.py index 545f2f38d..0de5c80fd 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -228,7 +228,7 @@ def remove_offline_worker(config, worker_name): strategy.purge() @staticmethod - def remove_offline_worker_data_only(worker_name): + def remove_offline_worker_data(worker_name): BaseStrategy.purge_database_only(worker_name) def do_next_tick(self, job): From 5dfcd546b448b2a8bda3fec719c7ea79a5020b5a Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 15 Jun 2018 12:50:12 +0300 Subject: [PATCH 0412/1846] Fix imports and add logging to CLI --- dexbot/cli.py | 4 ++++ dexbot/controllers/main_controller.py | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index e7df271fc..9c061c98f 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -6,6 +6,7 @@ import sys from dexbot.config import Config, DEFAULT_CONFIG_FILE +from dexbot.helper import initialize_orders_log from dexbot.ui import ( verbose, chain, @@ -31,6 +32,9 @@ format='%(asctime)s %(levelname)s %(message)s' ) +# Configure orders logging +initialize_orders_log() + @click.group() @click.option( diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index f9a005999..75d6d5534 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -1,7 +1,8 @@ import logging import sys -from dexbot import VERSION, helper #### +from dexbot import VERSION +from dexbot.helper import initialize_orders_log from dexbot.worker import WorkerInfrastructure from dexbot.views.errors import PyQtHandler @@ -31,7 +32,7 @@ def __init__(self, bitshares_instance, config): 'worker_name': 'NONE', 'account': 'NONE', 'market': 'NONE'}) # Configure orders logging - helper.initialize_orders_log() + initialize_orders_log() def set_info_handler(self, handler): self.pyqt_handler.set_info_handler(handler) From 6a749109ecf37814a042d4102e6ead3959537698 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 15 Jun 2018 14:14:18 +0300 Subject: [PATCH 0413/1846] Change logic in relative orders check_orders --- dexbot/basestrategy.py | 2 +- dexbot/strategies/relative_orders.py | 12 +++++++----- dexbot/worker.py | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 9b5248536..32862ced3 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -493,7 +493,7 @@ def purge(self): self.clear() @staticmethod - def purge_database_only(worker_name): + def purge_worker_data(worker_name): Storage.clear_worker_data(worker_name) @staticmethod diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 3cb136fcc..efb939834 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -135,22 +135,24 @@ def check_orders(self, *args, **kwargs): """ Tests if the orders need updating """ orders = self.fetch_orders() - order_check_flag = False if not orders: self.update_orders() else: - self.log.info("Orders correct on market") + orders_changed = False + + # Loop trough the orders and look for changes for order_id, order in orders.items(): current_order = self.get_order(order_id) if not current_order: - if not order_check_flag: - order_check_flag = True + orders_changed = True self.write_order_log(self.worker_name, order) - if order_check_flag: + if orders_changed: self.update_orders() + else: + self.log.info("Orders correct on market") if self.view: self.update_gui_profit() diff --git a/dexbot/worker.py b/dexbot/worker.py index 0de5c80fd..b2bccab1e 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -229,7 +229,7 @@ def remove_offline_worker(config, worker_name): @staticmethod def remove_offline_worker_data(worker_name): - BaseStrategy.purge_database_only(worker_name) + BaseStrategy.purge_worker_data(worker_name) def do_next_tick(self, job): """ Add a callable to be executed on the next tick """ From 7bc0e50a4b0c7fa2bb545416a98cd80b2c0623d5 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 15 Jun 2018 14:22:06 +0300 Subject: [PATCH 0414/1846] Change dexbot version number to 0.4.4 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index a16cb28f8..c93cd0563 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = "dexbot" -VERSION = '0.4.0' +VERSION = '0.4.4' AUTHOR = "codaone" __version__ = VERSION From 3dc37812cdb3ff582f4f4157d5959637ce20d088 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 15 Jun 2018 14:29:06 +0300 Subject: [PATCH 0415/1846] Modify strategy center price init --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 7fd4b179d..20c4b9c06 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -80,10 +80,10 @@ def init_strategy(self): self.clear_orders() # Dynamic / Manual center price - if self.worker['center_price_dynamic']: + if self.worker.get('center_price_dynamic', True): center_price = self.calculate_center_price() else: - center_price = self.worker['center_price'] + center_price = self.worker.get('center_price') amount = self.amount spread = self.spread From 237bf9817b52217f5c045ad87d47b859ddee8f9e Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 15 Jun 2018 14:40:57 +0300 Subject: [PATCH 0416/1846] Change dexbot version number to 0.4.5 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 402b1b402..bddc4043d 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.4.3' +VERSION = '0.4.5' AUTHOR = 'Codaone Oy' __version__ = VERSION From 9f9a945c2e6a2cb59a7bb305a9034dfbcdb71262 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 15 Jun 2018 15:19:01 +0300 Subject: [PATCH 0417/1846] Modify center price calculation logic --- dexbot/basestrategy.py | 50 +++++++++++++++------------- dexbot/strategies/relative_orders.py | 25 +++++++++----- 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index ab379cf02..2f45a31b1 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -181,7 +181,7 @@ def __init__( 'is_disabled': lambda: self.disabled} ) - def calculate_center_price(self, suppress_errors=False): + def _calculate_center_price(self, suppress_errors=False): ticker = self.market.ticker() highest_bid = ticker.get("highestBid") lowest_ask = ticker.get("lowestAsk") @@ -203,43 +203,47 @@ def calculate_center_price(self, suppress_errors=False): 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, manual_offset=0): + def calculate_center_price(self, center_price=None, + asset_offset=False, spread=None, order_ids=None, manual_offset=0): """ Calculate center price which shifts based on available funds """ 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 + calculated_center_price = self._calculate_center_price() else: # 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) + calculated_center_price = self._calculate_center_price(True) if not calculated_center_price: calculated_center_price = center_price - total_balance = self.total_balance(order_ids) - total = (total_balance['quote'] * calculated_center_price) + total_balance['base'] + if center_price: + calculated_center_price = center_price - if not total: # Prevent division by zero - balance = 0 - else: - # Returns a value between -1 and 1 - balance = (total_balance['base'] / total) * 2 - 1 - - if balance < 0: - # With less of base asset center price should be offset downward - offset_center_price = calculated_center_price / math.sqrt(1 + spread * (balance * -1)) - elif balance > 0: - # With more of base asset center price will be offset upwards - offset_center_price = calculated_center_price * math.sqrt(1 + spread * balance) - else: - offset_center_price = calculated_center_price + if asset_offset: + total_balance = self.total_balance(order_ids) + total = (total_balance['quote'] * calculated_center_price) + total_balance['base'] + + if not total: # Prevent division by zero + balance = 0 + else: + # Returns a value between -1 and 1 + balance = (total_balance['base'] / total) * 2 - 1 + + if balance < 0: + # With less of base asset center price should be offset downward + calculated_center_price = calculated_center_price / math.sqrt(1 + spread * (balance * -1)) + elif balance > 0: + # With more of base asset center price will be offset upwards + calculated_center_price = calculated_center_price * math.sqrt(1 + spread * balance) + else: + calculated_center_price = calculated_center_price # Calculate final_offset_price if manual center price offset is given if manual_offset: - offset_center_price = center_price + (center_price * manual_offset) + calculated_center_price = calculated_center_price + (calculated_center_price * manual_offset) - return offset_center_price + return calculated_center_price @property def orders(self): diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index fee20e99a..11ee04a3c 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -46,7 +46,8 @@ 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.is_asset_offset = self.worker.get('center_price_offset', False) + self.manual_offset = self.worker.get('manual_offset', 0) self.order_size = float(self.worker['amount']) self.spread = self.worker.get('spread') / 100 @@ -85,15 +86,21 @@ 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_offset_center_price( - self.spread, order_ids=self['order_ids']) - else: - self.center_price = self.calculate_center_price() + self.center_price = self.calculate_center_price( + None, + self.is_asset_offset, + self.spread, + self['order_ids'], + self.manual_offset + ) else: - if self.is_center_price_offset: - self.center_price = self.calculate_offset_center_price( - self.spread, self.center_price, self['order_ids']) + self.center_price = self.calculate_center_price( + self.center_price, + self.is_asset_offset, + self.spread, + self['order_ids'], + self.manual_offset + ) self.buy_price = self.center_price / math.sqrt(1 + self.spread) self.sell_price = self.center_price * math.sqrt(1 + self.spread) From d453e85359687d6ea0827e75821f510239cde211 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 15 Jun 2018 15:29:33 +0300 Subject: [PATCH 0418/1846] Fix manual offset calculation --- 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 09096eab6..d5eb07a91 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -47,7 +47,7 @@ def __init__(self, *args, **kwargs): self.is_relative_order_size = self.worker['amount_relative'] self.is_asset_offset = self.worker.get('center_price_offset', False) - self.manual_offset = self.worker.get('manual_offset', 0) + self.manual_offset = self.worker.get('manual_offset', 0) / 100 self.order_size = float(self.worker['amount']) self.spread = self.worker.get('spread') / 100 From bac12af7951a9945d394e9b77e652e3d9d9c427c Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 15 Jun 2018 15:29:59 +0300 Subject: [PATCH 0419/1846] Change dexbot version number to 0.4.6 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index bddc4043d..5b90af327 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.4.5' +VERSION = '0.4.6' AUTHOR = 'Codaone Oy' __version__ = VERSION From 012d1bc038bae02048ba865c5a0c3334de45eae4 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 18 Jun 2018 14:09:18 +0300 Subject: [PATCH 0420/1846] Change log file location to same folder as orders log --- dexbot/controllers/main_controller.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 75d6d5534..9712d9e3e 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -1,11 +1,15 @@ +import os import logging import sys from dexbot import VERSION +from dexbot import APP_NAME +from dexbot import AUTHOR from dexbot.helper import initialize_orders_log from dexbot.worker import WorkerInfrastructure from dexbot.views.errors import PyQtHandler +from appdirs import user_data_dir from bitshares.instance import set_shared_bitshares_instance @@ -18,10 +22,12 @@ def __init__(self, bitshares_instance, config): self.worker_manager = None # Configure logging + data_dir = user_data_dir(APP_NAME, AUTHOR) + filename = os.path.join(data_dir, 'dexbot.log') 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 = logging.FileHandler(filename) fh.setFormatter(formatter) logger.addHandler(fh) logger.setLevel(logging.INFO) From 2e8c029fc3c1248d1857754cacd727922aeb5208 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 19 Jun 2018 07:52:05 +0300 Subject: [PATCH 0421/1846] Change the name of orders.csv to history.csv --- dexbot/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/helper.py b/dexbot/helper.py index ccac91219..5bae94229 100644 --- a/dexbot/helper.py +++ b/dexbot/helper.py @@ -36,7 +36,7 @@ def initialize_orders_log(): """ Creates .csv log file, adds the headers first time only """ data_dir = user_data_dir(APP_NAME, AUTHOR) - filename = os.path.join(data_dir, 'orders.csv') + filename = os.path.join(data_dir, 'history.csv') file = os.path.isfile(filename) formatter = logging.Formatter('%(message)s') From 4947aee36d2fe015eea9c51cb7967225842e3cdc Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 19 Jun 2018 13:20:58 +0300 Subject: [PATCH 0422/1846] Fix duplicate private key error and wallet locked error --- dexbot/controllers/main_controller.py | 5 ++--- dexbot/views/worker_item.py | 5 +++-- dexbot/worker.py | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 75d6d5534..9cf7b2b85 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -59,14 +59,13 @@ def remove_worker(self, worker_name): else: # Worker not running config = self.config.get_worker_config(worker_name) - WorkerInfrastructure.remove_offline_worker(config, worker_name) + WorkerInfrastructure.remove_offline_worker(config, worker_name, self.bitshares_instance) else: # Worker manager not running config = self.config.get_worker_config(worker_name) - WorkerInfrastructure.remove_offline_worker(config, worker_name) + WorkerInfrastructure.remove_offline_worker(config, worker_name, self.bitshares_instance) @staticmethod def create_worker(worker_name): # Deletes old worker's data WorkerInfrastructure.remove_offline_worker_data(worker_name) - diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index d412b02a9..73d5afe92 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -3,11 +3,9 @@ from .ui.worker_item_widget_ui import Ui_widget from .confirmation import ConfirmationDialog from .edit_worker import EditWorkerView -from .errors import gui_error from dexbot.storage import db_worker from dexbot.controllers.worker_controller import WorkerController from dexbot.views.errors import gui_error -from dexbot.resources import icons_rc from PyQt5 import QtCore, QtWidgets @@ -141,7 +139,10 @@ def remove_widget_dialog(self): self.remove_widget() def remove_widget(self): + account = self.worker_config['workers'][self.worker_name]['account'] + self.main_ctrl.remove_worker(self.worker_name) + self.main_ctrl.bitshares_instance.wallet.removeAccount(account) self.view.remove_worker_widget(self.worker_name) self.main_ctrl.config.remove_worker_config(self.worker_name) self.deleteLater() diff --git a/dexbot/worker.py b/dexbot/worker.py index 7ca44a01a..15f2d011c 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -223,9 +223,8 @@ def remove_market(self, worker_name): self.markets.remove(market) @staticmethod - def remove_offline_worker(config, worker_name): + def remove_offline_worker(config, worker_name, bitshares_instance): # Initialize the base strategy to get control over the data - bitshares_instance = BitShares(config['node']) strategy = BaseStrategy(worker_name, config, bitshares_instance=bitshares_instance) strategy.purge() From 9a47386cb963fdce177c76aca6d4c0bbedfceb0e Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 20 Jun 2018 10:59:49 +0300 Subject: [PATCH 0423/1846] Fix CLI worker deletion --- dexbot/basestrategy.py | 2 +- dexbot/cli.py | 5 ++++- dexbot/cli_conf.py | 8 +++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 465f1ccdf..0b1c6e039 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -496,8 +496,8 @@ def calculate_order_data(self, order, amount, price): def purge(self): """ Clear all the worker data from the database and cancel all orders """ - self.cancel_all() self.clear_orders() + self.cancel_all() self.clear() @staticmethod diff --git a/dexbot/cli.py b/dexbot/cli.py index 9c061c98f..5e49f90aa 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -108,6 +108,9 @@ def run(ctx): @main.command() @click.pass_context +@configfile +@chain +@unlock def configure(ctx): """ Interactively configure dexbot """ @@ -117,7 +120,7 @@ def configure(ctx): os.system('systemctl --user stop dexbot') config = Config(path=ctx.obj['configfile']) - configure_dexbot(config) + configure_dexbot(config, ctx) config.save_config() click.echo("New configuration saved") diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 6215b8c91..cf91826d4 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -179,7 +179,7 @@ def configure_worker(d, worker): return worker -def configure_dexbot(config): +def configure_dexbot(config, ctx): d = get_whiptail() workers = config.get('workers', {}) if not workers: @@ -190,21 +190,23 @@ def configure_dexbot(config): break setup_systemd(d, config) else: + bitshares_instance = ctx.bitshares action = d.menu("You have an existing configuration.\nSelect an action:", [('NEW', 'Create a new worker'), ('DEL', 'Delete a worker'), ('EDIT', 'Edit a worker'), ('CONF', 'Redo general config')]) + if action == 'EDIT': worker_name = d.menu("Select worker to edit", [(i, i) for i in workers]) config['workers'][worker_name] = configure_worker(d, config['workers'][worker_name]) - bitshares_instance = BitShares(config['node']) + strategy = BaseStrategy(worker_name, bitshares_instance=bitshares_instance) strategy.purge() elif action == 'DEL': worker_name = d.menu("Select worker to delete", [(i, i) for i in workers]) del config['workers'][worker_name] - bitshares_instance = BitShares(config['node']) + strategy = BaseStrategy(worker_name, bitshares_instance=bitshares_instance) strategy.purge() elif action == 'NEW': From 0274ddde2b159ffb9ab13d61e4b6f4145e58ce09 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 21 Jun 2018 09:36:53 +0300 Subject: [PATCH 0424/1846] Change dexbot version number to 0.4.7 --- dexbot/__init__.py | 2 +- dexbot/controllers/main_controller.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 5b90af327..80ffb322d 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.4.6' +VERSION = '0.4.7' AUTHOR = 'Codaone Oy' __version__ = VERSION diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 9712d9e3e..446f20f6e 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -2,9 +2,7 @@ import logging import sys -from dexbot import VERSION -from dexbot import APP_NAME -from dexbot import AUTHOR +from dexbot import VERSION, APP_NAME, AUTHOR from dexbot.helper import initialize_orders_log from dexbot.worker import WorkerInfrastructure from dexbot.views.errors import PyQtHandler From d7a2fe21983fa7d487c605ef16d9d531cae98369 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 21 Jun 2018 10:14:57 +0300 Subject: [PATCH 0425/1846] Change dexbot version number to 0.4.8 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 5b90af327..67c103453 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.4.6' +VERSION = '0.4.8' AUTHOR = 'Codaone Oy' __version__ = VERSION From 88bab565ed1dc1a67b3cb25a6b0951b108a354cb Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Sun, 1 Jul 2018 21:48:07 +1000 Subject: [PATCH 0426/1846] asset names can have dots --- dexbot/basestrategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 0b1c6e039..243ddbbf9 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -102,7 +102,7 @@ def configure(cls): ConfigElement("account", "string", "", "BitShares account name for the bot to operate with", ""), ConfigElement("market", "string", "USD:BTS", "BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"", - "[A-Z]+[:\/][A-Z]+") + r"[A-Z\.]+[:\/][A-Z\.]+") ] def __init__( From cc6ebe0357212f26b586a4a1fad923e08ba94cf1 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Sun, 1 Jul 2018 18:42:50 +0300 Subject: [PATCH 0427/1846] Updated links to correct installations instructions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 23534351b..813a2d548 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ master: ## Installing and running the software -See instructions in the [Wiki](https://github.com/Codaone/DEXBot/wiki/Installing-and-Running) +See instructions in the [Wiki](https://github.com/Codaone/DEXBot/wiki) for [Linux](https://github.com/Codaone/DEXBot/wiki/Setup-Guide-for-Linux), and [Windows](https://github.com/Codaone/DEXBot/wiki/Setup-Guide-for-Windows). OSX users can try downloading the package or following the Linux guide. ## Contributing From e612e3138d2335085522c555f7d18b3c82ff1c99 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 17 Jul 2018 09:07:22 +0300 Subject: [PATCH 0428/1846] Change dexbot version number to 0.4.9 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 67c103453..fc72731a3 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.4.8' +VERSION = '0.4.9' AUTHOR = 'Codaone Oy' __version__ = VERSION From 237458ee862d28ee8514d7d27600e84adc68fe2d Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 17 Jul 2018 14:58:50 +0300 Subject: [PATCH 0429/1846] Add default list of nodes --- dexbot/config.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/dexbot/config.py b/dexbot/config.py index 2b8fca2f3..cc7cd0c07 100644 --- a/dexbot/config.py +++ b/dexbot/config.py @@ -35,6 +35,12 @@ def __init__(self, config=None, path=None): self.create_config(self.default_data, self.config_file) self._config = self.load_config(self.config_file) + # In case there is not a list of nodes in the config file, + # the node will be replaced by a list of pre-defined nodes. + if isinstance(self._config['node'], str): + self._config['node'] = self.node_list + self.save_config() + def __setitem__(self, key, value): self._config[key] = value @@ -52,7 +58,7 @@ def get(self, key, default=None): @property def default_data(self): - return {'node': 'wss://status200.bitshares.apasia.tech/ws', 'workers': {}} + return {'node': self.node_list, 'workers': {}} @property def workers_data(self): @@ -158,3 +164,24 @@ def construct_mapping(mapping_loader, node): yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, construct_mapping) return yaml.load(stream, OrderedLoader) + + @property + def node_list(self): + """ A pre-defined list of Bitshares nodes. """ + return [ + "wss://eu.openledger.info/ws", + "wss://bitshares.openledger.info/ws", + "wss://dexnode.net/ws", + "wss://japan.bitshares.apasia.tech/ws", + "wss://bitshares-api.wancloud.io/ws", + "wss://openledger.hk/ws", + "wss://bitshares.apasia.tech/ws", + "wss://bitshares.crypto.fans/ws", + "wss://kc-us-dex.xeldal.com/ws", + "wss://api.bts.blckchnd.com", + "wss://btsza.co.za:8091/ws", + "wss://bitshares.dacplay.org/ws", + "wss://bit.btsabc.org/ws", + "wss://bts.ai.la/ws", + "wss://ws.gdex.top" + ] From 4ba59cca7f4a5d38447d28fbd5e55d086f42bc4a Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Tue, 17 Jul 2018 15:25:14 +0300 Subject: [PATCH 0430/1846] Change setup.py layout and move around the dependencies --- requirements.txt | 4 +--- setup.py | 24 ++++++++---------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/requirements.txt b/requirements.txt index ed0bde7db..d3e5a7f68 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ pyqt5==5.10 pyqt-distutils==0.7.3 -click-datetime==0.2 -pyinstaller==3.3.1 -appdirs==1.4.3 \ No newline at end of file +click-datetime==0.2 \ No newline at end of file diff --git a/setup.py b/setup.py index fc6c6bc15..7cde58e77 100755 --- a/setup.py +++ b/setup.py @@ -1,17 +1,19 @@ #!/usr/bin/env python3 +from dexbot import VERSION, APP_NAME from setuptools import setup, find_packages from distutils.command import build as build_module -cmdclass = {} + +cmd_class = {} console_scripts = ['dexbot-cli = dexbot.cli:main'] install_requires = [ "bitshares==0.1.16", "uptick>=0.1.4", "click", "sqlalchemy", - "appdirs", + "ruamel.yaml>=0.15.37", "sdnotify", - "ruamel.yaml>=0.15.37" + "appdirs>=1.4.3" ] @@ -23,7 +25,7 @@ def run(self): try: from pyqt_distutils.build_ui import build_ui - cmdclass = { + cmd_class = { 'build_ui': build_ui, 'build': BuildCommand } @@ -32,8 +34,6 @@ def run(self): except BaseException as e: print("GUI not available: {}".format(e)) -from dexbot import VERSION, APP_NAME - setup( name=APP_NAME, @@ -54,18 +54,10 @@ def run(self): 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', ], - cmdclass=cmdclass, + cmdclass=cmd_class, entry_points={ 'console_scripts': console_scripts }, - install_requires=[ - "bitshares==0.1.16", - "uptick>=0.1.4", - "click", - "sqlalchemy", - "appdirs", - "ruamel.yaml>=0.15.37", - "sdnotify" - ], + install_requires=install_requires, include_package_data=True, ) From 430f1c064de3956545691766fc0e5e1309aa9e60 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 18 Jul 2018 13:54:41 +0500 Subject: [PATCH 0431/1846] Update dependency on python-bitshares Closes: #240 Time spent: 0.5h --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fc6c6bc15..60ba2e7cb 100755 --- a/setup.py +++ b/setup.py @@ -59,7 +59,7 @@ def run(self): 'console_scripts': console_scripts }, install_requires=[ - "bitshares==0.1.16", + "bitshares==0.1.18", "uptick>=0.1.4", "click", "sqlalchemy", From 1e519ae2ee8cf4de1b0f12cf67039c9c4f6d9e85 Mon Sep 17 00:00:00 2001 From: joelvai <10694041+joelvai@users.noreply.github.com> Date: Wed, 18 Jul 2018 12:24:52 +0300 Subject: [PATCH 0432/1846] Update requirements.txt --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d3e5a7f68..d15c84df3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ pyqt5==5.10 pyqt-distutils==0.7.3 -click-datetime==0.2 \ No newline at end of file +pyinstaller==3.3.1 +click-datetime==0.2 From 8b85606e6daaaa5423ccb3637cd2953b2abe9e0c Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 18 Jul 2018 12:48:56 +0300 Subject: [PATCH 0433/1846] Update version number --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index fc72731a3..2d68724b0 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.4.9' +VERSION = '0.4.10' AUTHOR = 'Codaone Oy' __version__ = VERSION From ce4b58cf48a17c4803f93cccaa49b2cdd86ace13 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 18 Jul 2018 13:16:17 +0300 Subject: [PATCH 0434/1846] Change dexbot version number to 0.4.11 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 2d68724b0..e2a7771f8 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.4.10' +VERSION = '0.4.11' AUTHOR = 'Codaone Oy' __version__ = VERSION From 6f788b14095b631082d0ed51cd981b60c16b12f4 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 20 Jul 2018 12:25:38 +0300 Subject: [PATCH 0435/1846] Update CLI configuration to use list of nodes --- dexbot/cli_conf.py | 74 +++++++++++++++++++++++++--------------------- dexbot/whiptail.py | 22 ++++++++++---- 2 files changed, 56 insertions(+), 40 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index cf91826d4..0585dd040 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -24,7 +24,6 @@ from dexbot.whiptail import get_whiptail from dexbot.basestrategy import BaseStrategy -from bitshares import BitShares # FIXME: auto-discovery of strategies would be cool but can't figure out a way STRATEGIES = [ @@ -61,26 +60,26 @@ def select_choice(current, choices): for tag, text in choices] -def process_config_element(elem, d, config): +def process_config_element(elem, whiptail, config): """ Process an item of configuration metadata display a widget as appropriate d: the Dialog object config: the config dictionary for this worker """ if elem.type == "string": - txt = d.prompt(elem.description, config.get(elem.key, elem.default)) + txt = whiptail.prompt(elem.description, config.get(elem.key, elem.default)) if elem.extra: while not re.match(elem.extra, txt): - d.alert("The value is not valid") - txt = d.prompt( + whiptail.alert("The value is not valid") + txt = whiptail.prompt( elem.description, config.get( elem.key, elem.default)) config[elem.key] = txt if elem.type == "bool": value = config.get(elem.key, elem.default) value = 'yes' if value else 'no' - config[elem.key] = d.confirm(elem.description, value) + config[elem.key] = whiptail.confirm(elem.description, value) if elem.type in ("float", "int"): - txt = d.prompt(elem.description, str(config.get(elem.key, elem.default))) + txt = whiptail.prompt(elem.description, str(config.get(elem.key, elem.default))) while True: try: if elem.type == "int": @@ -88,17 +87,17 @@ def process_config_element(elem, d, config): else: val = float(txt) if val < elem.extra[0]: - d.alert("The value is too low") + whiptail.alert("The value is too low") elif elem.extra[1] and val > elem.extra[1]: - d.alert("the value is too high") + whiptail.alert("the value is too high") else: break except ValueError: - d.alert("Not a valid value") - txt = d.prompt(elem.description, str(config.get(elem.key, elem.default))) + whiptail.alert("Not a valid value") + txt = whiptail.prompt(elem.description, str(config.get(elem.key, elem.default))) config[elem.key] = val if elem.type == "choice": - config[elem.key] = d.radiolist(elem.description, select_choice( + config[elem.key] = whiptail.radiolist(elem.description, select_choice( config.get(elem.key, elem.default), elem.extra)) @@ -113,24 +112,24 @@ def dexbot_service_running(): return False -def setup_systemd(d, config): +def setup_systemd(whiptail, config): if not os.path.exists("/etc/systemd"): return # No working systemd - if not d.confirm( + if not whiptail.confirm( "Do you want to run dexbot as a background (daemon) process?"): config['systemd_status'] = 'disabled' return redo_setup = False if os.path.exists(SYSTEMD_SERVICE_NAME): - redo_setup = d.confirm('Redo systemd setup?', 'no') + redo_setup = whiptail.confirm('Redo systemd setup?', 'no') if not os.path.exists(SYSTEMD_SERVICE_NAME) or redo_setup: path = '~/.local/share/systemd/user' path = os.path.expanduser(path) pathlib.Path(path).mkdir(parents=True, exist_ok=True) - password = d.prompt( + password = whiptail.prompt( "The wallet password\n" "NOTE: this will be saved on disc so the worker can run unattended. " "This means anyone with access to this computer's files can spend all your money", @@ -151,13 +150,13 @@ def setup_systemd(d, config): config['systemd_status'] = 'enabled' -def configure_worker(d, worker): +def configure_worker(whiptail, worker): default_strategy = worker.get('module', 'dexbot.strategies.relative_orders') for i in STRATEGIES: if default_strategy == i['class']: default_strategy = i['tag'] - worker['module'] = d.radiolist( + worker['module'] = whiptail.radiolist( "Choose a worker strategy", select_choice( default_strategy, [(i['tag'], i['name']) for i in STRATEGIES])) for i in STRATEGIES: @@ -172,48 +171,55 @@ def configure_worker(d, worker): configs = strategy_class.configure() if configs: for c in configs: - process_config_element(c, d, worker) + process_config_element(c, whiptail, worker) else: - d.alert("This worker type does not have configuration information. " + whiptail.alert("This worker type does not have configuration information. " "You will have to check the worker code and add configuration values to config.yml if required") return worker def configure_dexbot(config, ctx): - d = get_whiptail() + whiptail = get_whiptail() workers = config.get('workers', {}) if not workers: while True: - txt = d.prompt("Your name for the worker") - config['workers'] = {txt: configure_worker(d, {})} - if not d.confirm("Set up another worker?\n(DEXBot can run multiple workers in one instance)"): + txt = whiptail.prompt("Your name for the worker") + config['workers'] = {txt: configure_worker(whiptail, {})} + if not whiptail.confirm("Set up another worker?\n(DEXBot can run multiple workers in one instance)"): break - setup_systemd(d, config) + setup_systemd(whiptail, config) else: bitshares_instance = ctx.bitshares - action = d.menu("You have an existing configuration.\nSelect an action:", + action = whiptail.menu("You have an existing configuration.\nSelect an action:", [('NEW', 'Create a new worker'), ('DEL', 'Delete a worker'), ('EDIT', 'Edit a worker'), ('CONF', 'Redo general config')]) if action == 'EDIT': - worker_name = d.menu("Select worker to edit", [(i, i) for i in workers]) - config['workers'][worker_name] = configure_worker(d, config['workers'][worker_name]) + worker_name = whiptail.menu("Select worker to edit", [(i, i) for i in workers]) + config['workers'][worker_name] = configure_worker(whiptail, config['workers'][worker_name]) strategy = BaseStrategy(worker_name, bitshares_instance=bitshares_instance) strategy.purge() elif action == 'DEL': - worker_name = d.menu("Select worker to delete", [(i, i) for i in workers]) + worker_name = whiptail.menu("Select worker to delete", [(i, i) for i in workers]) del config['workers'][worker_name] strategy = BaseStrategy(worker_name, bitshares_instance=bitshares_instance) strategy.purge() elif action == 'NEW': - txt = d.prompt("Your name for the new worker") - config['workers'][txt] = configure_worker(d, {}) + txt = whiptail.prompt("Your name for the new worker") + config['workers'][txt] = configure_worker(whiptail, {}) elif action == 'CONF': - config['node'] = d.prompt("BitShares node to use", default=config['node']) - setup_systemd(d, config) - d.clear() + choice = whiptail.node_radiolist( + msg="Choose node", + items=select_choice(config['node'][0], [(i, i) for i in config['node']]) + ) + # Move selected node as first item in the config file's node list + config['node'].remove(choice) + config['node'].insert(0, choice) + + setup_systemd(whiptail, config) + whiptail.clear() return config diff --git a/dexbot/whiptail.py b/dexbot/whiptail.py index 70e29c895..181b15b28 100644 --- a/dexbot/whiptail.py +++ b/dexbot/whiptail.py @@ -73,15 +73,26 @@ def menu(self, msg='', items=(), prefix=' - '): def showlist(self, control, msg, items, prefix): if isinstance(items[0], str): - items = [(i, '', 'OFF') for i in items] + items = [(tag, '', 'OFF') for tag in items] else: - items = [(k, prefix + v, s) for k, v, s in items] + items = [(tag, prefix + value, state) for tag, value, state in items] + extra = self.calc_height(msg) + flatten(items) + return shlex.split(self.run(control, msg, extra).value) + + def show_tag_only_list(self, control, msg, items, prefix): + if isinstance(items[0], str): + items = [(tag, '', 'OFF') for tag in items] + else: + items = [(tag, '', state) for tag, value, state in items] extra = self.calc_height(msg) + flatten(items) return shlex.split(self.run(control, msg, extra).value) def radiolist(self, msg='', items=(), prefix=' - '): return self.showlist('radiolist', msg, items, prefix)[0] + def node_radiolist(self, msg='', items=(), prefix=''): + return self.show_tag_only_list('radiolist', msg, items, prefix)[0] + def checklist(self, msg='', items=(), prefix=' - '): return self.showlist('checklist', msg, items, prefix) @@ -149,9 +160,8 @@ def clear(self): pass # Don't tidy the screen -def get_whiptail(): +def get_whiptail(title=''): if shutil.which("whiptail"): - d = Whiptail() + return Whiptail(title=title) else: - d = NoWhiptail() # Use our own fake whiptail - return d + return NoWhiptail() # Use our own fake whiptail From e0e4dccebc80ff079cc1091cd2c5ef9725325a3d Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 24 Jul 2018 09:47:49 +0300 Subject: [PATCH 0436/1846] Refactor code comments --- dexbot/views/create_worker.py | 2 +- dexbot/views/edit_worker.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/views/create_worker.py b/dexbot/views/create_worker.py index 39d488991..64763d341 100644 --- a/dexbot/views/create_worker.py +++ b/dexbot/views/create_worker.py @@ -17,7 +17,7 @@ def __init__(self, bitshares_instance): validator = UppercaseValidator(self) # Todo: Using a model here would be more Qt like - # Populate the comboboxes + # Populate the combobox strategies = self.controller.strategies for strategy in strategies: self.strategy_input.addItem(strategies[strategy]['name'], strategy) diff --git a/dexbot/views/edit_worker.py b/dexbot/views/edit_worker.py index c48b442c5..6c8be0dd6 100644 --- a/dexbot/views/edit_worker.py +++ b/dexbot/views/edit_worker.py @@ -20,7 +20,7 @@ def __init__(self, parent_widget, bitshares_instance, worker_name, config): validator = UppercaseValidator(self) # Todo: Using a model here would be more Qt like - # Populate the comboboxes + # Populate the combobox strategies = self.controller.strategies for strategy in strategies: self.strategy_input.addItem(strategies[strategy]['name'], strategy) From 3a211d2f48bcc5d12b257d5d5b32128470929867 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 24 Jul 2018 14:28:15 +0300 Subject: [PATCH 0437/1846] Add methods for getting buy and sell orders --- dexbot/basestrategy.py | 58 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 243ddbbf9..f672f6170 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -257,6 +257,64 @@ 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_buy_orders(self, sort=None, orders=None): + """ Return buy orders + :param str sort: DESC or ASC will sort the orders accordingly, default None. + :param list orders: List of orders. If None given get all orders from Blockchain. + :return list buy_orders: List of buy orders only. + """ + buy_orders = [] + + if not orders: + orders = self.orders + + # Find buy orders + for order in orders: + if order['base']['symbol'] == self.market['base']['symbol']: + buy_orders.append(order) + if sort: + buy_orders = self.sort_orders(buy_orders, sort) + + return buy_orders + + def get_sell_orders(self, sort=None, orders=None): + """ Return sell orders + :param str sort: DESC or ASC will sort the orders accordingly, default None. + :param list orders: List of orders. If None given get all orders from Blockchain. + :return list sell_orders: List of sell orders only. + """ + sell_orders = [] + + if not orders: + orders = self.orders + + # Find sell orders + for order in orders: + if order['base']['symbol'] != self.market['base']['symbol']: + sell_orders.append(order) + + if sort: + sell_orders = self.sort_orders(sell_orders, sort) + + return sell_orders + + @staticmethod + def sort_orders(orders, sort='DESC'): + """ Return list of orders sorted ascending or descending + :param list orders: list of orders to be sorted + :param str sort: ASC or DESC. Default DESC + :return list: Sorted list of orders. + """ + if sort.upper() == 'ASC': + reverse = False + elif sort.upper() == 'DESC': + reverse = True + else: + return None + + # Sort orders by price + return sorted(orders, key=lambda order: order['price'], reverse=reverse) + @staticmethod def get_order(order_id, return_none=True): """ Returns the Order object for the order_id From 23dab082b85b63f6249f74e6a206a7db49cb496f Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 24 Jul 2018 14:44:38 +0300 Subject: [PATCH 0438/1846] Add mode field to Staggered Orders --- dexbot/controllers/strategy_controller.py | 85 ++++----- dexbot/controllers/worker_controller.py | 4 + .../views/ui/forms/staggered_orders_widget.ui | 169 ++++-------------- 3 files changed, 74 insertions(+), 184 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 1e151e7e5..ad00d964b 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -1,9 +1,6 @@ -from dexbot.qt_queue.idle_queue import idle_add -from dexbot.views.errors import gui_error -from dexbot.strategies.staggered_orders import Strategy as StaggeredOrdersStrategy +import collections -from bitshares.market import Market -from bitshares.asset import AssetDoesNotExistsException +from dexbot.views.errors import gui_error class RelativeOrdersController: @@ -93,23 +90,22 @@ def __init__(self, view, worker_controller, worker_data): self.view = view self.worker_controller = worker_controller + if view: + modes = self.strategy_modes + for strategy_mode in modes: + self.view.strategy_widget.mode_input.addItem(modes[strategy_mode], strategy_mode) + 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)) + + # Set strategy mode + index = widget.mode_input.findData(self.worker_controller.get_strategy_mode(worker_data)) + widget.mode_input.setCurrentIndex(index) + 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)) @@ -123,41 +119,6 @@ def set_config_values(self, worker_data): self.view.strategy_widget.center_price_dynamic_checkbox.setChecked(False) self.view.strategy_widget.center_price_input.setDisabled(False) - @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) @@ -166,8 +127,6 @@ def set_required_quote(self, 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(): @@ -179,7 +138,7 @@ def validation_errors(self): @property def values(self): data = { - 'amount': self.view.strategy_widget.amount_input.value(), + 'mode': self.view.strategy_widget.mode_input.currentData(), 'spread': self.view.strategy_widget.spread_input.value(), 'center_price': self.view.strategy_widget.center_price_input.value(), 'center_price_dynamic': self.view.strategy_widget.center_price_dynamic_checkbox.isChecked(), @@ -188,3 +147,21 @@ def values(self): 'upper_bound': self.view.strategy_widget.upper_bound_input.value() } return data + + @property + def strategy_modes(self): + # Todo: Activate rest of the modes once the logic is done + modes = collections.OrderedDict() + + # modes['neutral'] = 'Neutral' + modes['mountain'] = 'Mountain' + # modes['valley'] = 'Valley' + # modes['buy_slope'] = 'Buy Slope' + # modes['sell_slope'] = 'Sell Slope' + + return modes + + @classmethod + def strategy_modes_tuples(cls): + modes = cls(None, None, None).strategy_modes + return [(key, value) for key, value in modes.items()] diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index e5e0db42c..2b0429933 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -125,6 +125,10 @@ def get_strategy_name(self, module): def get_strategy_module(worker_data): return worker_data['module'] + @staticmethod + def get_strategy_mode(worker_data): + return worker_data['mode'] + @staticmethod def get_assets(worker_data): return re.split("[/:]", worker_data['market']) diff --git a/dexbot/views/ui/forms/staggered_orders_widget.ui b/dexbot/views/ui/forms/staggered_orders_widget.ui index 2eda84934..fc7202e14 100644 --- a/dexbot/views/ui/forms/staggered_orders_widget.ui +++ b/dexbot/views/ui/forms/staggered_orders_widget.ui @@ -6,8 +6,8 @@ 0 0 - 382 - 350 + 364 + 272 @@ -33,35 +33,14 @@ - - - - 0 - 0 - - - - - 110 - 0 - - - - - 110 - 16777215 - - + - Amount - - - spread_input + Mode - + 0 @@ -74,21 +53,6 @@ 0 - - - - - - - - 8 - - - 100000.000000000000000 - - - 0.000000000000000 - @@ -259,62 +223,6 @@ - - - - - 0 - 0 - - - - - 110 - 0 - - - - - 110 - 16777215 - - - - Upper bound - - - spread_input - - - - - - - - 0 - 0 - - - - - 140 - 0 - - - - - - - 8 - - - 1000000000.000000000000000 - - - 1000000.000000000000000 - - - @@ -390,20 +298,14 @@ - - - - - - - Worker info - - - - 3 - - - + + + + + 0 + 0 + + 110 @@ -417,31 +319,38 @@ - Required quote + Upper bound - - true + + spread_input - - - - N/A + + + + + 0 + 0 + - - - - - - Required base + + + 140 + 0 + - - - - - - N/A + + + + + 8 + + + 1000000000.000000000000000 + + + 1000000.000000000000000 From 258f8a7488fb0e69ce0fe6b79357708c4acf5fed Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 24 Jul 2018 14:51:22 +0300 Subject: [PATCH 0439/1846] Change Staggered Orders logic, WIP --- dexbot/strategies/staggered_orders.py | 310 +++++--------------------- 1 file changed, 59 insertions(+), 251 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 9508b9cc2..a75019889 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1,21 +1,19 @@ -import math from datetime import datetime -from datetime import timedelta from dexbot.basestrategy import BaseStrategy, ConfigElement +from dexbot.controllers.strategy_controller import StaggeredOrdersController from dexbot.qt_queue.idle_queue import idle_add class Strategy(BaseStrategy): - """ Staggered Orders strategy - """ + """ Staggered Orders strategy """ @classmethod def configure(cls): return BaseStrategy.configure() + [ ConfigElement( - 'amount', 'float', 1.0, - 'The amount of buy/sell orders', (0.0, None)), + 'strategy_mode', 'choice', 'mountain', + 'Choose strategy mode', StaggeredOrdersController.strategy_modes_tuples()), ConfigElement( 'center_price_dynamic', 'bool', True, 'Dynamic centre price', None), @@ -38,282 +36,90 @@ def configure(cls): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.log.info("Initializing Staggered Orders") - # Define Callbacks - self.onMarketUpdate += self.on_market_update_wrapper - self.onAccount += self.check_orders - self.ontick += self.tick + # Tick counter + self.counter = 0 + # Define callbacks + self.onMarketUpdate += self.maintain_strategy() + self.onAccount += self.maintain_strategy() + self.ontick += self.tick self.error_ontick = self.error self.error_onMarketUpdate = self.error self.error_onAccount = self.error + # Worker parameters self.worker_name = kwargs.get('name') self.view = kwargs.get('view') - self.amount = self.worker['amount'] + self.mode = self.worker['mode'] 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'] - # Order expiration time, should be high enough - self.expiration = 60*60*24*365*5 - self.last_check = datetime.now() - - if self['setup_done']: - self.check_orders() - else: - self.init_strategy() - - self.log.info('Done initializing Staggered Orders') - - if self.view: - self.update_gui_profit() - self.update_gui_slider() - - def error(self, *args, **kwargs): - self.disabled = True - - def init_strategy(self): - # Make sure no orders remain - self.cancel_all() - self.clear_orders() - - # Dynamic / Manual center price - if self.worker.get('center_price_dynamic', True): - center_price = self.calculate_center_price() - else: - center_price = self.worker.get('center_price') - - amount = self.amount - spread = self.spread - increment = self.increment - lower_bound = self.lower_bound - upper_bound = self.upper_bound - - # Calculate buy prices - buy_prices = self.calculate_buy_prices(center_price, spread, increment, lower_bound) - - # Calculate sell prices - sell_prices = self.calculate_sell_prices(center_price, spread, increment, upper_bound) - - # 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 - 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'], expiration=self.expiration) - 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'], expiration=self.expiration) - if order: - self.save_order(order) - - self['setup_done'] = True - self.log.info("Done placing orders") - - def pause(self, *args, **kwargs): - """ Override pause() method because we don't want to remove orders - """ - self.log.info("Stopping and leaving orders on the market") - - def place_reverse_order(self, order): - """ Replaces an order with a reverse order - buy orders become sell orders and sell orders become buy orders - """ - if order['base']['symbol'] == self.market['base']['symbol']: # Buy order - price = order['price'] * (1 + self.spread) - amount = order['quote']['amount'] - new_order = self.market_sell(amount, price, expiration=self.expiration) - else: # Sell order - price = (order['price'] ** -1) / (1 + self.spread) - amount = order['base']['amount'] - new_order = self.market_buy(amount, price, expiration=self.expiration) - 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'] - new_order = self.market_buy(amount, price, expiration=self.expiration) - else: # Sell order - price = order['price'] ** -1 - amount = order['base']['amount'] - new_order = self.market_sell(amount, price, expiration=self.expiration) - - self.save_order(new_order) - - def place_orders(self): - """ Place all the orders found in the database - FIXME: unused method - """ - orders = self.fetch_orders() - for order_id, order in orders.items(): - if not self.get_order(order_id): - self.place_order(order) - - self.log.info("Done placing orders") - - def on_market_update_wrapper(self, *args, **kwargs): - """ Handle market update callbacks - """ - delta = datetime.now() - self.last_check - - # Only allow to check orders whether minimal time passed - if delta > timedelta(seconds=5): - self.check_orders(*args, **kwargs) - - 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: - # Write order to .csv log - self.write_order_log(self.worker_name, order) - self.place_reverse_order(order) - order_placed = True - - if order_placed: - self.log.info("Done placing orders") + # Order expiration time + self.expiration = 60 * 60 * 24 * 365 * 5 + self.last_check = datetime.now() if self.view: - self.update_gui_profit() self.update_gui_slider() - self.last_check = datetime.now() + def maintain_strategy(self): + """ Logic of the strategy """ + # Get orders + orders = self.orders - @staticmethod - def calculate_buy_prices(center_price, spread, increment, lower_bound): - buy_prices = [] - if lower_bound > center_price / math.sqrt(1 + increment + spread): - return buy_prices + # Calculate market center price + market_center_price = self.calculate_center_price() - buy_price = center_price / math.sqrt(1 + increment + spread) - while buy_price > lower_bound: - buy_prices.append(buy_price) - buy_price = buy_price / (1 + increment) - return buy_prices + # Get sorted orders + buy_orders = self.get_buy_orders('DESC', orders) + sell_orders = self.get_sell_orders('DESC', orders) - @staticmethod - def calculate_sell_prices(center_price, spread, increment, upper_bound): - sell_prices = [] - if upper_bound < center_price * math.sqrt(1 + increment + spread): - return sell_prices + # Highest buy and lowest sell prices + highest_buy_price = buy_orders[0] + lowest_sell_price = sell_orders[-1] - sell_price = center_price * math.sqrt(1 + increment + spread) - while sell_price < upper_bound: - sell_prices.append(sell_price) - sell_price = sell_price * (1 + increment) - return sell_prices + # Get account balances + base_asset_balance = self.balance(self.market['base']['symbol']) + quote_asset_balance = self.balance(self.market['quote']['symbol']) - @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 asset thresholds + base_asset_threshold = base_asset_balance / 20000 + quote_asset_threshold = quote_asset_balance / 20000 - # Calculate sell amounts - sell_orders = [] - if sell_prices: - lowest_sell_price = sell_prices.pop(0) - 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) - sell_orders.append({'amount': current_amount, 'price': sell_price}) + # Check boundaries + if market_center_price > self.upper_bound: + self.upper_bound = market_center_price + elif market_center_price < self.lower_bound: + self.lower_bound = market_center_price - return [buy_orders, sell_orders] - - @staticmethod - def get_required_assets(market, amount, spread, increment, lower_bound, upper_bound): - if not amount or 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 + # Base asset check + # Todo: Check the logic + if base_asset_balance > base_asset_threshold: + self.allocate_base() else: - 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) - - # 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'] * buy_order['price'] + if market_center_price > highest_buy_price * (1 + self.spread): + self.shift_orders_up() - needed_sell_asset = 0 - for sell_order in sell_orders: - needed_sell_asset += sell_order['amount'] + # Check which mode is in use + if self.mode == 'mountain': + self.maintain_mountain_mode() - return [needed_buy_asset, needed_sell_asset] + def maintain_mountain_mode(self): + """ Mountain mode """ + pass 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 + """ Ticks come in on every block """ + if not (self.counter or 0) % 5: + self.maintain_strategy() + self.counter += 1 def update_gui_slider(self): ticker = self.market.ticker() latest_price = ticker.get('latest', {}).get('price', None) + if not latest_price: return @@ -322,12 +128,14 @@ def update_gui_slider(self): order_ids = orders.keys() else: order_ids = None + total_balance = self.total_balance(order_ids) total = (total_balance['quote'] * latest_price) + total_balance['base'] - if not total: # Prevent division by zero + # Prevent division by zero + if not total: percentage = 50 else: percentage = (total_balance['base'] / total) * 100 + idle_add(self.view.set_worker_slider, self.worker_name, percentage) - self['slider'] = percentage From 9e4c69b04a1721e75bf07d151a5a3493d34adf3c Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 25 Jul 2018 10:31:08 +0300 Subject: [PATCH 0440/1846] Add orders instant fill field --- dexbot/controllers/strategy_controller.py | 6 +++++- dexbot/controllers/worker_controller.py | 4 ++++ dexbot/views/ui/forms/staggered_orders_widget.ui | 15 ++++++++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index ad00d964b..8fc1d384f 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -119,6 +119,9 @@ def set_config_values(self, worker_data): self.view.strategy_widget.center_price_dynamic_checkbox.setChecked(False) self.view.strategy_widget.center_price_input.setDisabled(False) + # Set allow instant order fill + self.view.strategy_widget.allow_instant_fill_checkbox.setChecked(worker_data.get('allow_instant_fill', True)) + def set_required_base(self, text): self.view.strategy_widget.required_base_text.setText(text) @@ -144,7 +147,8 @@ def values(self): 'center_price_dynamic': self.view.strategy_widget.center_price_dynamic_checkbox.isChecked(), '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() + 'upper_bound': self.view.strategy_widget.upper_bound_input.value(), + 'allow_instant_fill': self.view.strategy_widget.allow_instant_fill_checkbox.isChecked() } return data diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index 2b0429933..721dcf2b6 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -129,6 +129,10 @@ def get_strategy_module(worker_data): def get_strategy_mode(worker_data): return worker_data['mode'] + @staticmethod + def get_allow_instant_fill(worder_data): + return worder_data['allow_instant_fill'] + @staticmethod def get_assets(worker_data): return re.split("[/:]", worker_data['market']) diff --git a/dexbot/views/ui/forms/staggered_orders_widget.ui b/dexbot/views/ui/forms/staggered_orders_widget.ui index fc7202e14..b4a9aa82c 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 364 - 272 + 300 @@ -37,6 +37,9 @@ Mode + + mode_input + @@ -354,6 +357,16 @@ + + + + Allow order instant fill + + + true + + + From a2b41dfb20293337fac67ff534677404fd61796e Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 25 Jul 2018 10:35:22 +0300 Subject: [PATCH 0441/1846] Clean up strategies --- dexbot/strategies/echo.py | 21 +++++++++------------ dexbot/strategies/storagedemo.py | 17 ----------------- dexbot/strategies/walls.py | 11 ++++------- 3 files changed, 13 insertions(+), 36 deletions(-) delete mode 100644 dexbot/strategies/storagedemo.py diff --git a/dexbot/strategies/echo.py b/dexbot/strategies/echo.py index 87c685c38..c7a732aaa 100644 --- a/dexbot/strategies/echo.py +++ b/dexbot/strategies/echo.py @@ -2,16 +2,14 @@ class Strategy(BaseStrategy): - """ - Echo strategy - Strategy that logs all events within the blockchain + """ Echo strategy + Strategy that logs all events within the blockchain """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - """ set call backs for events - """ + # Set call backs for events self.onOrderMatched += self.print_orderMatched self.onOrderPlaced += self.print_orderPlaced self.onUpdateCallOrder += self.print_UpdateCallOrder @@ -36,7 +34,7 @@ def print_orderMatched(self, i): :param bitshares.price.FilledOrder i: Filled order details """ - self.log.info("order matched: %s" % i) + self.log.info("Order matched: {}".format(i)) def print_orderPlaced(self, i): """ Is called when a new order in the market is placed @@ -46,7 +44,7 @@ def print_orderPlaced(self, i): :param bitshares.price.Order i: Order details """ - self.log.info("order placed: %s" % i) + self.log.info("Order placed: {}".format(i)) def print_UpdateCallOrder(self, i): """ Is called when a call order for a market pegged asset is updated @@ -56,7 +54,7 @@ def print_UpdateCallOrder(self, i): :param bitshares.price.CallOrder i: Call order details """ - self.log.info("call update: %s" % i) + self.log.info("Call update: {}".format(i)) def print_marketUpdate(self, i): """ Is called when Something happens in your market. @@ -67,7 +65,7 @@ def print_marketUpdate(self, i): :param object i: Can be instance of ``FilledOrder``, ``Order``, or ``CallOrder`` """ - self.log.info("marketupdate: %s" % i) + self.log.info("Market update: {}".format(i)) def print_newBlock(self, i): """ Is called when a block is received @@ -79,12 +77,11 @@ def print_newBlock(self, i): need to know the most recent block number, you need to use ``bitshares.blockchain.Blockchain`` """ - self.log.info("new1 block: %s" % i) - # raise ValueError("Testing disabling") + self.log.info("New block: {}".format(i)) def print_accountUpdate(self, i): """ 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. """ - self.log.info("account: %s" % i) + self.log.info("Account: {}".format(i)) diff --git a/dexbot/strategies/storagedemo.py b/dexbot/strategies/storagedemo.py deleted file mode 100644 index 98677ccf7..000000000 --- a/dexbot/strategies/storagedemo.py +++ /dev/null @@ -1,17 +0,0 @@ -from dexbot.basestrategy import BaseStrategy - - -class Strategy(BaseStrategy): - """ - Storage demo strategy - Strategy that prints all new blocks in the blockchain - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.ontick += self.tick - - def tick(self, i): - print("previous block: %s" % self["block"]) - print("new block: %s" % i) - self["block"] = i diff --git a/dexbot/strategies/walls.py b/dexbot/strategies/walls.py index 2bd28358c..91137d3e6 100644 --- a/dexbot/strategies/walls.py +++ b/dexbot/strategies/walls.py @@ -6,8 +6,7 @@ class Strategy(BaseStrategy): - """ - Walls strategy + """ Walls strategy """ @classmethod @@ -17,9 +16,7 @@ def configure(cls): ConfigElement("threshold", "int", 5, "percentage the feed has to move before we change orders", (0, 100)), ConfigElement("buy", "float", 0.0, "the default amount to buy", (0.0, None)), ConfigElement("sell", "float", 0.0, "the default amount to sell", (0.0, None)), - ConfigElement("blocks", "int", 20, "number of blocks to wait before re-calculating", (0, 10000)), - ConfigElement("dry_run", "bool", False, - "Dry Run Mode\nIf Yes the bot won't buy or sell anything, just log what it would do.\nIf No, the bot will buy and sell for real.", None) + ConfigElement("blocks", "int", 20, "number of blocks to wait before re-calculating", (0, 10000)) ] def __init__(self, *args, **kwargs): @@ -51,7 +48,7 @@ def updateorders(self): self.log.info("Replacing orders") # Canceling orders - self.cancelall() + self.cancel_all() # Target target = self.worker.get("target", {}) @@ -116,7 +113,7 @@ def test(self, *args, **kwargs): orders = self.orders # Test if still 2 orders in the market (the walls) - if len(orders) < 2 and len(orders) > 0: + if 0 < len(orders) < 2: if ( not self["insufficient_buy"] and not self["insufficient_sell"] From 0730f4530c75ae24bcc5de15ec30585cbef6ddd1 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 25 Jul 2018 10:40:14 +0300 Subject: [PATCH 0442/1846] Fix resize bug --- dexbot/controllers/worker_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index e5e0db42c..636657cc5 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -157,7 +157,7 @@ def change_strategy_form(self, worker_data=None): # Resize the dialog to be minimum possible height width = self.view.geometry().width() - self.view.setMinimumSize(width, 0) + self.view.setMinimumHeight(0) self.view.resize(width, 1) def validate_worker_name(self, worker_name, old_worker_name=None): From 599d0a3655ec15091b16686512e62c114be5b648 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 25 Jul 2018 10:48:06 +0300 Subject: [PATCH 0443/1846] Add title and other extra stuff to ConfigElement --- dexbot/basestrategy.py | 12 ++++++----- dexbot/cli_conf.py | 19 ++++++++++------- dexbot/controllers/worker_controller.py | 9 ++++---- dexbot/strategies/relative_orders.py | 27 ++++++++++++------------ dexbot/strategies/staggered_orders.py | 28 ++++++++++++------------- 5 files changed, 50 insertions(+), 45 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 243ddbbf9..346357f8d 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -21,7 +21,7 @@ MAX_TRIES = 3 -ConfigElement = collections.namedtuple('ConfigElement', 'key type default description extra') +ConfigElement = collections.namedtuple('ConfigElement', 'key type default title description extra') # Bots need to specify their own configuration values # I want this to be UI-agnostic so a future web or GUI interface can use it too # so each bot can have a class method 'configure' which returns a list of ConfigElement @@ -29,9 +29,11 @@ # Key: the key in the bot config dictionary that gets saved back to config.yml # Type: one of "int", "float", "bool", "string", "choice" # Default: the default value. must be right type. +# Title: name shown to the user, preferably not too long # Description: comments to user, full sentences encouraged # Extra: -# For int & float: a (min, max) tuple +# For int: a (min, max, suffix) tuple +# For float: a (min, max, precision, suffix) tuple # For string: a regular expression, entries must match it, can be None which equivalent to .* # For bool, ignored # For choice: a list of choices, choices are in turn (tag, label) tuples. @@ -97,10 +99,10 @@ def configure(cls): NOTE: when overriding you almost certainly will want to call the ancestor and then add your config values to the list. """ - # these configs are common to all bots return [ - ConfigElement("account", "string", "", "BitShares account name for the bot to operate with", ""), - ConfigElement("market", "string", "USD:BTS", + # These configs are common to all bots + ConfigElement("account", "string", "", "Account", "BitShares account name for the bot to operate with", ""), + ConfigElement("market", "string", "USD:BTS", "Market", "BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"", r"[A-Z\.]+[:\/][A-Z\.]+") ] diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index cf91826d4..0b53ddfcd 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -24,8 +24,6 @@ from dexbot.whiptail import get_whiptail from dexbot.basestrategy import BaseStrategy -from bitshares import BitShares - # FIXME: auto-discovery of strategies would be cool but can't figure out a way STRATEGIES = [ {'tag': 'relative', @@ -66,21 +64,26 @@ def process_config_element(elem, d, config): d: the Dialog object config: the config dictionary for this worker """ + if elem.description: + title = '{} - {}'.format(elem.title, elem.description) + else: + title = elem.title + if elem.type == "string": - txt = d.prompt(elem.description, config.get(elem.key, elem.default)) + txt = d.prompt(title, config.get(elem.key, elem.default)) if elem.extra: while not re.match(elem.extra, txt): d.alert("The value is not valid") txt = d.prompt( - elem.description, config.get( + title, config.get( elem.key, elem.default)) config[elem.key] = txt if elem.type == "bool": value = config.get(elem.key, elem.default) value = 'yes' if value else 'no' - config[elem.key] = d.confirm(elem.description, value) + config[elem.key] = d.confirm(title, value) if elem.type in ("float", "int"): - txt = d.prompt(elem.description, str(config.get(elem.key, elem.default))) + txt = d.prompt(title, str(config.get(elem.key, elem.default))) while True: try: if elem.type == "int": @@ -95,10 +98,10 @@ def process_config_element(elem, d, config): break except ValueError: d.alert("Not a valid value") - txt = d.prompt(elem.description, str(config.get(elem.key, elem.default))) + txt = d.prompt(title, str(config.get(elem.key, elem.default))) config[elem.key] = val if elem.type == "choice": - config[elem.key] = d.radiolist(elem.description, select_choice( + config[elem.key] = d.radiolist(title, select_choice( config.get(elem.key, elem.default), elem.extra)) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index 636657cc5..ddff2b050 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -35,12 +35,11 @@ def strategies(self): } return strategies - @staticmethod - def get_strategies(): - """ Static method for getting the strategies + @classmethod + def get_strategies(cls): + """ Class method for getting the strategies """ - controller = WorkerController(None, None, None) - return controller.strategies + return cls(None, None, None).strategies @property def base_assets(self): diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index d5eb07a91..3582a392f 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -11,20 +11,21 @@ class Strategy(BaseStrategy): @classmethod def configure(cls): return BaseStrategy.configure() + [ - ConfigElement('amount_relative', 'bool', False, + ConfigElement('amount_relative', 'bool', False, 'Relative amount', 'Amount is expressed as a percentage of the account balance of quote/base asset', None), - ConfigElement('amount', 'float', 1.0, - 'The amount of buy/sell orders', (0.0, None)), - ConfigElement('center_price_dynamic', 'bool', True, - 'Dynamic centre price', None), - ConfigElement('center_price', 'float', 0.0, - 'Initial center price', (0, 0, None)), - ConfigElement('center_price_offset', 'bool', False, - 'Center price offset based on asset balances', None), - ConfigElement('spread', 'float', 5.0, - 'The percentage difference between buy and sell (Spread)', (0.0, 100.0)), - ConfigElement('manual_offset', 'float', 0.0, - 'Manual center price offset', (-50.0, 100.0)) + ConfigElement('amount', 'float', 1, 'Amount', + 'Fixed order size, expressed in quote asset, unless "relative order size" selected', (0, None, 8, '')), + ConfigElement('center_price_dynamic', 'bool', True, 'Dynamic center price', + 'Always calculate the middle of the closest opposite market orders', None), + ConfigElement('center_price', 'float', 0, 'Center price', + 'Fixed center price expressed in base asset: base/quote', (0, None, 8, '%')), + ConfigElement('center_price_offset', 'bool', False, 'Center price offset based on asset balances', + 'Automatically adjust orders up or down based on the imbalance of your assets', None), + ConfigElement('spread', 'float', 5, 'Spread', + 'The percentage difference between buy and sell', (0, 100, 2, '%')), + ConfigElement('manual_offset', 'float', 0.0, 'Manual center price offset', + "Manually adjust orders up or down. " + "Works independently of other offsets and doesn't override them", (-50, 100, 2, '%')) ] def __init__(self, *args, **kwargs): diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 9508b9cc2..b67cfd5ad 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -14,26 +14,26 @@ class Strategy(BaseStrategy): def configure(cls): return BaseStrategy.configure() + [ ConfigElement( - 'amount', 'float', 1.0, - 'The amount of buy/sell orders', (0.0, None)), + 'amount', 'float', 1.0, 'Amount', + 'Fixed order size, expressed in quote asset', (0, None, 8, '')), ConfigElement( - 'center_price_dynamic', 'bool', True, - 'Dynamic centre price', None), + 'center_price_dynamic', 'bool', True, 'Dynamic center price', + 'Always calculate the middle from the closest market orders', None), ConfigElement( - 'center_price', 'float', 0.0, - 'Initial center price', (0, 0, None)), + 'center_price', 'float', 0, 'Center price', + 'Fixed center price expressed in base asset: base/quote', (0, None, 8, '')), ConfigElement( - 'spread', 'float', 6.0, - 'The percentage difference between buy and sell (Spread)', (0.0, None)), + 'spread', 'float', 6.0, 'Spread', + 'The percentage difference between buy and sell', (0, None, 2, '%')), ConfigElement( - 'increment', 'float', 4.0, - 'The percentage difference between staggered orders (Increment)', (0.0, None)), + 'increment', 'float', 4.0, 'Increment', + 'The percentage difference between staggered orders', (0, None, 2, '%')), ConfigElement( - 'upper_bound', 'float', 1.0, - 'The top price in the range', (0.0, None)), + 'upper_bound', 'float', 1.0, 'Upper bound', + 'The top price in the range', (0.0, None, 8, '')), ConfigElement( - 'lower_bound', 'float', 1000.0, - 'The bottom price in the range', (0.0, None)) + 'lower_bound', 'float', 1000.0, 'Lower bound', + 'The bottom price in the range', (0.0, None, 8, '')) ] def __init__(self, *args, **kwargs): From 2a2bdc57b604994c94f609bc7bf9c9b7d71d0801 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 25 Jul 2018 10:49:38 +0300 Subject: [PATCH 0444/1846] Add tooltips to strategies --- dexbot/views/ui/edit_worker_window.ui | 206 +++++- .../views/ui/forms/relative_orders_widget.ui | 657 +++++++++++++++-- .../views/ui/forms/staggered_orders_widget.ui | 674 +++++++++++++++--- 3 files changed, 1331 insertions(+), 206 deletions(-) diff --git a/dexbot/views/ui/edit_worker_window.ui b/dexbot/views/ui/edit_worker_window.ui index aa16adcea..3d9db798c 100644 --- a/dexbot/views/ui/edit_worker_window.ui +++ b/dexbot/views/ui/edit_worker_window.ui @@ -7,7 +7,7 @@ 0 0 428 - 302 + 314 @@ -74,80 +74,214 @@ - - + + + + + 0 + 0 + + - 110 + 150 0 - - - 110 - 16777215 - - - - Base Asset + + true - - - - - 110 - 0 - + + + + + 0 + 0 + - 110 + 125 16777215 - - Quote Asset - - - quote_asset_input - - - + + - + 0 0 + + + 110 + 0 + + - 80 + 110 16777215 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Base Asset + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Asset to be used as unit of measure + + + ? + + + 5 + + + + - - + + - + 0 0 - 105 + 110 0 - - true + + + 110 + 16777215 + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Quote Asset + + + quote_asset_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Asset to be bought and sold + + + ? + + + 5 + + + + diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index 9d8f6d94c..efb14defc 100644 --- a/dexbot/views/ui/forms/relative_orders_widget.ui +++ b/dexbot/views/ui/forms/relative_orders_widget.ui @@ -6,8 +6,8 @@ 0 0 - 416 - 258 + 439 + 291 @@ -48,9 +48,9 @@ 9 - + - + 0 0 @@ -67,12 +67,79 @@ 16777215 - - Amount - - - amount_input - + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Order size + + + amount_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Fixed order size, expressed in quote asset, unless "relative order size" selected + + + ? + + + 5 + + + + @@ -100,10 +167,10 @@ - - + + - + 0 0 @@ -120,15 +187,169 @@ 16777215 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Amount is expressed as a percentage of the account balance of quote/base asset + + + ? + + + 5 + + + + + + + + - Center Price + Relative order size + + + + + + + + 0 + 0 + + + + + 110 + 0 + - - center_price_input + + + 110 + 16777215 + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Center Price + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Fixed center price expressed in base asset: base/quote + + + ? + + + 5 + + + + - + false @@ -166,9 +387,9 @@ - + - + 0 0 @@ -185,16 +406,174 @@ 16777215 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Always calculate the middle of the closest opposite market orders + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + - Spread + Update center price from closest market orders - - spread_input + + true + + + false - - + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Automatically adjust orders up or down based on the imbalance of your assets + + + ? + + + 5 + + + + + + + + + + Center price offset based on asset balances + + + + + 0 @@ -210,47 +589,133 @@ + + QAbstractSpinBox::UpDownArrows + + + false + % - - 100000.000000000000000 + + -50.000000000000000 - - 5.000000000000000 + + 100.000000000000000 - - - - Calculate center price dynamically + + + + + 0 + 0 + - - true + + + 110 + 0 + - - - - - - Relative order size + + + 110 + 16777215 + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + + + + Manual center price offset + + + true + + + manual_offset_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Manually adjust orders up or down. Works independently of other offsets and doesn't override them + + + ? + + + 5 + + + + - - - - Center price offset based on asset balances + + + + + 0 + 0 + - - - - 110 - 31 + 0 @@ -259,19 +724,83 @@ 16777215 - - Manual center price offset - - - true - - - manual_offset_input - + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Spread + + + spread_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + The percentage difference between buy and sell + + + ? + + + 5 + + + + - - + + 0 @@ -290,11 +819,11 @@ % - - -50.000000000000000 - - 100.000000000000000 + 100000.000000000000000 + + + 5.000000000000000 diff --git a/dexbot/views/ui/forms/staggered_orders_widget.ui b/dexbot/views/ui/forms/staggered_orders_widget.ui index 2eda84934..ca3ecf36b 100644 --- a/dexbot/views/ui/forms/staggered_orders_widget.ui +++ b/dexbot/views/ui/forms/staggered_orders_widget.ui @@ -6,8 +6,8 @@ 0 0 - 382 - 350 + 439 + 415 @@ -32,14 +32,8 @@ Worker Parameters - - - - - 0 - 0 - - + + 110 @@ -52,15 +46,82 @@ 16777215 - - Amount - - - spread_input - + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Order size + + + true + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Amount of sell/buy order + + + ? + + + 5 + + + + - + @@ -91,10 +152,10 @@ - - + + - + 0 0 @@ -111,15 +172,82 @@ 16777215 - - Spread - - - spread_input - + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Spread + + + true + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + The percentage difference between buy and sell + + + ? + + + 5 + + + + - + @@ -147,10 +275,10 @@ - - + + - + 0 0 @@ -167,15 +295,82 @@ 16777215 - - Increment - - - spread_input - + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Increment + + + true + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + The percentage difference between each staggered order + + + ? + + + 5 + + + + - + @@ -203,10 +398,10 @@ - - + + - + 0 0 @@ -223,15 +418,82 @@ 16777215 - - Lower bound - - - spread_input - + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Lower bound + + + true + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + The bottom price in the range + + + ? + + + 5 + + + + - + @@ -259,10 +521,10 @@ - - + + - + 0 0 @@ -279,15 +541,187 @@ 16777215 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Center Price + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Fixed center price expressed in base asset: base/quote + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + - Upper bound + Update center price from closest market orders - - spread_input + + true - + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Upper bound + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + The top price in the range + + + ? + + + 5 + + + + + + + @@ -315,10 +749,10 @@ - - + + - + 0 0 @@ -335,15 +769,70 @@ 16777215 - - Center Price - - - center_price_input - + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Always calculate the middle of the closest opposite market orders + + + ? + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 5 + + + + - + false @@ -380,16 +869,6 @@ - - - - Calculate center price dynamically - - - true - - - @@ -402,6 +881,20 @@ 3 + + + + Required base + + + + + + + N/A + + + @@ -431,42 +924,11 @@ - - - - Required base - - - - - - - N/A - - - - - - center_price_dynamic_checkbox - clicked(bool) - center_price_input - setDisabled(bool) - - - 252 - 165 - - - 208 - 136 - - - - + From c2810681be35e1de469cb71c4e33fbf0eb3ac3f0 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 25 Jul 2018 13:00:44 +0300 Subject: [PATCH 0445/1846] Add basic ui autogeneration Saving and config value fetching still missing --- dexbot/basestrategy.py | 46 ++++--- dexbot/controllers/strategy_controller.py | 25 ++++ dexbot/helper.py | 7 ++ dexbot/strategies/relative_orders.py | 4 +- dexbot/strategies/staggered_orders.py | 4 +- dexbot/strategies/walls.py | 19 +-- dexbot/views/auto_strategy_form.py | 142 ++++++++++++++++++++++ dexbot/views/strategy_form.py | 47 ++++--- 8 files changed, 240 insertions(+), 54 deletions(-) create mode 100644 dexbot/views/auto_strategy_form.py diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 346357f8d..4c0d36e81 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -7,6 +7,7 @@ from .storage import Storage from .statemachine import StateMachine from .config import Config +from .helper import truncate from events import Events import bitsharesapi @@ -18,20 +19,18 @@ from bitshares.price import FilledOrder, Order, UpdateCallOrder from bitshares.instance import shared_bitshares_instance - MAX_TRIES = 3 ConfigElement = collections.namedtuple('ConfigElement', 'key type default title description extra') -# Bots need to specify their own configuration values -# I want this to be UI-agnostic so a future web or GUI interface can use it too -# so each bot can have a class method 'configure' which returns a list of ConfigElement -# named tuples. Tuple fields as follows. -# Key: the key in the bot config dictionary that gets saved back to config.yml -# Type: one of "int", "float", "bool", "string", "choice" -# Default: the default value. must be right type. -# Title: name shown to the user, preferably not too long -# Description: comments to user, full sentences encouraged -# Extra: +# Strategies need to specify their own configuration values, so each strategy can have +# a class method 'configure' which returns a list of ConfigElement named tuples. +# Tuple fields as follows: +# - Key: the key in the bot config dictionary that gets saved back to config.yml +# - Type: one of "int", "float", "bool", "string", "choice" +# - Default: the default value. must be right type. +# - Title: name shown to the user, preferably not too long +# - Description: comments to user, full sentences encouraged +# - Extra: # For int: a (min, max, suffix) tuple # For float: a (min, max, precision, suffix) tuple # For string: a regular expression, entries must match it, can be None which equivalent to .* @@ -63,7 +62,7 @@ class BaseStrategy(Storage, StateMachine, Events): * ``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` + Also, BaseStrategy inherits :class:`dexbot.storage.Storage` which allows to permanently store data in a sqlite database using: @@ -71,9 +70,9 @@ class BaseStrategy(Storage, StateMachine, Events): .. note:: This applies a ``json.loads(json.dumps(value))``! - 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. + 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. """ __events__ = [ @@ -89,7 +88,7 @@ class BaseStrategy(Storage, StateMachine, Events): ] @classmethod - def configure(cls): + def configure(cls, return_base_config=True): """ Return a list of ConfigElement objects defining the configuration values for this class @@ -99,13 +98,16 @@ def configure(cls): NOTE: when overriding you almost certainly will want to call the ancestor and then add your config values to the list. """ - return [ # These configs are common to all bots + base_config = [ ConfigElement("account", "string", "", "Account", "BitShares account name for the bot to operate with", ""), ConfigElement("market", "string", "USD:BTS", "Market", "BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"", r"[A-Z\.]+[:\/][A-Z\.]+") ] + if return_base_config: + return base_config + return [] def __init__( self, @@ -411,7 +413,7 @@ def pause(self): def market_buy(self, amount, price, return_none=False, *args, **kwargs): symbol = self.market['base']['symbol'] precision = self.market['base']['precision'] - base_amount = self.truncate(price * amount, precision) + base_amount = truncate(price * amount, precision) # Make sure we have enough balance for the order if self.balance(self.market['base']) < base_amount: @@ -450,7 +452,7 @@ def market_buy(self, amount, price, return_none=False, *args, **kwargs): def market_sell(self, amount, price, return_none=False, *args, **kwargs): symbol = self.market['quote']['symbol'] precision = self.market['quote']['precision'] - quote_amount = self.truncate(amount, precision) + quote_amount = truncate(amount, precision) # Make sure we have enough balance for the order if self.balance(self.market['quote']) < quote_amount: @@ -601,12 +603,6 @@ def retry_action(self, action, *args, **kwargs): else: raise - @staticmethod - def truncate(number, decimals): - """ Change the decimal point of a number without rounding - """ - return math.floor(number * 10 ** decimals) / 10 ** decimals - def write_order_log(self, worker_name, order): operation_type = 'TRADE' diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 1e151e7e5..92aafcb5b 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -6,6 +6,31 @@ from bitshares.asset import AssetDoesNotExistsException +class StrategyController: + """ General controller for strategies that don't have a custom controller + """ + + 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) + + @gui_error + def set_config_values(self, worker_data): + pass + + def validation_errors(self): + error_texts = [] + return error_texts + + @property + def values(self): + data = {} + return data + + class RelativeOrdersController: def __init__(self, view, worker_controller, worker_data): diff --git a/dexbot/helper.py b/dexbot/helper.py index 5bae94229..db00b1081 100644 --- a/dexbot/helper.py +++ b/dexbot/helper.py @@ -1,4 +1,5 @@ import os +import math import shutil import errno import logging @@ -32,6 +33,12 @@ def remove(path): return +def truncate(number, decimals): + """ Change the decimal point of a number without rounding + """ + return math.floor(number * 10 ** decimals) / 10 ** decimals + + def initialize_orders_log(): """ Creates .csv log file, adds the headers first time only """ diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 3582a392f..4f8a3021d 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -9,8 +9,8 @@ class Strategy(BaseStrategy): """ @classmethod - def configure(cls): - return BaseStrategy.configure() + [ + def configure(cls, return_base_config=True): + return BaseStrategy.configure(return_base_config) + [ ConfigElement('amount_relative', 'bool', False, 'Relative amount', 'Amount is expressed as a percentage of the account balance of quote/base asset', None), ConfigElement('amount', 'float', 1, 'Amount', diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index b67cfd5ad..bc60e1578 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -11,8 +11,8 @@ class Strategy(BaseStrategy): """ @classmethod - def configure(cls): - return BaseStrategy.configure() + [ + def configure(cls, return_base_config=True): + return BaseStrategy.configure(return_base_config) + [ ConfigElement( 'amount', 'float', 1.0, 'Amount', 'Fixed order size, expressed in quote asset', (0, None, 8, '')), diff --git a/dexbot/strategies/walls.py b/dexbot/strategies/walls.py index 91137d3e6..2b6e4d91f 100644 --- a/dexbot/strategies/walls.py +++ b/dexbot/strategies/walls.py @@ -10,13 +10,18 @@ class Strategy(BaseStrategy): """ @classmethod - def configure(cls): - return BaseStrategy.configure()+[ - ConfigElement("spread", "int", 5, "the spread between sell and buy as percentage", (0, 100)), - ConfigElement("threshold", "int", 5, "percentage the feed has to move before we change orders", (0, 100)), - ConfigElement("buy", "float", 0.0, "the default amount to buy", (0.0, None)), - ConfigElement("sell", "float", 0.0, "the default amount to sell", (0.0, None)), - ConfigElement("blocks", "int", 20, "number of blocks to wait before re-calculating", (0, 10000)) + def configure(cls, return_base_config=True): + return BaseStrategy.configure(return_base_config) + [ + ConfigElement("spread", "float", 5, "Spread", + "The spread between sell and buy as percentage", (0, 100, 2, '%')), + ConfigElement("threshold", "float", 5, "Threshold", + "Percentage the feed has to move before we change orders", (0, 100, 2, '%')), + ConfigElement("buy", "float", 0, "Buy", + "The default amount to buy", (0, None, 8, '')), + ConfigElement("sell", "float", 0, "Sell", + "The default amount to sell", (0, None, 8, '')), + ConfigElement("blocks", "int", 20, "Block num", + "Number of blocks to wait before re-calculating", (0, 10000, '')) ] def __init__(self, *args, **kwargs): diff --git a/dexbot/views/auto_strategy_form.py b/dexbot/views/auto_strategy_form.py new file mode 100644 index 000000000..27d6f4548 --- /dev/null +++ b/dexbot/views/auto_strategy_form.py @@ -0,0 +1,142 @@ +import importlib + +from PyQt5 import QtWidgets, QtCore, QtGui + + +class AutoStrategyFormWidget(QtWidgets.QWidget): + """ Automatic strategy form UI generator + """ + + def __init__(self, view, strategy_module, config): + super().__init__() + self.index = 0 + + self.vertical_layout = QtWidgets.QVBoxLayout(view) + self.vertical_layout.setContentsMargins(0, 0, 0, 0) + + self.group_box = QtWidgets.QGroupBox(view) + self.group_box.setTitle("Worker Parameters") + self.vertical_layout.addWidget(self.group_box) + self.form_layout = QtWidgets.QFormLayout(self.group_box) + + strategy = getattr( + importlib.import_module(strategy_module), + 'Strategy' + ) + configure = strategy.configure(False) + + for config in configure: + self.add_element(config) + + def add_element(self, config): + extra = config.extra + if config.type == 'float': + self.add_double_spin_box( + config.title, config.default, extra[0], extra[1], extra[2], extra[3], config.description) + elif config.type == 'int': + self.add_spin_box(config.title, config.default, extra[0], extra[1], extra[2], config.description) + elif config.type == 'string': + self.add_line_edit(config.title, config.default, config.description) + elif config.type == 'bool': + self.add_checkbox(config.title, config.default, config.description) + + def _add_label_wrap(self): + wrap = QtWidgets.QWidget(self.group_box) + wrap.setMinimumSize(QtCore.QSize(110, 0)) + wrap.setMaximumSize(QtCore.QSize(110, 16777215)) + + layout = QtWidgets.QHBoxLayout(wrap) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.LabelRole, wrap) + return wrap + + def add_tooltip(self, tooltip_text, container): + tooltip = QtWidgets.QLabel(container) + font = QtGui.QFont() + font.setBold(True) + tooltip.setFont(font) + tooltip.setCursor(QtGui.QCursor(QtCore.Qt.WhatsThisCursor)) + + size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + tooltip.setSizePolicy(size_policy) + + tooltip.setText('?') + tooltip.setToolTip(tooltip_text) + + layout = container.layout() + layout.addWidget(tooltip) + + def _add_label(self, text, description=''): + wrap = self._add_label_wrap() + label = QtWidgets.QLabel(wrap) + label.setWordWrap(True) + + size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + label.setSizePolicy(size_policy) + + layout = wrap.layout() + layout.addWidget(label) + label.setText(text) + + if description: + self.add_tooltip(description, wrap) + + def add_double_spin_box(self, text, default, minimum, maximum, precision, suffix='', description=''): + self._add_label(text, description) + + input_field = QtWidgets.QDoubleSpinBox(self.group_box) + input_field.setDecimals(precision) + if minimum is not None: + input_field.setMinimum(minimum) + if maximum is not None: + input_field.setMaximum(maximum) + if suffix: + input_field.setSuffix(suffix) + input_field.setProperty('value', default) + + input_field.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) + size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + input_field.setSizePolicy(size_policy) + input_field.setMinimumSize(QtCore.QSize(151, 0)) + + self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) + self.index += 1 + + def add_spin_box(self, text, default, minimum, maximum, suffix='', description=''): + self._add_label(text, description) + + input_field = QtWidgets.QSpinBox(self.group_box) + if minimum is not None: + input_field.setMinimum(minimum) + if maximum is not None: + input_field.setMaximum(maximum) + if suffix: + input_field.setSuffix(suffix) + input_field.setProperty('value', default) + + size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + input_field.setSizePolicy(size_policy) + input_field.setMinimumSize(QtCore.QSize(151, 0)) + + self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) + self.index += 1 + + def add_line_edit(self, text, default, description=''): + self._add_label(text, description) + + input_field = QtWidgets.QLineEdit(self.group_box) + input_field.setProperty('value', default) + self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) + self.index += 1 + + def add_checkbox(self, text, default, description=''): + self._add_label('', description) + + input_field = QtWidgets.QCheckBox(self.group_box) + input_field.setText(text) + input_field.setChecked(default) + self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) + self.index += 1 + diff --git a/dexbot/views/strategy_form.py b/dexbot/views/strategy_form.py index e1cd7adbc..6177f7d11 100644 --- a/dexbot/views/strategy_form.py +++ b/dexbot/views/strategy_form.py @@ -1,6 +1,7 @@ import importlib import dexbot.controllers.strategy_controller +from dexbot.views.auto_strategy_form import AutoStrategyFormWidget from PyQt5 import QtWidgets @@ -13,24 +14,34 @@ def __init__(self, controller, strategy_module, config=None): 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) - - # Invoke the correct controller - class_name = '' - if self.module_name == 'relative_orders': - class_name = 'RelativeOrdersController' - elif self.module_name == 'staggered_orders': - class_name = 'StaggeredOrdersController' - - strategy_controller = getattr( - dexbot.controllers.strategy_controller, - class_name - ) + try: + widget = getattr( + importlib.import_module(form_module), + 'Ui_Form' + ) + self.strategy_widget = widget() + self.strategy_widget.setupUi(self) + except (ValueError, AttributeError): + self.strategy_widget = AutoStrategyFormWidget(self, strategy_module, config) + + # Assemble the controller class name + parts = self.module_name.split('_') + class_name = ''.join(map(str.capitalize, parts)) + class_name = class_name + 'Controller' + + try: + # Try to get the controller + strategy_controller = getattr( + dexbot.controllers.strategy_controller, + class_name + ) + except AttributeError: + # The controller doesn't exist, use the default controller + strategy_controller = getattr( + dexbot.controllers.strategy_controller, + 'StrategyController' + ) + self.strategy_controller = strategy_controller(self, controller, config) @property From 382b9f135aca18c39eeebbce211fbdb6114af082 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 25 Jul 2018 15:26:48 +0300 Subject: [PATCH 0446/1846] Add autogenerator value saving --- dexbot/controllers/strategy_controller.py | 3 +- dexbot/views/auto_strategy_form.py | 142 ------------------ dexbot/views/strategy_form.py | 170 +++++++++++++++++++++- 3 files changed, 167 insertions(+), 148 deletions(-) delete mode 100644 dexbot/views/auto_strategy_form.py diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 92aafcb5b..d6f0160f0 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -27,8 +27,7 @@ def validation_errors(self): @property def values(self): - data = {} - return data + return self.view.strategy_widget.values class RelativeOrdersController: diff --git a/dexbot/views/auto_strategy_form.py b/dexbot/views/auto_strategy_form.py deleted file mode 100644 index 27d6f4548..000000000 --- a/dexbot/views/auto_strategy_form.py +++ /dev/null @@ -1,142 +0,0 @@ -import importlib - -from PyQt5 import QtWidgets, QtCore, QtGui - - -class AutoStrategyFormWidget(QtWidgets.QWidget): - """ Automatic strategy form UI generator - """ - - def __init__(self, view, strategy_module, config): - super().__init__() - self.index = 0 - - self.vertical_layout = QtWidgets.QVBoxLayout(view) - self.vertical_layout.setContentsMargins(0, 0, 0, 0) - - self.group_box = QtWidgets.QGroupBox(view) - self.group_box.setTitle("Worker Parameters") - self.vertical_layout.addWidget(self.group_box) - self.form_layout = QtWidgets.QFormLayout(self.group_box) - - strategy = getattr( - importlib.import_module(strategy_module), - 'Strategy' - ) - configure = strategy.configure(False) - - for config in configure: - self.add_element(config) - - def add_element(self, config): - extra = config.extra - if config.type == 'float': - self.add_double_spin_box( - config.title, config.default, extra[0], extra[1], extra[2], extra[3], config.description) - elif config.type == 'int': - self.add_spin_box(config.title, config.default, extra[0], extra[1], extra[2], config.description) - elif config.type == 'string': - self.add_line_edit(config.title, config.default, config.description) - elif config.type == 'bool': - self.add_checkbox(config.title, config.default, config.description) - - def _add_label_wrap(self): - wrap = QtWidgets.QWidget(self.group_box) - wrap.setMinimumSize(QtCore.QSize(110, 0)) - wrap.setMaximumSize(QtCore.QSize(110, 16777215)) - - layout = QtWidgets.QHBoxLayout(wrap) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.LabelRole, wrap) - return wrap - - def add_tooltip(self, tooltip_text, container): - tooltip = QtWidgets.QLabel(container) - font = QtGui.QFont() - font.setBold(True) - tooltip.setFont(font) - tooltip.setCursor(QtGui.QCursor(QtCore.Qt.WhatsThisCursor)) - - size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - tooltip.setSizePolicy(size_policy) - - tooltip.setText('?') - tooltip.setToolTip(tooltip_text) - - layout = container.layout() - layout.addWidget(tooltip) - - def _add_label(self, text, description=''): - wrap = self._add_label_wrap() - label = QtWidgets.QLabel(wrap) - label.setWordWrap(True) - - size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - label.setSizePolicy(size_policy) - - layout = wrap.layout() - layout.addWidget(label) - label.setText(text) - - if description: - self.add_tooltip(description, wrap) - - def add_double_spin_box(self, text, default, minimum, maximum, precision, suffix='', description=''): - self._add_label(text, description) - - input_field = QtWidgets.QDoubleSpinBox(self.group_box) - input_field.setDecimals(precision) - if minimum is not None: - input_field.setMinimum(minimum) - if maximum is not None: - input_field.setMaximum(maximum) - if suffix: - input_field.setSuffix(suffix) - input_field.setProperty('value', default) - - input_field.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) - size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - input_field.setSizePolicy(size_policy) - input_field.setMinimumSize(QtCore.QSize(151, 0)) - - self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) - self.index += 1 - - def add_spin_box(self, text, default, minimum, maximum, suffix='', description=''): - self._add_label(text, description) - - input_field = QtWidgets.QSpinBox(self.group_box) - if minimum is not None: - input_field.setMinimum(minimum) - if maximum is not None: - input_field.setMaximum(maximum) - if suffix: - input_field.setSuffix(suffix) - input_field.setProperty('value', default) - - size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - input_field.setSizePolicy(size_policy) - input_field.setMinimumSize(QtCore.QSize(151, 0)) - - self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) - self.index += 1 - - def add_line_edit(self, text, default, description=''): - self._add_label(text, description) - - input_field = QtWidgets.QLineEdit(self.group_box) - input_field.setProperty('value', default) - self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) - self.index += 1 - - def add_checkbox(self, text, default, description=''): - self._add_label('', description) - - input_field = QtWidgets.QCheckBox(self.group_box) - input_field.setText(text) - input_field.setChecked(default) - self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) - self.index += 1 - diff --git a/dexbot/views/strategy_form.py b/dexbot/views/strategy_form.py index 6177f7d11..c5f271eca 100644 --- a/dexbot/views/strategy_form.py +++ b/dexbot/views/strategy_form.py @@ -1,9 +1,8 @@ import importlib import dexbot.controllers.strategy_controller -from dexbot.views.auto_strategy_form import AutoStrategyFormWidget -from PyQt5 import QtWidgets +from PyQt5 import QtWidgets, QtCore, QtGui class StrategyFormWidget(QtWidgets.QWidget): @@ -22,7 +21,8 @@ def __init__(self, controller, strategy_module, config=None): self.strategy_widget = widget() self.strategy_widget.setupUi(self) except (ValueError, AttributeError): - self.strategy_widget = AutoStrategyFormWidget(self, strategy_module, config) + # Generate the strategy form widget automatically + self.strategy_widget = AutoStrategyFormGenerator(self, strategy_module, config) # Assemble the controller class name parts = self.module_name.split('_') @@ -46,6 +46,168 @@ def __init__(self, controller, strategy_module, config=None): @property def values(self): - """ Returns values all the form values based on selected strategy + """ Returns all the form values based on selected strategy """ return self.strategy_controller.values + + +class AutoStrategyFormGenerator: + """ Automatic strategy form UI generator + """ + + def __init__(self, view, strategy_module, config): + self.index = 0 + self.elements = {} + + self.vertical_layout = QtWidgets.QVBoxLayout(view) + self.vertical_layout.setContentsMargins(0, 0, 0, 0) + + self.group_box = QtWidgets.QGroupBox(view) + self.group_box.setTitle("Worker Parameters") + self.vertical_layout.addWidget(self.group_box) + self.form_layout = QtWidgets.QFormLayout(self.group_box) + + strategy = getattr( + importlib.import_module(strategy_module), + 'Strategy' + ) + configure = strategy.configure(False) + + for config in configure: + self.add_element(config) + + def add_element(self, config): + extra = config.extra + if config.type == 'float': + self.add_double_spin_box( + config.key, config.title, config.default, + extra[0], extra[1], extra[2], extra[3], config.description) + elif config.type == 'int': + self.add_spin_box( + config.key, config.title, config.default, + extra[0], extra[1], extra[2], config.description) + elif config.type == 'string': + self.add_line_edit(config.key, config.title, config.default, config.description) + elif config.type == 'bool': + self.add_checkbox(config.key, config.title, config.default, config.description) + elif config.type == 'choice': + pass + + def _add_label_wrap(self): + wrap = QtWidgets.QWidget(self.group_box) + wrap.setMinimumSize(QtCore.QSize(110, 0)) + wrap.setMaximumSize(QtCore.QSize(110, 16777215)) + + layout = QtWidgets.QHBoxLayout(wrap) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.LabelRole, wrap) + return wrap + + def add_tooltip(self, tooltip_text, container): + tooltip = QtWidgets.QLabel(container) + font = QtGui.QFont() + font.setBold(True) + tooltip.setFont(font) + tooltip.setCursor(QtGui.QCursor(QtCore.Qt.WhatsThisCursor)) + + size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + tooltip.setSizePolicy(size_policy) + + tooltip.setText('?') + tooltip.setToolTip(tooltip_text) + + layout = container.layout() + layout.addWidget(tooltip) + + def _add_label(self, text, description=''): + wrap = self._add_label_wrap() + label = QtWidgets.QLabel(wrap) + label.setWordWrap(True) + + size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + label.setSizePolicy(size_policy) + + layout = wrap.layout() + layout.addWidget(label) + label.setText(text) + + if description: + self.add_tooltip(description, wrap) + + def add_double_spin_box(self, key, text, default, minimum, maximum, precision, suffix='', description=''): + self._add_label(text, description) + + input_field = QtWidgets.QDoubleSpinBox(self.group_box) + input_field.setDecimals(precision) + if minimum is not None: + input_field.setMinimum(minimum) + if maximum is not None: + input_field.setMaximum(maximum) + if suffix: + input_field.setSuffix(suffix) + input_field.setProperty('value', default) + + input_field.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) + size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + input_field.setSizePolicy(size_policy) + input_field.setMinimumSize(QtCore.QSize(151, 0)) + + self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) + self.elements[key] = input_field + self.index += 1 + + def add_spin_box(self, key, text, default, minimum, maximum, suffix='', description=''): + self._add_label(text, description) + + input_field = QtWidgets.QSpinBox(self.group_box) + if minimum is not None: + input_field.setMinimum(minimum) + if maximum is not None: + input_field.setMaximum(maximum) + if suffix: + input_field.setSuffix(suffix) + input_field.setProperty('value', default) + + size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + input_field.setSizePolicy(size_policy) + input_field.setMinimumSize(QtCore.QSize(151, 0)) + + self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) + self.elements[key] = input_field + self.index += 1 + + def add_line_edit(self, key, text, default, description=''): + self._add_label(text, description) + + input_field = QtWidgets.QLineEdit(self.group_box) + input_field.setProperty('value', default) + + self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) + self.elements[key] = input_field + self.index += 1 + + def add_checkbox(self, key, text, default, description=''): + self._add_label('', description) + + input_field = QtWidgets.QCheckBox(self.group_box) + input_field.setText(text) + input_field.setChecked(default) + + self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) + self.elements[key] = input_field + self.index += 1 + + @property + def values(self): + data = {} + for key, element in self.elements.items(): + class_name = element.__class__.__name__ + if class_name in ('QDoubleSpinBox', 'QSpinBox', 'QLineEdit'): + data[key] = element.value() + elif class_name == 'QCheckBox': + data[key] = element.isChecked() + elif class_name == 'QComboBox': + data[key] = element.currentData() + return data From 6c935d10070a8a4539ce51c469e26b6262853fab Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 26 Jul 2018 09:11:11 +0300 Subject: [PATCH 0447/1846] Add value setting to ui autogenerator --- dexbot/views/strategy_form.py | 75 +++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/dexbot/views/strategy_form.py b/dexbot/views/strategy_form.py index c5f271eca..29236c3e6 100644 --- a/dexbot/views/strategy_form.py +++ b/dexbot/views/strategy_form.py @@ -7,7 +7,7 @@ class StrategyFormWidget(QtWidgets.QWidget): - def __init__(self, controller, strategy_module, config=None): + def __init__(self, controller, strategy_module, worker_config=None): super().__init__() self.controller = controller self.module_name = strategy_module.split('.')[-1] @@ -22,7 +22,7 @@ def __init__(self, controller, strategy_module, config=None): self.strategy_widget.setupUi(self) except (ValueError, AttributeError): # Generate the strategy form widget automatically - self.strategy_widget = AutoStrategyFormGenerator(self, strategy_module, config) + self.strategy_widget = AutoStrategyFormGenerator(self, strategy_module, worker_config) # Assemble the controller class name parts = self.module_name.split('_') @@ -42,7 +42,7 @@ def __init__(self, controller, strategy_module, config=None): 'StrategyController' ) - self.strategy_controller = strategy_controller(self, controller, config) + self.strategy_controller = strategy_controller(self, controller, worker_config) @property def values(self): @@ -55,7 +55,7 @@ class AutoStrategyFormGenerator: """ Automatic strategy form UI generator """ - def __init__(self, view, strategy_module, config): + def __init__(self, view, strategy_module, worker_config): self.index = 0 self.elements = {} @@ -76,20 +76,53 @@ def __init__(self, view, strategy_module, config): for config in configure: self.add_element(config) + self.set_values(configure, worker_config) + + @property + def values(self): + data = {} + for key, element in self.elements.items(): + class_name = element.__class__.__name__ + if class_name in ('QDoubleSpinBox', 'QSpinBox', 'QLineEdit'): + data[key] = element.value() + elif class_name == 'QCheckBox': + data[key] = element.isChecked() + elif class_name == 'QComboBox': + data[key] = element.currentData() + return data + + def set_values(self, configure, worker_config): + for option in configure: + if worker_config and worker_config.get(option.key) is not None: + value = worker_config[option.key] + else: + value = option.default + + element = self.elements[option.key] + if option.type in ('int', 'float', 'string'): + element.setValue(value) + if option.type == 'bool': + if value: + element.setChecked(True) + else: + element.setChecked(False) + if option.type == 'choice': + pass + def add_element(self, config): extra = config.extra if config.type == 'float': self.add_double_spin_box( - config.key, config.title, config.default, + config.key, config.title, extra[0], extra[1], extra[2], extra[3], config.description) elif config.type == 'int': self.add_spin_box( - config.key, config.title, config.default, + config.key, config.title, extra[0], extra[1], extra[2], config.description) elif config.type == 'string': - self.add_line_edit(config.key, config.title, config.default, config.description) + self.add_line_edit(config.key, config.title, config.description) elif config.type == 'bool': - self.add_checkbox(config.key, config.title, config.default, config.description) + self.add_checkbox(config.key, config.title, config.description) elif config.type == 'choice': pass @@ -136,7 +169,7 @@ def _add_label(self, text, description=''): if description: self.add_tooltip(description, wrap) - def add_double_spin_box(self, key, text, default, minimum, maximum, precision, suffix='', description=''): + def add_double_spin_box(self, key, text, minimum, maximum, precision, suffix='', description=''): self._add_label(text, description) input_field = QtWidgets.QDoubleSpinBox(self.group_box) @@ -147,7 +180,6 @@ def add_double_spin_box(self, key, text, default, minimum, maximum, precision, s input_field.setMaximum(maximum) if suffix: input_field.setSuffix(suffix) - input_field.setProperty('value', default) input_field.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) @@ -158,7 +190,7 @@ def add_double_spin_box(self, key, text, default, minimum, maximum, precision, s self.elements[key] = input_field self.index += 1 - def add_spin_box(self, key, text, default, minimum, maximum, suffix='', description=''): + def add_spin_box(self, key, text, minimum, maximum, suffix='', description=''): self._add_label(text, description) input_field = QtWidgets.QSpinBox(self.group_box) @@ -168,7 +200,6 @@ def add_spin_box(self, key, text, default, minimum, maximum, suffix='', descript input_field.setMaximum(maximum) if suffix: input_field.setSuffix(suffix) - input_field.setProperty('value', default) size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) input_field.setSizePolicy(size_policy) @@ -178,36 +209,20 @@ def add_spin_box(self, key, text, default, minimum, maximum, suffix='', descript self.elements[key] = input_field self.index += 1 - def add_line_edit(self, key, text, default, description=''): + def add_line_edit(self, key, text, description=''): self._add_label(text, description) input_field = QtWidgets.QLineEdit(self.group_box) - input_field.setProperty('value', default) - self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) self.elements[key] = input_field self.index += 1 - def add_checkbox(self, key, text, default, description=''): + def add_checkbox(self, key, text, description=''): self._add_label('', description) input_field = QtWidgets.QCheckBox(self.group_box) input_field.setText(text) - input_field.setChecked(default) self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) self.elements[key] = input_field self.index += 1 - - @property - def values(self): - data = {} - for key, element in self.elements.items(): - class_name = element.__class__.__name__ - if class_name in ('QDoubleSpinBox', 'QSpinBox', 'QLineEdit'): - data[key] = element.value() - elif class_name == 'QCheckBox': - data[key] = element.isChecked() - elif class_name == 'QComboBox': - data[key] = element.currentData() - return data From 3bf8e5ccc4d1e098cb7257c490a29d6ebcb769fa Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 26 Jul 2018 10:18:27 +0300 Subject: [PATCH 0448/1846] Add combobox input for choice elements --- dexbot/views/strategy_form.py | 67 ++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/dexbot/views/strategy_form.py b/dexbot/views/strategy_form.py index 29236c3e6..c6500e8b6 100644 --- a/dexbot/views/strategy_form.py +++ b/dexbot/views/strategy_form.py @@ -73,8 +73,8 @@ def __init__(self, view, strategy_module, worker_config): ) configure = strategy.configure(False) - for config in configure: - self.add_element(config) + for option in configure: + self.add_element(option) self.set_values(configure, worker_config) @@ -107,24 +107,29 @@ def set_values(self, configure, worker_config): else: element.setChecked(False) if option.type == 'choice': - pass - - def add_element(self, config): - extra = config.extra - if config.type == 'float': + # Fill the combobox + for tag, label in option.extra: + element.addItem(label, tag) + # Set the value + index = element.findData(value) + element.setCurrentIndex(index) + + def add_element(self, option): + extra = option.extra + if option.type == 'float': self.add_double_spin_box( - config.key, config.title, - extra[0], extra[1], extra[2], extra[3], config.description) - elif config.type == 'int': + option.key, option.title, + extra[0], extra[1], extra[2], extra[3], option.description) + elif option.type == 'int': self.add_spin_box( - config.key, config.title, - extra[0], extra[1], extra[2], config.description) - elif config.type == 'string': - self.add_line_edit(config.key, config.title, config.description) - elif config.type == 'bool': - self.add_checkbox(config.key, config.title, config.description) - elif config.type == 'choice': - pass + option.key, option.title, + extra[0], extra[1], extra[2], option.description) + elif option.type == 'string': + self.add_line_edit(option.key, option.title, option.description) + elif option.type == 'bool': + self.add_checkbox(option.key, option.title, option.description) + elif option.type == 'choice': + self.add_combo_box(option.key, option.title, option.description) def _add_label_wrap(self): wrap = QtWidgets.QWidget(self.group_box) @@ -138,7 +143,7 @@ def _add_label_wrap(self): self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.LabelRole, wrap) return wrap - def add_tooltip(self, tooltip_text, container): + def _add_tooltip(self, tooltip_text, container): tooltip = QtWidgets.QLabel(container) font = QtGui.QFont() font.setBold(True) @@ -154,7 +159,7 @@ def add_tooltip(self, tooltip_text, container): layout = container.layout() layout.addWidget(tooltip) - def _add_label(self, text, description=''): + def add_label(self, text, description=''): wrap = self._add_label_wrap() label = QtWidgets.QLabel(wrap) label.setWordWrap(True) @@ -167,10 +172,10 @@ def _add_label(self, text, description=''): label.setText(text) if description: - self.add_tooltip(description, wrap) + self._add_tooltip(description, wrap) def add_double_spin_box(self, key, text, minimum, maximum, precision, suffix='', description=''): - self._add_label(text, description) + self.add_label(text, description) input_field = QtWidgets.QDoubleSpinBox(self.group_box) input_field.setDecimals(precision) @@ -191,7 +196,7 @@ def add_double_spin_box(self, key, text, minimum, maximum, precision, suffix='', self.index += 1 def add_spin_box(self, key, text, minimum, maximum, suffix='', description=''): - self._add_label(text, description) + self.add_label(text, description) input_field = QtWidgets.QSpinBox(self.group_box) if minimum is not None: @@ -210,7 +215,7 @@ def add_spin_box(self, key, text, minimum, maximum, suffix='', description=''): self.index += 1 def add_line_edit(self, key, text, description=''): - self._add_label(text, description) + self.add_label(text, description) input_field = QtWidgets.QLineEdit(self.group_box) self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) @@ -218,7 +223,7 @@ def add_line_edit(self, key, text, description=''): self.index += 1 def add_checkbox(self, key, text, description=''): - self._add_label('', description) + self.add_label('', description) input_field = QtWidgets.QCheckBox(self.group_box) input_field.setText(text) @@ -226,3 +231,15 @@ def add_checkbox(self, key, text, description=''): self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) self.elements[key] = input_field self.index += 1 + + def add_combo_box(self, key, text, description=''): + self.add_label(text, description) + + input_field = QtWidgets.QComboBox(self.group_box) + size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + input_field.setSizePolicy(size_policy) + input_field.setMinimumSize(QtCore.QSize(151, 0)) + + self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) + self.elements[key] = input_field + self.index += 1 From 017ddff021b6fe9274ff3226c31d6431748dfd2b Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 26 Jul 2018 10:59:11 +0300 Subject: [PATCH 0449/1846] Change strategy ui layout --- dexbot/views/strategy_form.py | 8 +- dexbot/views/ui/create_worker_window.ui | 202 +++++++++++++++--- dexbot/views/ui/edit_worker_window.ui | 4 +- .../views/ui/forms/relative_orders_widget.ui | 20 +- .../views/ui/forms/staggered_orders_widget.ui | 30 ++- 5 files changed, 218 insertions(+), 46 deletions(-) diff --git a/dexbot/views/strategy_form.py b/dexbot/views/strategy_form.py index c6500e8b6..64e81665d 100644 --- a/dexbot/views/strategy_form.py +++ b/dexbot/views/strategy_form.py @@ -189,7 +189,8 @@ def add_double_spin_box(self, key, text, minimum, maximum, precision, suffix='', input_field.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates)) size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) input_field.setSizePolicy(size_policy) - input_field.setMinimumSize(QtCore.QSize(151, 0)) + input_field.setMinimumSize(QtCore.QSize(170, 0)) + input_field.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) self.elements[key] = input_field @@ -208,7 +209,8 @@ def add_spin_box(self, key, text, minimum, maximum, suffix='', description=''): size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) input_field.setSizePolicy(size_policy) - input_field.setMinimumSize(QtCore.QSize(151, 0)) + input_field.setMinimumSize(QtCore.QSize(170, 0)) + input_field.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) self.elements[key] = input_field @@ -238,7 +240,7 @@ def add_combo_box(self, key, text, description=''): input_field = QtWidgets.QComboBox(self.group_box) size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) input_field.setSizePolicy(size_policy) - input_field.setMinimumSize(QtCore.QSize(151, 0)) + input_field.setMinimumSize(QtCore.QSize(170, 0)) self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) self.elements[key] = input_field diff --git a/dexbot/views/ui/create_worker_window.ui b/dexbot/views/ui/create_worker_window.ui index 9e6e9eb1a..023b0a227 100644 --- a/dexbot/views/ui/create_worker_window.ui +++ b/dexbot/views/ui/create_worker_window.ui @@ -243,80 +243,220 @@ - - + + + + + 0 + 0 + + - 110 + 145 0 - 110 + 80 16777215 - - Base Asset - - - + + + + + 0 + 0 + + - 110 + 170 0 - - - 110 - 16777215 - - - - Quote Asset - - - quote_asset_input + + true - - + + - + 0 0 + + + 110 + 0 + + - 80 + 110 16777215 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Base Asset + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Asset to be used as unit of measure + + + ? + + + 5 + + + + - - + + - + 0 0 - 105 + 110 0 - - true + + + 110 + 16777215 + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Quote Asset + + + quote_asset_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Asset to be bought and sold + + + ? + + + 5 + + + + diff --git a/dexbot/views/ui/edit_worker_window.ui b/dexbot/views/ui/edit_worker_window.ui index 3d9db798c..31359790f 100644 --- a/dexbot/views/ui/edit_worker_window.ui +++ b/dexbot/views/ui/edit_worker_window.ui @@ -84,7 +84,7 @@ - 150 + 170 0 @@ -103,7 +103,7 @@ - 125 + 145 16777215 diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index efb14defc..8c198c14d 100644 --- a/dexbot/views/ui/forms/relative_orders_widget.ui +++ b/dexbot/views/ui/forms/relative_orders_widget.ui @@ -152,13 +152,16 @@ - 140 + 170 0 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + 8 @@ -362,13 +365,16 @@ - 140 + 170 0 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + false @@ -582,13 +588,16 @@ - 151 + 170 0 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + QAbstractSpinBox::UpDownArrows @@ -809,13 +818,16 @@ - 151 + 170 0 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + % diff --git a/dexbot/views/ui/forms/staggered_orders_widget.ui b/dexbot/views/ui/forms/staggered_orders_widget.ui index ca3ecf36b..859872035 100644 --- a/dexbot/views/ui/forms/staggered_orders_widget.ui +++ b/dexbot/views/ui/forms/staggered_orders_widget.ui @@ -131,13 +131,16 @@ - 151 + 170 0 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + @@ -257,13 +260,16 @@ - 151 + 170 0 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + % @@ -380,13 +386,16 @@ - 151 + 170 0 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + % @@ -503,13 +512,16 @@ - 140 + 170 0 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + 8 @@ -731,13 +743,16 @@ - 140 + 170 0 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + 8 @@ -845,13 +860,16 @@ - 140 + 170 0 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + false From 3577b2c0741681831ca4096968a371943bc228d5 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 26 Jul 2018 12:18:54 +0300 Subject: [PATCH 0450/1846] Update Bitshares to version 0.1.19 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 94da7f0a2..70d945a67 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ cmd_class = {} console_scripts = ['dexbot-cli = dexbot.cli:main'] install_requires = [ - "bitshares==0.1.18", + "bitshares==0.1.19", "uptick>=0.1.4", "click", "sqlalchemy", From 009da8e0c147cdb15f72c46572121a61231b7462 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 26 Jul 2018 12:21:13 +0300 Subject: [PATCH 0451/1846] Fix crashing when connecting to a node --- dexbot/gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/gui.py b/dexbot/gui.py index 5a1ab4997..e54ddd1b0 100644 --- a/dexbot/gui.py +++ b/dexbot/gui.py @@ -16,7 +16,7 @@ def __init__(self, sys_argv): super(App, self).__init__(sys_argv) config = Config() - bitshares_instance = BitShares(config['node']) + bitshares_instance = BitShares(config['node'], num_retries=-1) # Wallet unlock unlock_ctrl = WalletController(bitshares_instance) From 3123a588c5cb5a21e39ece0be121e7db3c58af50 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 27 Jul 2018 10:16:54 +0300 Subject: [PATCH 0452/1846] Add default value and config value for gui elements --- dexbot/controllers/strategy_controller.py | 182 +++++++++--------- dexbot/strategies/relative_orders.py | 6 +- dexbot/strategies/staggered_orders.py | 28 +-- dexbot/views/strategy_form.py | 105 ++++------ .../views/ui/forms/relative_orders_widget.ui | 10 +- .../views/ui/forms/staggered_orders_widget.ui | 85 ++++---- 6 files changed, 194 insertions(+), 222 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index d6f0160f0..70cc9e0c2 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -4,147 +4,148 @@ from bitshares.market import Market from bitshares.asset import AssetDoesNotExistsException +from PyQt5 import QtWidgets class StrategyController: - """ General controller for strategies that don't have a custom controller + """ Parent controller for strategies that don't have a custom controller """ - def __init__(self, view, worker_controller, worker_data): + def __init__(self, view, configure, worker_controller, worker_data): self.view = view + self.configure = configure self.worker_controller = worker_controller - if worker_data: - self.set_config_values(worker_data) - - @gui_error - def set_config_values(self, worker_data): - pass + self.set_values(configure, worker_data) def validation_errors(self): - error_texts = [] - return error_texts + return [] + + def set_values(self, configure, worker_config): + for option in configure: + if worker_config and worker_config.get(option.key) is not None: + value = worker_config[option.key] + else: + value = option.default + + element = self.elements[option.key] + if not element: + continue + + if option.type in ('int', 'float', 'string'): + element.setValue(value) + if option.type == 'bool': + if value: + element.setChecked(True) + else: + element.setChecked(False) + if option.type == 'choice': + # Fill the combobox + for tag, label in option.extra: + element.addItem(label, tag) + # Set the value + index = element.findData(value) + element.setCurrentIndex(index) @property def values(self): - return self.view.strategy_widget.values + data = {} + for key, element in self.elements.items(): + class_name = element.__class__.__name__ + if class_name in ('QDoubleSpinBox', 'QSpinBox', 'QLineEdit'): + data[key] = element.value() + elif class_name == 'QCheckBox': + data[key] = element.isChecked() + elif class_name == 'QComboBox': + data[key] = element.currentData() + return data + @property + def elements(self): + """ Use ConfigElement of the strategy to find the elements + """ + elements = {} + types = ( + QtWidgets.QDoubleSpinBox, + QtWidgets.QSpinBox, + QtWidgets.QLineEdit, + QtWidgets.QCheckBox, + QtWidgets.QComboBox + ) + + for option in self.configure: + element_name = ''.join([option.key, '_input']) + elements[option.key] = self.view.findChild(types, element_name) + return elements -class RelativeOrdersController: - def __init__(self, view, worker_controller, worker_data): +class RelativeOrdersController(StrategyController): + + def __init__(self, view, configure, worker_controller, worker_data): self.view = view + self.configure = configure self.worker_controller = worker_controller - self.view.strategy_widget.relative_order_size_checkbox.toggled.connect( - self.onchange_relative_order_size_checkbox + self.view.strategy_widget.relative_order_size_input.toggled.connect( + self.onchange_relative_order_size_input ) - if worker_data: - self.set_config_values(worker_data) + # Do this after the event connecting + super().__init__(view, configure, worker_controller, worker_data) - @gui_error - def onchange_relative_order_size_checkbox(self, checked): + if not self.view.strategy_widget.center_price_dynamic_input.isChecked(): + self.view.strategy_widget.center_price_input.setDisabled(False) + + def onchange_relative_order_size_input(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.setMinimumWidth(170) 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)) - self.view.strategy_widget.manual_offset_input.setValue(worker_data.get('manual_offset', 0)) - - 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) - 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) - 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(): - error_texts.append("Amount can't be 0") + error_texts.append("Order size 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 - 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(), - 'center_price_offset': self.view.strategy_widget.center_price_offset_checkbox.isChecked(), - 'spread': self.view.strategy_widget.spread_input.value(), - 'manual_offset': self.view.strategy_widget.manual_offset_input.value() - } - return data +class StaggeredOrdersController(StrategyController): -class StaggeredOrdersController: - - def __init__(self, view, worker_controller, worker_data): + def __init__(self, view, configure, worker_controller, worker_data): self.view = view + self.configure = configure 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.center_price_dynamic_input.stateChanged.connect(lambda: self.on_value_change()) + widget.center_price_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)) - - self.view.strategy_widget.center_price_input.setValue(worker_data.get('center_price', 0)) + # Do this after the event connecting + super().__init__(view, configure, worker_controller, worker_data) - 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) + if not self.view.strategy_widget.center_price_dynamic_input.isChecked(): self.view.strategy_widget.center_price_input.setDisabled(False) @gui_error @@ -163,6 +164,10 @@ def on_value_change(self): 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 self.view.strategy_widget.center_price_dynamic_input.isChecked(): + center_price = None + else: + center_price = self.view.strategy_widget.center_price_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') @@ -170,7 +175,7 @@ def on_value_change(self): return strategy = StaggeredOrdersStrategy - result = strategy.get_required_assets(market, amount, spread, increment, lower_bound, upper_bound) + result = strategy.get_required_assets(market, amount, spread, increment, center_price, lower_bound, upper_bound) if not result: idle_add(self.set_required_base, 'N/A') idle_add(self.set_required_quote, 'N/A') @@ -191,7 +196,7 @@ def set_required_quote(self, text): def validation_errors(self): error_texts = [] if not self.view.strategy_widget.amount_input.value(): - error_texts.append("Amount can't be 0") + error_texts.append("Order size 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(): @@ -199,16 +204,3 @@ def validation_errors(self): 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 = { - 'amount': self.view.strategy_widget.amount_input.value(), - 'spread': self.view.strategy_widget.spread_input.value(), - 'center_price': self.view.strategy_widget.center_price_input.value(), - 'center_price_dynamic': self.view.strategy_widget.center_price_dynamic_checkbox.isChecked(), - '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/relative_orders.py b/dexbot/strategies/relative_orders.py index 4f8a3021d..b73159b47 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -11,19 +11,19 @@ class Strategy(BaseStrategy): @classmethod def configure(cls, return_base_config=True): return BaseStrategy.configure(return_base_config) + [ - ConfigElement('amount_relative', 'bool', False, 'Relative amount', + ConfigElement('relative_order_size', 'bool', False, 'Relative order size', 'Amount is expressed as a percentage of the account balance of quote/base asset', None), ConfigElement('amount', 'float', 1, 'Amount', 'Fixed order size, expressed in quote asset, unless "relative order size" selected', (0, None, 8, '')), ConfigElement('center_price_dynamic', 'bool', True, 'Dynamic center price', - 'Always calculate the middle of the closest opposite market orders', None), + 'Always calculate the middle from the closest market orders', None), ConfigElement('center_price', 'float', 0, 'Center price', 'Fixed center price expressed in base asset: base/quote', (0, None, 8, '%')), ConfigElement('center_price_offset', 'bool', False, 'Center price offset based on asset balances', 'Automatically adjust orders up or down based on the imbalance of your assets', None), ConfigElement('spread', 'float', 5, 'Spread', 'The percentage difference between buy and sell', (0, 100, 2, '%')), - ConfigElement('manual_offset', 'float', 0.0, 'Manual center price offset', + ConfigElement('manual_offset', 'float', 0, 'Manual center price offset', "Manually adjust orders up or down. " "Works independently of other offsets and doesn't override them", (-50, 100, 2, '%')) ] diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index bc60e1578..d2c279742 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -14,8 +14,14 @@ class Strategy(BaseStrategy): def configure(cls, return_base_config=True): return BaseStrategy.configure(return_base_config) + [ ConfigElement( - 'amount', 'float', 1.0, 'Amount', + 'amount', 'float', 1, 'Amount', 'Fixed order size, expressed in quote asset', (0, None, 8, '')), + ConfigElement( + 'spread', 'float', 6, 'Spread', + 'The percentage difference between buy and sell', (0, None, 2, '%')), + ConfigElement( + 'increment', 'float', 4, 'Increment', + 'The percentage difference between staggered orders', (0, None, 2, '%')), ConfigElement( 'center_price_dynamic', 'bool', True, 'Dynamic center price', 'Always calculate the middle from the closest market orders', None), @@ -23,17 +29,11 @@ def configure(cls, return_base_config=True): 'center_price', 'float', 0, 'Center price', 'Fixed center price expressed in base asset: base/quote', (0, None, 8, '')), ConfigElement( - 'spread', 'float', 6.0, 'Spread', - 'The percentage difference between buy and sell', (0, None, 2, '%')), - ConfigElement( - 'increment', 'float', 4.0, 'Increment', - 'The percentage difference between staggered orders', (0, None, 2, '%')), - ConfigElement( - 'upper_bound', 'float', 1.0, 'Upper bound', - 'The top price in the range', (0.0, None, 8, '')), + 'lower_bound', 'float', 1, 'Lower bound', + 'The bottom price in the range', (0, None, 8, '')), ConfigElement( - 'lower_bound', 'float', 1000.0, 'Lower bound', - 'The bottom price in the range', (0.0, None, 8, '')) + 'upper_bound', 'float', 1000000, 'Upper bound', + 'The top price in the range', (0, None, 8, '')) ] def __init__(self, *args, **kwargs): @@ -265,14 +265,16 @@ def calculate_amounts(buy_prices, sell_prices, amount, spread, increment): return [buy_orders, sell_orders] @staticmethod - def get_required_assets(market, amount, spread, increment, lower_bound, upper_bound): + def get_required_assets(market, amount, spread, increment, center_price, lower_bound, upper_bound): if not amount or 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): + if center_price: + pass + elif not float(highest_bid): return None elif not float(lowest_ask): return None diff --git a/dexbot/views/strategy_form.py b/dexbot/views/strategy_form.py index 64e81665d..7ce45ca76 100644 --- a/dexbot/views/strategy_form.py +++ b/dexbot/views/strategy_form.py @@ -12,6 +12,11 @@ def __init__(self, controller, strategy_module, worker_config=None): self.controller = controller self.module_name = strategy_module.split('.')[-1] + strategy_class = getattr( + importlib.import_module(strategy_module), + 'Strategy' + ) + configure = strategy_class.configure(False) form_module = controller.strategies[strategy_module]['form_module'] try: widget = getattr( @@ -22,12 +27,12 @@ def __init__(self, controller, strategy_module, worker_config=None): self.strategy_widget.setupUi(self) except (ValueError, AttributeError): # Generate the strategy form widget automatically - self.strategy_widget = AutoStrategyFormGenerator(self, strategy_module, worker_config) + self.strategy_widget = AutoStrategyFormGenerator(self, configure, worker_config) # Assemble the controller class name parts = self.module_name.split('_') class_name = ''.join(map(str.capitalize, parts)) - class_name = class_name + 'Controller' + class_name = ''.join([class_name, 'Controller']) try: # Try to get the controller @@ -42,7 +47,7 @@ def __init__(self, controller, strategy_module, worker_config=None): 'StrategyController' ) - self.strategy_controller = strategy_controller(self, controller, worker_config) + self.strategy_controller = strategy_controller(self, configure, controller, worker_config) @property def values(self): @@ -55,7 +60,7 @@ class AutoStrategyFormGenerator: """ Automatic strategy form UI generator """ - def __init__(self, view, strategy_module, worker_config): + def __init__(self, view, configure, worker_config): self.index = 0 self.elements = {} @@ -67,69 +72,30 @@ def __init__(self, view, strategy_module, worker_config): self.vertical_layout.addWidget(self.group_box) self.form_layout = QtWidgets.QFormLayout(self.group_box) - strategy = getattr( - importlib.import_module(strategy_module), - 'Strategy' - ) - configure = strategy.configure(False) - for option in configure: self.add_element(option) - self.set_values(configure, worker_config) - - @property - def values(self): - data = {} - for key, element in self.elements.items(): - class_name = element.__class__.__name__ - if class_name in ('QDoubleSpinBox', 'QSpinBox', 'QLineEdit'): - data[key] = element.value() - elif class_name == 'QCheckBox': - data[key] = element.isChecked() - elif class_name == 'QComboBox': - data[key] = element.currentData() - return data - - def set_values(self, configure, worker_config): - for option in configure: - if worker_config and worker_config.get(option.key) is not None: - value = worker_config[option.key] - else: - value = option.default - - element = self.elements[option.key] - if option.type in ('int', 'float', 'string'): - element.setValue(value) - if option.type == 'bool': - if value: - element.setChecked(True) - else: - element.setChecked(False) - if option.type == 'choice': - # Fill the combobox - for tag, label in option.extra: - element.addItem(label, tag) - # Set the value - index = element.findData(value) - element.setCurrentIndex(index) - def add_element(self, option): extra = option.extra if option.type == 'float': - self.add_double_spin_box( - option.key, option.title, - extra[0], extra[1], extra[2], extra[3], option.description) + element = self.add_double_spin_box( + option.title, extra[0], extra[1], extra[2], extra[3], option.description) elif option.type == 'int': - self.add_spin_box( - option.key, option.title, - extra[0], extra[1], extra[2], option.description) + element = self.add_spin_box( + option.title, extra[0], extra[1], extra[2], option.description) elif option.type == 'string': - self.add_line_edit(option.key, option.title, option.description) + element = self.add_line_edit(option.title, option.description) elif option.type == 'bool': - self.add_checkbox(option.key, option.title, option.description) + element = self.add_checkbox(option.title, option.description) elif option.type == 'choice': - self.add_combo_box(option.key, option.title, option.description) + element = self.add_combo_box(option.title, option.description) + else: + return + + element_name = ''.join([option.key, '_input']) + element.setObjectName(element_name) + self.index += 1 + self.elements[option.key] = element def _add_label_wrap(self): wrap = QtWidgets.QWidget(self.group_box) @@ -174,7 +140,7 @@ def add_label(self, text, description=''): if description: self._add_tooltip(description, wrap) - def add_double_spin_box(self, key, text, minimum, maximum, precision, suffix='', description=''): + def add_double_spin_box(self, text, minimum, maximum, precision, suffix='', description=''): self.add_label(text, description) input_field = QtWidgets.QDoubleSpinBox(self.group_box) @@ -193,10 +159,9 @@ def add_double_spin_box(self, key, text, minimum, maximum, precision, suffix='', input_field.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) - self.elements[key] = input_field - self.index += 1 + return input_field - def add_spin_box(self, key, text, minimum, maximum, suffix='', description=''): + def add_spin_box(self, text, minimum, maximum, suffix='', description=''): self.add_label(text, description) input_field = QtWidgets.QSpinBox(self.group_box) @@ -213,28 +178,25 @@ def add_spin_box(self, key, text, minimum, maximum, suffix='', description=''): input_field.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter) self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) - self.elements[key] = input_field - self.index += 1 + return input_field - def add_line_edit(self, key, text, description=''): + def add_line_edit(self, text, description=''): self.add_label(text, description) input_field = QtWidgets.QLineEdit(self.group_box) self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) - self.elements[key] = input_field - self.index += 1 + return input_field - def add_checkbox(self, key, text, description=''): + def add_checkbox(self, text, description=''): self.add_label('', description) input_field = QtWidgets.QCheckBox(self.group_box) input_field.setText(text) self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) - self.elements[key] = input_field - self.index += 1 + return input_field - def add_combo_box(self, key, text, description=''): + def add_combo_box(self, text, description=''): self.add_label(text, description) input_field = QtWidgets.QComboBox(self.group_box) @@ -243,5 +205,4 @@ def add_combo_box(self, key, text, description=''): input_field.setMinimumSize(QtCore.QSize(170, 0)) self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) - self.elements[key] = input_field - self.index += 1 + return input_field diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index 8c198c14d..0e02741a3 100644 --- a/dexbot/views/ui/forms/relative_orders_widget.ui +++ b/dexbot/views/ui/forms/relative_orders_widget.ui @@ -251,7 +251,7 @@ - + Relative order size @@ -459,7 +459,7 @@ WhatsThisCursor - Always calculate the middle of the closest opposite market orders + Always calculate the middle from the closest market orders ? @@ -473,7 +473,7 @@ - + 0 @@ -572,7 +572,7 @@ - + Center price offset based on asset balances @@ -847,7 +847,7 @@ - center_price_dynamic_checkbox + center_price_dynamic_input clicked(bool) center_price_input setDisabled(bool) diff --git a/dexbot/views/ui/forms/staggered_orders_widget.ui b/dexbot/views/ui/forms/staggered_orders_widget.ui index 859872035..850db0ab9 100644 --- a/dexbot/views/ui/forms/staggered_orders_widget.ui +++ b/dexbot/views/ui/forms/staggered_orders_widget.ui @@ -626,7 +626,7 @@ - + 0 @@ -733,37 +733,6 @@ - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 8 - - - 1000000000.000000000000000 - - - 1000000.000000000000000 - - - @@ -831,7 +800,7 @@ WhatsThisCursor - Always calculate the middle of the closest opposite market orders + Always calculate the middle from the closest market orders ? @@ -887,6 +856,37 @@ + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 8 + + + 1000000000.000000000000000 + + + 1000000.000000000000000 + + + @@ -948,5 +948,22 @@ - + + + center_price_dynamic_input + toggled(bool) + center_price_input + setDisabled(bool) + + + 281 + 272 + + + 217 + 236 + + + + From 743bad9056be561fa6f28594beffa680097688af Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 27 Jul 2018 10:38:39 +0300 Subject: [PATCH 0453/1846] Change dexbot version number to 0.4.12 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index e2a7771f8..3cb9a783b 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.4.11' +VERSION = '0.4.12' AUTHOR = 'Codaone Oy' __version__ = VERSION From b0ad8d6595911817aeac5e39c7b9f00ad3e53944 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 27 Jul 2018 10:55:13 +0300 Subject: [PATCH 0454/1846] Add missing title to whiptail --- dexbot/cli_conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 0585dd040..eb46eacf1 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -179,7 +179,7 @@ def configure_worker(whiptail, worker): def configure_dexbot(config, ctx): - whiptail = get_whiptail() + whiptail = get_whiptail('DEXBot configure') workers = config.get('workers', {}) if not workers: while True: From a0116f351db0e986983d72f44a5753c7cbc7123c Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 27 Jul 2018 12:31:22 +0300 Subject: [PATCH 0455/1846] Change dexbot version number to 0.4.13 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 67c103453..44c15b6d2 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.4.8' +VERSION = '0.4.13' AUTHOR = 'Codaone Oy' __version__ = VERSION From 1e4ff6a024e10a816dffa2fffc70c53c647bfabd Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 27 Jul 2018 13:07:09 +0300 Subject: [PATCH 0456/1846] Change dexbot version number to 0.5.0 --- dexbot/__init__.py | 2 +- dexbot/cli_conf.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 44c15b6d2..e07f25eac 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.4.13' +VERSION = '0.5.0' AUTHOR = 'Codaone Oy' __version__ = VERSION diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 920cd2d81..fedb27d46 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -1,7 +1,7 @@ """ A module to provide an interactive text-based tool for dexbot configuration The result is dexbot can be run without having to hand-edit config files. -If systemd is detected it will offer to install a user service unit (under ~/.local/share/systemd +If systemd is detected it will offer to install a user service unit (under ~/.local/share/systemd) This requires a per-user systemd process to be running Requires the 'whiptail' tool for text-based configuration (so UNIX only) @@ -166,7 +166,7 @@ def configure_worker(whiptail, worker): for i in STRATEGIES: if i['tag'] == worker['module']: worker['module'] = i['class'] - # Import the worker class but we don't __init__ it here + # Import the strategy class but we don't __init__ it here strategy_class = getattr( importlib.import_module(worker["module"]), 'Strategy' From 734a76f058304f247fc66b0dcb9497b389db128d Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 27 Jul 2018 13:14:11 +0300 Subject: [PATCH 0457/1846] Change dexbot version number to 0.5.1 A quick hotfix for relative order strategy --- dexbot/__init__.py | 2 +- dexbot/strategies/relative_orders.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index e07f25eac..93b4245eb 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.5.0' +VERSION = '0.5.1' AUTHOR = 'Codaone Oy' __version__ = VERSION diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index b73159b47..e33a37c49 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -46,10 +46,10 @@ def __init__(self, *args, **kwargs): else: self.center_price = self.worker["center_price"] - self.is_relative_order_size = self.worker['amount_relative'] + self.is_relative_order_size = self.worker.get('relative_order_size', False) self.is_asset_offset = self.worker.get('center_price_offset', False) self.manual_offset = self.worker.get('manual_offset', 0) / 100 - self.order_size = float(self.worker['amount']) + self.order_size = float(self.worker.get('amount', 1)) self.spread = self.worker.get('spread') / 100 self.buy_price = None From 9cf8badb6ea91839fd240060b25a78cc32448348 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 27 Jul 2018 13:46:56 +0300 Subject: [PATCH 0458/1846] Add a node to the default node list --- dexbot/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dexbot/config.py b/dexbot/config.py index cc7cd0c07..1beda59fb 100644 --- a/dexbot/config.py +++ b/dexbot/config.py @@ -169,6 +169,7 @@ def construct_mapping(mapping_loader, node): def node_list(self): """ A pre-defined list of Bitshares nodes. """ return [ + "wss://status200.bitshares.apasia.tech/ws", "wss://eu.openledger.info/ws", "wss://bitshares.openledger.info/ws", "wss://dexnode.net/ws", From 09417986aed4481e325ceedf3c72f625bcf9af61 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 27 Jul 2018 13:47:33 +0300 Subject: [PATCH 0459/1846] Change dexbot version number to 0.5.2 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 93b4245eb..0026037ac 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.5.1' +VERSION = '0.5.2' AUTHOR = 'Codaone Oy' __version__ = VERSION From 3271713b945f17b9d21f9b0f968f3413e0334dc4 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 30 Jul 2018 12:10:03 +0300 Subject: [PATCH 0460/1846] Solve merge conflict --- dexbot/controllers/strategy_controller.py | 30 ++++++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index fa2d0800e..9d0adc89f 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -1,11 +1,8 @@ import collections +from decimal import * -from dexbot.qt_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 from PyQt5 import QtWidgets @@ -116,6 +113,31 @@ def order_size_input_to_static(self): 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(Decimal(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)) + self.view.strategy_widget.manual_offset_input.setValue(worker_data.get('manual_offset', 0)) + + 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) + 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) + 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(): From cffef67c14228843c479d0145b71142ef42c986e Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 30 Jul 2018 12:11:26 +0300 Subject: [PATCH 0461/1846] Add all orders property --- dexbot/basestrategy.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 954bfd13d..6df843de1 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -261,6 +261,13 @@ def orders(self): self.account.refresh() return [o for o in self.account.openorders if self.worker["market"] == o.market and self.account.openorders] + @property + def all_orders(self): + """ Return the worker's open accounts in all markets + """ + self.account.refresh() + return [o for o in self.account.openorders] + def get_buy_orders(self, sort=None, orders=None): """ Return buy orders :param str sort: DESC or ASC will sort the orders accordingly, default None. From 46ddefdd1c79f313bb30c845cd8266110624c6e7 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 30 Jul 2018 13:49:37 +0300 Subject: [PATCH 0462/1846] Fix UI problems caused by merge --- dexbot/controllers/strategy_controller.py | 52 +- dexbot/strategies/staggered_orders.py | 8 +- .../views/ui/forms/staggered_orders_widget.ui | 755 ++++++++++++++++-- 3 files changed, 688 insertions(+), 127 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 9d0adc89f..d005f7fe9 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -113,31 +113,6 @@ def order_size_input_to_static(self): 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(Decimal(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)) - self.view.strategy_widget.manual_offset_input.setValue(worker_data.get('manual_offset', 0)) - - 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) - 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) - 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(): @@ -159,15 +134,12 @@ def __init__(self, view, configure, worker_controller, worker_data): for strategy_mode in modes: self.view.strategy_widget.mode_input.addItem(modes[strategy_mode], strategy_mode) + if not self.view.strategy_widget.center_price_dynamic_input.isChecked(): + self.view.strategy_widget.center_price_input.setDisabled(False) + # Do this after the event connecting super().__init__(view, configure, worker_controller, worker_data) - if not self.view.strategy_widget.center_price_dynamic_input.isChecked(): - self.view.strategy_widget.center_price_input.setDisabled(False) - - # Set allow instant order fill - self.view.strategy_widget.allow_instant_fill_checkbox.setChecked(worker_data.get('allow_instant_fill', True)) - def set_required_base(self, text): self.view.strategy_widget.required_base_text.setText(text) @@ -176,8 +148,6 @@ def set_required_quote(self, text): def validation_errors(self): error_texts = [] - if not self.view.strategy_widget.amount_input.value(): - error_texts.append("Order size 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(): @@ -186,20 +156,6 @@ def validation_errors(self): error_texts.append("Lower bound can't be 0") return error_texts - @property - def values(self): - data = { - 'mode': self.view.strategy_widget.mode_input.currentData(), - 'spread': self.view.strategy_widget.spread_input.value(), - 'center_price': self.view.strategy_widget.center_price_input.value(), - 'center_price_dynamic': self.view.strategy_widget.center_price_dynamic_checkbox.isChecked(), - '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(), - 'allow_instant_fill': self.view.strategy_widget.allow_instant_fill_checkbox.isChecked() - } - return data - @property def strategy_modes(self): # Todo: Activate rest of the modes once the logic is done @@ -215,5 +171,5 @@ def strategy_modes(self): @classmethod def strategy_modes_tuples(cls): - modes = cls(None, None, None).strategy_modes + modes = cls(None, [], None, {}).strategy_modes return [(key, value) for key, value in modes.items()] diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index d502bfc95..3b270bcaf 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -15,7 +15,7 @@ def configure(cls, return_base_config=True): return BaseStrategy.configure(return_base_config) + [ ConfigElement( 'strategy_mode', 'choice', 'mountain', - 'Choose strategy mode', StaggeredOrdersController.strategy_modes_tuples()), + 'Choose strategy mode', StaggeredOrdersController.strategy_modes_tuples(), (0, None, 0, '')), ConfigElement( 'spread', 'float', 6, 'Spread', 'The percentage difference between buy and sell', (0, None, 2, '%')), @@ -33,7 +33,10 @@ def configure(cls, return_base_config=True): 'The bottom price in the range', (0, None, 8, '')), ConfigElement( 'upper_bound', 'float', 1000000, 'Upper bound', - 'The top price in the range', (0, None, 8, '')) + 'The top price in the range', (0, None, 8, '')), + ConfigElement( + 'allow_instant_fill', 'bool', True, 'Allow instant fill', + 'Allows DEXBot to make orders, which instantly fill', None) ] def __init__(self, *args, **kwargs): @@ -58,6 +61,7 @@ def __init__(self, *args, **kwargs): self.increment = self.worker['increment'] / 100 self.upper_bound = self.worker['upper_bound'] self.lower_bound = self.worker['lower_bound'] + self.instant_fill = self.worker['allow_instant_fill'] # Order expiration time self.expiration = 60 * 60 * 24 * 365 * 5 diff --git a/dexbot/views/ui/forms/staggered_orders_widget.ui b/dexbot/views/ui/forms/staggered_orders_widget.ui index b4a9aa82c..3d19fd784 100644 --- a/dexbot/views/ui/forms/staggered_orders_widget.ui +++ b/dexbot/views/ui/forms/staggered_orders_widget.ui @@ -6,8 +6,8 @@ 0 0 - 364 - 300 + 439 + 372 @@ -32,17 +32,96 @@ Worker Parameters - - - - Mode + + + + + 110 + 0 + - - mode_input + + + 110 + 16777215 + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Order size + + + true + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Amount of sell/buy order + + + ? + + + 5 + + + + - + @@ -52,16 +131,16 @@ - 151 + 170 0 - - + + - + 0 0 @@ -78,15 +157,82 @@ 16777215 - - Spread - - - spread_input - + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Spread + + + true + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + The percentage difference between buy and sell + + + ? + + + 5 + + + + - + @@ -96,13 +242,16 @@ - 151 + 170 0 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + % @@ -114,10 +263,10 @@ - - + + - + 0 0 @@ -134,15 +283,82 @@ 16777215 - - Increment - - - spread_input - + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Increment + + + true + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + The percentage difference between each staggered order + + + ? + + + 5 + + + + - + @@ -152,13 +368,16 @@ - 151 + 170 0 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + % @@ -170,10 +389,10 @@ - - + + - + 0 0 @@ -190,15 +409,82 @@ 16777215 - - Lower bound - - - spread_input - + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Lower bound + + + true + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + The bottom price in the range + + + ? + + + 5 + + + + - + @@ -208,13 +494,16 @@ - 140 + 170 0 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + 8 @@ -226,10 +515,10 @@ - - + + - + 0 0 @@ -246,15 +535,79 @@ 16777215 - - Center Price - - - center_price_input - + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Center Price + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Fixed center price expressed in base asset: base/quote + + + ? + + + 5 + + + + - + false @@ -267,13 +620,16 @@ - 140 + 170 0 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + false @@ -291,20 +647,109 @@ - - + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Always calculate the middle from the closest market orders + + + ? + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 5 + + + + + + + + + + + 0 + 0 + + - Calculate center price dynamically + Update center price from closest market orders true - - + + - + 0 0 @@ -321,15 +766,79 @@ 16777215 - - Upper bound - - - spread_input - + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Upper bound + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + The top price in the range + + + ? + + + 5 + + + + - + @@ -339,13 +848,16 @@ - 140 + 170 0 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + 8 @@ -357,10 +869,99 @@ - - + + + + + 0 + 0 + + + + + 110 + 0 + + + + + 110 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Allow orders that fill instantly + + + ? + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 5 + + + + + + + + + + + 0 + 0 + + - Allow order instant fill + Allow instant fill true @@ -375,18 +976,18 @@ - center_price_dynamic_checkbox - clicked(bool) + center_price_dynamic_input + toggled(bool) center_price_input setDisabled(bool) - 252 - 165 + 281 + 272 - 208 - 136 + 217 + 236 From 6920233dd87fcef345bd045b4b4f96237c5a6831 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 30 Jul 2018 19:58:24 +0500 Subject: [PATCH 0463/1846] Pass num_retries to bitshares instance for cli Fixes exceptions during handling on_market callbacks. --- dexbot/ui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dexbot/ui.py b/dexbot/ui.py index 965a20286..2ce5ff70b 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -78,6 +78,7 @@ def chain(f): def new_func(ctx, *args, **kwargs): ctx.bitshares = BitShares( ctx.config["node"], + num_retries=-1, **ctx.obj ) set_shared_bitshares_instance(ctx.bitshares) From 16f375ec0b07616cfde730c39df5c812be4337f5 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 31 Jul 2018 09:45:03 +0300 Subject: [PATCH 0464/1846] Update UI help texts --- dexbot/controllers/strategy_controller.py | 1 - dexbot/strategies/staggered_orders.py | 5 +++-- dexbot/views/ui/forms/staggered_orders_widget.ui | 9 ++++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index d005f7fe9..bec4851dc 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -1,5 +1,4 @@ import collections -from decimal import * from dexbot.views.errors import gui_error diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 3b270bcaf..4bb12325d 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -15,7 +15,8 @@ def configure(cls, return_base_config=True): return BaseStrategy.configure(return_base_config) + [ ConfigElement( 'strategy_mode', 'choice', 'mountain', - 'Choose strategy mode', StaggeredOrdersController.strategy_modes_tuples(), (0, None, 0, '')), + 'How to allocate funds and profits. Doesn\'t effect existing orders, only future ones', + StaggeredOrdersController.strategy_modes_tuples(), (0, None, 0, '')), ConfigElement( 'spread', 'float', 6, 'Spread', 'The percentage difference between buy and sell', (0, None, 2, '%')), @@ -36,7 +37,7 @@ def configure(cls, return_base_config=True): 'The top price in the range', (0, None, 8, '')), ConfigElement( 'allow_instant_fill', 'bool', True, 'Allow instant fill', - 'Allows DEXBot to make orders, which instantly fill', None) + 'Allow bot to make orders which might fill immediately upon placement', None) ] def __init__(self, *args, **kwargs): diff --git a/dexbot/views/ui/forms/staggered_orders_widget.ui b/dexbot/views/ui/forms/staggered_orders_widget.ui index 3d19fd784..a5d3192ed 100644 --- a/dexbot/views/ui/forms/staggered_orders_widget.ui +++ b/dexbot/views/ui/forms/staggered_orders_widget.ui @@ -83,7 +83,7 @@ - Order size + Mode true @@ -108,7 +108,10 @@ WhatsThisCursor - Amount of sell/buy order + How to allocate funds and profits. Doesn't effect existing orders, only future ones + + + ? @@ -936,7 +939,7 @@ WhatsThisCursor - Allow orders that fill instantly + Allow bot to make orders which might fill immediately upon placement ? From 093976ea7fc723da9b9f9aa70469adda84653a92 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 31 Jul 2018 09:46:34 +0300 Subject: [PATCH 0465/1846] Modify total balance calculation function --- dexbot/basestrategy.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 6df843de1..fb9375201 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -194,7 +194,7 @@ def _calculate_center_price(self, suppress_errors=False): ticker = self.market.ticker() highest_bid = ticker.get("highestBid") lowest_ask = ticker.get("lowestAsk") - if not float(highest_bid): + if not highest_bid: if not suppress_errors: self.log.critical( "Cannot estimate center price, there is no highest bid." @@ -594,15 +594,23 @@ def total_balance(self, order_ids=None, return_asset=False): quote_asset = self.market['quote']['id'] base_asset = self.market['base']['id'] + # Total balance calculation 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 order_ids is None: + # Get all orders from Blockchain + order_ids = [] + + for order in self.orders: + order_ids.append(order['id']) + elif order_ids: + orders_balance = self.orders_balance(order_ids) + quote += orders_balance['quote'] + base += orders_balance['base'] if return_asset: quote = Amount(quote, quote_asset) From 3ebcd088119892e878c23920c92f28cc1c649a3e Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 31 Jul 2018 09:48:47 +0300 Subject: [PATCH 0466/1846] Add function to calculate value of the account --- dexbot/basestrategy.py | 45 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index fb9375201..715fea4b0 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -618,6 +618,51 @@ def total_balance(self, order_ids=None, return_asset=False): return {'quote': quote, 'base': base} + def asset_total_balance(self, return_asset): + """ Returns the whole value of the account as one asset only + :param str return_asset: Asset which is wanted as return + :return: float: Value of the account in one asset + """ + total_value = 0 + + # Total balance calculation + for balance in self.balances: + if balance.asset['symbol'] != return_asset: + # Convert to asset if different + total_value += self.convert_asset(balance['amount'], balance['symbol'], return_asset) + else: + total_value += balance['amount'] + + # Orders balance calculation + for order in self.all_orders: + updated_order = self.get_updated_order(order['id']) + + if not order: + continue + if updated_order['base']['symbol'] == return_asset: + total_value += updated_order['base']['amount'] + else: + total_value += self.convert_asset( + updated_order['quote']['amount'], + updated_order['quote']['symbol'], + return_asset + ) + + return total_value + + @staticmethod + def convert_asset(from_value, from_asset, to_asset): + """Converts asset to another based on the latest market value + :param from_value: Amount of the input asset + :param from_asset: Symbol of the input asset + :param to_asset: Symbol of the output asset + :return: Asset converted to another asset as float value + """ + market = Market('{}/{}'.format(from_asset, to_asset)) + ticker = market.ticker() + latest_price = ticker.get('latest', {}).get('price', None) + return from_value * latest_price + def orders_balance(self, order_ids, return_asset=False): if not order_ids: order_ids = [] From 1d7d9ea70a869a0170afc92d9cdeba6d9d3088ed Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 1 Aug 2018 09:30:36 +0300 Subject: [PATCH 0467/1846] Change dexbot version number to 0.5.3 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 0026037ac..37805ae44 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.5.2' +VERSION = '0.5.3' AUTHOR = 'Codaone Oy' __version__ = VERSION From 711a10c20ec6416e7ccf4c548c3896d5d87476fe Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 1 Aug 2018 15:31:43 +0300 Subject: [PATCH 0468/1846] Change private key input to optional value in the GUI --- dexbot/controllers/worker_controller.py | 142 +++++++++--------- dexbot/views/ui/create_worker_window.ui | 140 ++++++++++++----- dexbot/views/ui/edit_worker_window.ui | 20 +-- .../views/ui/forms/relative_orders_widget.ui | 28 ++-- .../views/ui/forms/staggered_orders_widget.ui | 44 ++++-- dexbot/views/worker_item.py | 3 - 6 files changed, 220 insertions(+), 157 deletions(-) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index ddff2b050..c48c8f2df 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -48,54 +48,6 @@ def base_assets(self): ] return assets - @staticmethod - def is_worker_name_valid(worker_name): - worker_names = Config().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: - Asset(asset, bitshares_instance=self.bitshares) - return True - except bitshares.exceptions.AssetDoesNotExistsException: - return False - - def account_exists(self, account): - try: - Account(account, bitshares_instance=self.bitshares) - return True - except bitshares.exceptions.AccountDoesNotExistsException: - 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) - except ValueError: - return False - - accounts = wallet.getAllAccounts(pubkey) - account_names = [account['name'] for account in accounts] - - if account in account_names: - return True - else: - return False - - @staticmethod - def is_account_in_use(account): - workers = Config().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: @@ -106,7 +58,8 @@ 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" + %n is the next available index """ index = 1 workers = Config().workers_data.keys() @@ -117,9 +70,6 @@ def get_unique_worker_name(): return worker_name - def get_strategy_name(self, module): - return self.strategies[module]['name'] - @staticmethod def get_strategy_module(worker_data): return worker_data['module'] @@ -159,28 +109,73 @@ def change_strategy_form(self, worker_data=None): self.view.setMinimumHeight(0) self.view.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) + @classmethod + def validate_worker_name(cls, worker_name, old_worker_name=None): + if old_worker_name != worker_name: + worker_names = Config().workers_data.keys() + # Check that the name is unique + if worker_name in worker_names: + return False return True + return True def validate_asset(self, asset): - return self.is_asset_valid(asset) + try: + Asset(asset, bitshares_instance=self.bitshares) + return True + except bitshares.exceptions.AssetDoesNotExistsException: + return False - def validate_market(self, base_asset, quote_asset): + @classmethod + def validate_market(cls, base_asset, quote_asset): return base_asset.lower() != quote_asset.lower() def validate_account_name(self, account): - return self.account_exists(account) + if not account: + return False + try: + Account(account, bitshares_instance=self.bitshares) + return True + except bitshares.exceptions.AccountDoesNotExistsException: + return False + + def validate_private_key(self, account, private_key): + wallet = self.bitshares.wallet + if not private_key: + # Check if the private key is already in the database + accounts = wallet.getAccounts() + if any(account == d['name'] for d in accounts): + return True + return False + + try: + pubkey = format(PrivateKey(private_key).pubkey, self.bitshares.prefix) + except ValueError: + return False + + accounts = wallet.getAllAccounts(pubkey) + account_names = [account['name'] for account in accounts] + + if account in account_names: + return True + else: + return False - def validate_account(self, account, private_key): - return self.is_account_valid(account, private_key) + def validate_private_key_type(self, account, private_key): + account = Account(account) + pubkey = format(PrivateKey(private_key).pubkey, self.bitshares.prefix) + key_type = self.bitshares.wallet.getKeyType(account, pubkey) + if key_type != 'active': + return False + return True - def validate_account_not_in_use(self, account): - return not self.is_account_in_use(account) + @classmethod + def validate_account_not_in_use(cls, account): + workers = Config().workers_data + for worker_name, worker in workers.items(): + if worker['account'] == account: + return False + return True @gui_error def validate_form(self): @@ -188,7 +183,11 @@ def validate_form(self): base_asset = self.view.base_asset_input.currentText() quote_asset = self.view.quote_asset_input.text() worker_name = self.view.worker_name_input.text() + old_worker_name = None if self.mode == 'add' else self.view.worker_name + if not self.validate_worker_name(worker_name, old_worker_name): + error_texts.append( + 'Worker name needs to be unique. "{}" is already in use.'.format(worker_name)) if not self.validate_asset(base_asset): error_texts.append('Field "Base Asset" does not have a valid asset.') if not self.validate_asset(quote_asset): @@ -198,17 +197,14 @@ def validate_form(self): 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_texts.append('Worker name needs to be unique. "{}" is already in use.'.format(worker_name)) if not self.validate_account_name(account): error_texts.append("Account doesn't exist.") - if not self.validate_account(account, private_key): - error_texts.append('Private key is invalid.') if not self.validate_account_not_in_use(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_texts.append('Worker name needs to be unique. "{}" is already in use.'.format(worker_name)) + if not self.validate_private_key(account, private_key): + error_texts.append('Private key is invalid.') + elif not self.validate_private_key_type(account, private_key): + error_texts.append('Please use active private key.') error_texts.extend(self.view.strategy_widget.strategy_controller.validation_errors()) error_text = '\n'.join(error_texts) @@ -231,7 +227,7 @@ def handle_save(self): self.add_private_key(private_key) account = self.view.account_input.text() - else: + else: # Edit account = self.view.account_name.text() base_asset = self.view.base_asset_input.currentText() diff --git a/dexbot/views/ui/create_worker_window.ui b/dexbot/views/ui/create_worker_window.ui index 023b0a227..4034e8a39 100644 --- a/dexbot/views/ui/create_worker_window.ui +++ b/dexbot/views/ui/create_worker_window.ui @@ -109,13 +109,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -130,54 +130,112 @@ - - + + - + 0 0 - - - 110 - 0 - - - - - 110 - 16777215 - - - - Private Active Key + + QLineEdit::Password - + false - - true - - - private_key_input - - - + + - + 0 0 - - QLineEdit::Password + + + 120 + 0 + - - false + + + 120 + 16777215 + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Private Active Key + + + true + + + quote_asset_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + <html><head/><body><p>Active private key of the account in WIF format.</p><p>This field is optional if it was previously saved.</p></body></html> + + + ? + + + 5 + + + + @@ -193,13 +251,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -222,13 +280,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -294,13 +352,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -380,13 +438,13 @@ - 110 + 120 0 - 110 + 120 16777215 diff --git a/dexbot/views/ui/edit_worker_window.ui b/dexbot/views/ui/edit_worker_window.ui index 31359790f..520994455 100644 --- a/dexbot/views/ui/edit_worker_window.ui +++ b/dexbot/views/ui/edit_worker_window.ui @@ -24,13 +24,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -53,13 +53,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -119,13 +119,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -205,13 +205,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -306,13 +306,13 @@ - 110 + 120 0 - 110 + 120 16777215 diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index 0e02741a3..a8345a88f 100644 --- a/dexbot/views/ui/forms/relative_orders_widget.ui +++ b/dexbot/views/ui/forms/relative_orders_widget.ui @@ -57,13 +57,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -180,13 +180,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -267,13 +267,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -402,13 +402,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -501,13 +501,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -625,13 +625,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -723,13 +723,13 @@ - 110 + 120 0 - 110 + 120 16777215 diff --git a/dexbot/views/ui/forms/staggered_orders_widget.ui b/dexbot/views/ui/forms/staggered_orders_widget.ui index 850db0ab9..102e15826 100644 --- a/dexbot/views/ui/forms/staggered_orders_widget.ui +++ b/dexbot/views/ui/forms/staggered_orders_widget.ui @@ -36,13 +36,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -165,13 +165,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -291,13 +291,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -417,13 +417,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -543,13 +543,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -651,13 +651,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -743,13 +743,13 @@ - 110 + 120 0 - 110 + 120 16777215 @@ -901,6 +901,18 @@ + + + 120 + 0 + + + + + 120 + 16777215 + + Required base @@ -917,13 +929,13 @@ - 110 + 120 0 - 110 + 120 16777215 diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 73d5afe92..064724520 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -139,10 +139,7 @@ def remove_widget_dialog(self): self.remove_widget() def remove_widget(self): - account = self.worker_config['workers'][self.worker_name]['account'] - self.main_ctrl.remove_worker(self.worker_name) - self.main_ctrl.bitshares_instance.wallet.removeAccount(account) self.view.remove_worker_widget(self.worker_name) self.main_ctrl.config.remove_worker_config(self.worker_name) self.deleteLater() From d24f10a6d70fb5656d28a99a4a2d3b52a6675937 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 1 Aug 2018 16:34:29 +0300 Subject: [PATCH 0469/1846] Update center price and total balance calculations --- dexbot/basestrategy.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 715fea4b0..68872e0bb 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -212,13 +212,13 @@ def _calculate_center_price(self, suppress_errors=False): center_price = highest_bid['price'] * math.sqrt(lowest_ask['price'] / highest_bid['price']) return center_price - def calculate_center_price(self, center_price=None, - asset_offset=False, spread=None, order_ids=None, manual_offset=0): + def calculate_center_price(self, center_price=None, asset_offset=False, spread=None, + order_ids=None, manual_offset=0, suppress_errors=False): """ Calculate center price which shifts based on available funds """ if center_price is None: # No center price was given so we simply calculate the center price - calculated_center_price = self._calculate_center_price() + calculated_center_price = self._calculate_center_price(suppress_errors) else: # Center price was given so we only use the calculated center price # for quote to base asset conversion @@ -627,7 +627,7 @@ def asset_total_balance(self, return_asset): # Total balance calculation for balance in self.balances: - if balance.asset['symbol'] != return_asset: + if balance['symbol'] != return_asset: # Convert to asset if different total_value += self.convert_asset(balance['amount'], balance['symbol'], return_asset) else: @@ -643,8 +643,8 @@ def asset_total_balance(self, return_asset): total_value += updated_order['base']['amount'] else: total_value += self.convert_asset( - updated_order['quote']['amount'], - updated_order['quote']['symbol'], + updated_order['base']['amount'], + updated_order['base']['symbol'], return_asset ) From 49c772eb4f020922e5702edcc81531b1a71713a4 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 1 Aug 2018 16:35:43 +0300 Subject: [PATCH 0470/1846] Change strategy modes location to strategy file --- dexbot/controllers/strategy_controller.py | 28 ++++------------------- dexbot/strategies/staggered_orders.py | 16 +++++++++---- 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index bec4851dc..e9864acd7 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -26,7 +26,7 @@ def set_values(self, configure, worker_config): else: value = option.default - element = self.elements[option.key] + element = self.elements.get(option.key) if not element: continue @@ -73,7 +73,9 @@ def elements(self): for option in self.configure: element_name = ''.join([option.key, '_input']) - elements[option.key] = self.view.findChild(types, element_name) + element = self.view.findChild(types, element_name) + if element: + elements[option.key] = element return elements @@ -129,10 +131,6 @@ def __init__(self, view, configure, worker_controller, worker_data): self.worker_controller = worker_controller if view: - modes = self.strategy_modes - for strategy_mode in modes: - self.view.strategy_widget.mode_input.addItem(modes[strategy_mode], strategy_mode) - if not self.view.strategy_widget.center_price_dynamic_input.isChecked(): self.view.strategy_widget.center_price_input.setDisabled(False) @@ -154,21 +152,3 @@ def validation_errors(self): if not self.view.strategy_widget.lower_bound_input.value(): error_texts.append("Lower bound can't be 0") return error_texts - - @property - def strategy_modes(self): - # Todo: Activate rest of the modes once the logic is done - modes = collections.OrderedDict() - - # modes['neutral'] = 'Neutral' - modes['mountain'] = 'Mountain' - # modes['valley'] = 'Valley' - # modes['buy_slope'] = 'Buy Slope' - # modes['sell_slope'] = 'Sell Slope' - - return modes - - @classmethod - def strategy_modes_tuples(cls): - modes = cls(None, [], None, {}).strategy_modes - return [(key, value) for key, value in modes.items()] diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 4bb12325d..4b8e38092 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -3,7 +3,6 @@ from datetime import timedelta from dexbot.basestrategy import BaseStrategy, ConfigElement -from dexbot.controllers.strategy_controller import StaggeredOrdersController from dexbot.qt_queue.idle_queue import idle_add @@ -12,11 +11,20 @@ class Strategy(BaseStrategy): @classmethod def configure(cls, return_base_config=True): + # Todo: - Modes don't list in worker add / edit + # Todo: - Add other modes + modes = [ + ('mountain', 'Mountain'), + # ('neutral', 'Neutral'), + # ('valley', 'Valley'), + # ('buy_slope', 'Buy Slope'), + # ('sell_slope', 'Sell Slope') + ] + return BaseStrategy.configure(return_base_config) + [ ConfigElement( - 'strategy_mode', 'choice', 'mountain', - 'How to allocate funds and profits. Doesn\'t effect existing orders, only future ones', - StaggeredOrdersController.strategy_modes_tuples(), (0, None, 0, '')), + 'mode', 'choice', 'mountain', 'Strategy mode', + 'How to allocate funds and profits. Doesn\'t effect existing orders, only future ones', modes), ConfigElement( 'spread', 'float', 6, 'Spread', 'The percentage difference between buy and sell', (0, None, 2, '%')), From 4139c720e76b08abd078f4519c893a2b4b88487d Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 1 Aug 2018 16:37:41 +0300 Subject: [PATCH 0471/1846] WIP Change staggered orders logic --- dexbot/strategies/staggered_orders.py | 216 +++++++++++++++++++++++--- 1 file changed, 191 insertions(+), 25 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 4b8e38092..3136fd2ad 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -55,8 +55,8 @@ def __init__(self, *args, **kwargs): self.counter = 0 # Define callbacks - self.onMarketUpdate += self.maintain_strategy() - self.onAccount += self.maintain_strategy() + self.onMarketUpdate += self.maintain_strategy + self.onAccount += self.maintain_strategy self.ontick += self.tick self.error_ontick = self.error self.error_onMarketUpdate = self.error @@ -72,6 +72,10 @@ def __init__(self, *args, **kwargs): self.lower_bound = self.worker['lower_bound'] self.instant_fill = self.worker['allow_instant_fill'] + # Strategy variables + self.buy_orders = [] + self.sell_orders = [] + # Order expiration time self.expiration = 60 * 60 * 24 * 365 * 5 self.last_check = datetime.now() @@ -79,29 +83,46 @@ def __init__(self, *args, **kwargs): if self.view: self.update_gui_slider() - def maintain_strategy(self): - """ Logic of the strategy """ + def maintain_strategy(self, *args, **kwargs): + """ Logic of the strategy + :param args: Order which was added after the bot was started and if there was no market center price + :param kwargs: + :return: + """ + + # Calculate market center price + market_center_price = self.calculate_center_price(suppress_errors=True) + + # Loop until center price appears on the market + if not market_center_price: + return + # Get orders orders = self.orders - # Calculate market center price - market_center_price = self.calculate_center_price() + # Sort buy and sell orders + self.buy_orders = self.get_buy_orders('DESC', orders) + self.sell_orders = self.get_sell_orders('DESC', orders) - # Get sorted orders - buy_orders = self.get_buy_orders('DESC', orders) - sell_orders = self.get_sell_orders('DESC', orders) + # Get highest buy and lowest sell prices from orders + if self.buy_orders: + highest_buy_order = self.buy_orders[0] - # Highest buy and lowest sell prices - highest_buy_price = buy_orders[0] - lowest_sell_price = sell_orders[-1] + if self.sell_orders: + lowest_sell_order = self.sell_orders[-1] # Get account balances - base_asset_balance = self.balance(self.market['base']['symbol']) - quote_asset_balance = self.balance(self.market['quote']['symbol']) + account_balances = self.total_balance(order_ids=[], return_asset=True) + + base_balance = account_balances['base'] + quote_balance = account_balances['quote'] + + total_value_base = self.asset_total_balance(base_balance['symbol']) + total_value_quote = self.asset_total_balance(quote_balance['symbol']) # Calculate asset thresholds - base_asset_threshold = base_asset_balance / 20000 - quote_asset_threshold = quote_asset_balance / 20000 + base_asset_threshold = total_value_base / 20000 + quote_asset_threshold = total_value_quote / 20000 # Check boundaries if market_center_price > self.upper_bound: @@ -110,19 +131,164 @@ def maintain_strategy(self): self.lower_bound = market_center_price # Base asset check - # Todo: Check the logic - if base_asset_balance > base_asset_threshold: - self.allocate_base() + if total_value_base > base_asset_threshold: + self.allocate_base_asset() else: - if market_center_price > highest_buy_price * (1 + self.spread): - self.shift_orders_up() + if market_center_price > highest_buy_order['base']['amount'] * (1 + self.spread): + # Cancel lowest buy order + self.shift_orders_up(self.buy_orders[-0]) - # Check which mode is in use - if self.mode == 'mountain': - self.maintain_mountain_mode() + # Quote asset check + if total_value_quote > quote_asset_threshold: + self.allocate_quote_asset() + else: + if market_center_price < lowest_sell_order['base']['amount'] * (1 - self.spread): + # Cancel highest sell order + self.shift_orders_down(self.sell_orders[0]) def maintain_mountain_mode(self): - """ Mountain mode """ + """ Mountain mode + This structure is not final, but an idea was that each mode has separate function which runs the loop. + """ + # Todo: Work in progress + pass + + def allocate_base_asset(self): + """ Allocates base asset + + :return: + + bid = market_buy_order + ask = market_sell_order + buy = own_buy_order + sell = own_sell_order + + Mountain mode + Lowest sell price = cp * spread / 2 + Next sell price = previous price + increment + Lowest sell amount = Balance * increment + Next sell amount = last amount + increment + + """ + # Todo: Work in progress, this is based on the strategy diagram + # Placeholders for now + bid = 0 + order_size_correct = True + actual_spread = 0 + + if bid: + if order_size_correct: + # Todo: Make order size check function + if self.instant_fill: + if actual_spread >= self.spread + self.increment: + self.place_higher_buy_order(self.buy_orders[0]) + return + else: + if self.highest_buy + self.increment < self.lowest_ask: + pass + else: + # Todo: what order should be canceled? + self.cancel() + else: + self.place_lowest_bid() + + def allocate_quote_asset(self): + """ Allocates quote asset + """ + # Todo: Work in progress + pass + + def shift_orders_up(self, order): + """ Removes lowest buy order and places higher buy order + :param order: Lowest buy order + :return: + """ + self.cancel(order) + self.place_higher_buy_order(order) + + def shift_orders_down(self, order): + """ Removes highest sell order and places lower sell order + :param order: Highest sell order + :return: + """ + self.cancel(order) + self.place_lower_sell_order(order) + + def place_higher_buy_order(self, order): + """ Place higher buy order + + amount (QUOTE) = lower_buy_order_amount * (1 + increment) + price (BASE) = lower_buy_order_price * (1 + increment) + + :param order: Previously highest buy order + :return: + """ + amount = order['quote']['amount'] * (1 + self.increment) + price = order['base']['price'] * (1 + self.increment) + + self.market_buy(amount, price) + + def place_higher_sell_order(self, order): + """ Place higher sell order + + amount (QUOTE) = higher_sell_order_amount / (1 + increment) + price (BASE) = higher_sell_order_price * (1 + increment) + + :param order: highest_sell_order + :return: + """ + amount = order['quote']['amount'] / (1 + self.increment) + price = order['base']['price'] * (1 + self.increment) + + self.market_sell(amount, price) + + def place_lower_buy_order(self, order): + """ Place lower buy order + + amount (QUOTE) = lowest_buy_order_amount / (1 + increment) + price (BASE) = lowest_buy_order_price / (1 + increment) + + :param order: Previously lowest buy order + :return: + """ + amount = order['quote']['amount'] / (1 + self.increment) + price = order['base']['price'] / (1 + self.increment) + + self.market_buy(amount, price) + + def place_lower_sell_order(self, order): + """ Place lower sell order + + amount (QUOTE) = higher_sell_order_amount * (1 + increment) + price (BASE) = higher_sell_order_price / (1 + increment) + + :param order: Previously higher sell order + :return: + """ + amount = order['quote']['amount'] * (1 + self.increment) + price = order['base']['price'] / (1 + self.increment) + + self.market_sell(amount, price) + + def place_lowest_bid(self): + """ + :return: + """ + # Todo: Work in progress + pass + + def place_highest_ask(self): + """ + :return: + """ + # Todo: Work in progress + pass + + def error(self, *args, **kwargs): + self.disabled = True + + def pause(self): + """ Override pause() in BaseStrategy """ pass def tick(self, d): From d6caabc1a54eea6ac4a01a99a287ed74ed79b5a0 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 2 Aug 2018 09:47:16 +0300 Subject: [PATCH 0472/1846] Fix element fetching logic in strategy_controller --- dexbot/controllers/strategy_controller.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 70cc9e0c2..4b702b454 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -28,8 +28,8 @@ def set_values(self, configure, worker_config): else: value = option.default - element = self.elements[option.key] - if not element: + element = self.elements.get(option.key) + if element is None: continue if option.type in ('int', 'float', 'string'): @@ -62,7 +62,7 @@ def values(self): @property def elements(self): - """ Use ConfigElement of the strategy to find the elements + """ Use ConfigElements of the strategy to find the elements """ elements = {} types = ( @@ -75,7 +75,9 @@ def elements(self): for option in self.configure: element_name = ''.join([option.key, '_input']) - elements[option.key] = self.view.findChild(types, element_name) + element = self.view.findChild(types, element_name) + if element is not None: + elements[option.key] = element return elements From 062ca033150299d30617225aec82007252d93c45 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 2 Aug 2018 09:47:16 +0300 Subject: [PATCH 0473/1846] Fix element fetching logic in strategy_controller --- dexbot/controllers/strategy_controller.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index e9864acd7..4a4c42cc3 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -27,7 +27,8 @@ def set_values(self, configure, worker_config): value = option.default element = self.elements.get(option.key) - if not element: + + if element is None: continue if option.type in ('int', 'float', 'string'): @@ -60,7 +61,7 @@ def values(self): @property def elements(self): - """ Use ConfigElement of the strategy to find the elements + """ Use ConfigElements of the strategy to find the elements """ elements = {} types = ( @@ -74,7 +75,7 @@ def elements(self): for option in self.configure: element_name = ''.join([option.key, '_input']) element = self.view.findChild(types, element_name) - if element: + if element is not None: elements[option.key] = element return elements From fe94bc7784f96e756a3b30b1e1d02eef2e33fa6d Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 2 Aug 2018 12:27:00 +0300 Subject: [PATCH 0474/1846] Add attribute access to AutoStrategyFormGenerator elements --- dexbot/controllers/strategy_controller.py | 2 +- dexbot/views/strategy_form.py | 78 ++++++++++++++--------- 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 4b702b454..c4ff42275 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -62,7 +62,7 @@ def values(self): @property def elements(self): - """ Use ConfigElements of the strategy to find the elements + """ Use ConfigElements of the strategy to find the input elements """ elements = {} types = ( diff --git a/dexbot/views/strategy_form.py b/dexbot/views/strategy_form.py index 7ce45ca76..c6f5666ce 100644 --- a/dexbot/views/strategy_form.py +++ b/dexbot/views/strategy_form.py @@ -27,7 +27,7 @@ def __init__(self, controller, strategy_module, worker_config=None): self.strategy_widget.setupUi(self) except (ValueError, AttributeError): # Generate the strategy form widget automatically - self.strategy_widget = AutoStrategyFormGenerator(self, configure, worker_config) + self.strategy_widget = AutoStrategyFormGenerator(self, configure) # Assemble the controller class name parts = self.module_name.split('_') @@ -51,7 +51,7 @@ def __init__(self, controller, strategy_module, worker_config=None): @property def values(self): - """ Returns all the form values based on selected strategy + """ Returns all the form values based on the selected strategy """ return self.strategy_controller.values @@ -60,7 +60,8 @@ class AutoStrategyFormGenerator: """ Automatic strategy form UI generator """ - def __init__(self, view, configure, worker_config): + def __init__(self, view, configure): + self.view = view self.index = 0 self.elements = {} @@ -75,29 +76,40 @@ def __init__(self, view, configure, worker_config): for option in configure: self.add_element(option) + def __getattr__(self, item): + element = self.view.findChild(QtWidgets.QWidget, item) + if element is None: + raise AttributeError + return element + def add_element(self, option): + key = option.key + element_type = option.type + title = option.title + description = option.description extra = option.extra - if option.type == 'float': + + if element_type == 'float': element = self.add_double_spin_box( - option.title, extra[0], extra[1], extra[2], extra[3], option.description) - elif option.type == 'int': + key, title, extra[0], extra[1], extra[2], extra[3], description) + elif element_type == 'int': element = self.add_spin_box( - option.title, extra[0], extra[1], extra[2], option.description) - elif option.type == 'string': - element = self.add_line_edit(option.title, option.description) - elif option.type == 'bool': - element = self.add_checkbox(option.title, option.description) - elif option.type == 'choice': - element = self.add_combo_box(option.title, option.description) + key, title, extra[0], extra[1], extra[2], description) + elif element_type == 'string': + element = self.add_line_edit(key, title, description) + elif element_type == 'bool': + element = self.add_checkbox(key, title, description) + elif element_type == 'choice': + element = self.add_combo_box(key, title, description) else: return - element_name = ''.join([option.key, '_input']) + element_name = ''.join([key, '_input']) element.setObjectName(element_name) + self.elements[key] = element self.index += 1 - self.elements[option.key] = element - def _add_label_wrap(self): + def _add_label_wrap(self, key): wrap = QtWidgets.QWidget(self.group_box) wrap.setMinimumSize(QtCore.QSize(110, 0)) wrap.setMaximumSize(QtCore.QSize(110, 16777215)) @@ -106,10 +118,12 @@ def _add_label_wrap(self): layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) + element_name = ''.join([key, '_wrap']) + wrap.setObjectName(element_name) self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.LabelRole, wrap) return wrap - def _add_tooltip(self, tooltip_text, container): + def _add_tooltip(self, key, tooltip_text, container): tooltip = QtWidgets.QLabel(container) font = QtGui.QFont() font.setBold(True) @@ -122,26 +136,30 @@ def _add_tooltip(self, tooltip_text, container): tooltip.setText('?') tooltip.setToolTip(tooltip_text) + element_name = ''.join([key, '_tooltip']) + tooltip.setObjectName(element_name) layout = container.layout() layout.addWidget(tooltip) - def add_label(self, text, description=''): - wrap = self._add_label_wrap() + def add_label(self, key, text, description=''): + wrap = self._add_label_wrap(key) label = QtWidgets.QLabel(wrap) label.setWordWrap(True) size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) label.setSizePolicy(size_policy) + element_name = ''.join([key, '_label']) + label.setObjectName(element_name) layout = wrap.layout() layout.addWidget(label) label.setText(text) if description: - self._add_tooltip(description, wrap) + self._add_tooltip(key, description, wrap) - def add_double_spin_box(self, text, minimum, maximum, precision, suffix='', description=''): - self.add_label(text, description) + def add_double_spin_box(self, key, text, minimum, maximum, precision, suffix='', description=''): + self.add_label(key, text, description) input_field = QtWidgets.QDoubleSpinBox(self.group_box) input_field.setDecimals(precision) @@ -161,8 +179,8 @@ def add_double_spin_box(self, text, minimum, maximum, precision, suffix='', desc self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) return input_field - def add_spin_box(self, text, minimum, maximum, suffix='', description=''): - self.add_label(text, description) + def add_spin_box(self, key, text, minimum, maximum, suffix='', description=''): + self.add_label(key, text, description) input_field = QtWidgets.QSpinBox(self.group_box) if minimum is not None: @@ -180,15 +198,15 @@ def add_spin_box(self, text, minimum, maximum, suffix='', description=''): self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) return input_field - def add_line_edit(self, text, description=''): - self.add_label(text, description) + def add_line_edit(self, key, text, description=''): + self.add_label(key, text, description) input_field = QtWidgets.QLineEdit(self.group_box) self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) return input_field - def add_checkbox(self, text, description=''): - self.add_label('', description) + def add_checkbox(self, key, text, description=''): + self.add_label(key, '', description) input_field = QtWidgets.QCheckBox(self.group_box) input_field.setText(text) @@ -196,8 +214,8 @@ def add_checkbox(self, text, description=''): self.form_layout.setWidget(self.index, QtWidgets.QFormLayout.FieldRole, input_field) return input_field - def add_combo_box(self, text, description=''): - self.add_label(text, description) + def add_combo_box(self, key, text, description=''): + self.add_label(key, text, description) input_field = QtWidgets.QComboBox(self.group_box) size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) From 2040f2e0e17aec09f078ac1ca2cb25f71523cbba Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 2 Aug 2018 12:53:19 +0300 Subject: [PATCH 0475/1846] Fix form_module key requirement for strategies property --- dexbot/views/strategy_form.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/views/strategy_form.py b/dexbot/views/strategy_form.py index c6f5666ce..77daa703d 100644 --- a/dexbot/views/strategy_form.py +++ b/dexbot/views/strategy_form.py @@ -17,7 +17,7 @@ def __init__(self, controller, strategy_module, worker_config=None): 'Strategy' ) configure = strategy_class.configure(False) - form_module = controller.strategies[strategy_module]['form_module'] + form_module = controller.strategies[strategy_module].get('form_module') try: widget = getattr( importlib.import_module(form_module), From 8ac74e39b583868de3b9a2fa6e10e9836937a7a2 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 2 Aug 2018 15:30:43 +0300 Subject: [PATCH 0476/1846] WIP Modify staggered orders --- dexbot/strategies/staggered_orders.py | 125 +++++++++++++------------- 1 file changed, 61 insertions(+), 64 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 3136fd2ad..2b3085c98 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -73,6 +73,7 @@ def __init__(self, *args, **kwargs): self.instant_fill = self.worker['allow_instant_fill'] # Strategy variables + self.market_center_price = None self.buy_orders = [] self.sell_orders = [] @@ -91,10 +92,10 @@ def maintain_strategy(self, *args, **kwargs): """ # Calculate market center price - market_center_price = self.calculate_center_price(suppress_errors=True) + self.market_center_price = self.calculate_center_price(suppress_errors=True) # Loop until center price appears on the market - if not market_center_price: + if not self.market_center_price: return # Get orders @@ -125,26 +126,24 @@ def maintain_strategy(self, *args, **kwargs): quote_asset_threshold = total_value_quote / 20000 # Check boundaries - if market_center_price > self.upper_bound: - self.upper_bound = market_center_price - elif market_center_price < self.lower_bound: - self.lower_bound = market_center_price + if self.market_center_price > self.upper_bound: + self.upper_bound = self.market_center_price + elif self.market_center_price < self.lower_bound: + self.lower_bound = self.market_center_price # Base asset check - if total_value_base > base_asset_threshold: - self.allocate_base_asset() - else: - if market_center_price > highest_buy_order['base']['amount'] * (1 + self.spread): - # Cancel lowest buy order - self.shift_orders_up(self.buy_orders[-0]) + if base_balance > base_asset_threshold: + self.allocate_base_asset(base_balance) + elif self.market_center_price > highest_buy_order['base']['amount'] * (1 + self.spread): + # Cancel lowest buy order + self.shift_orders_up(self.buy_orders[-0]) # Quote asset check - if total_value_quote > quote_asset_threshold: - self.allocate_quote_asset() - else: - if market_center_price < lowest_sell_order['base']['amount'] * (1 - self.spread): - # Cancel highest sell order - self.shift_orders_down(self.sell_orders[0]) + if quote_balance > quote_asset_threshold: + self.allocate_quote_asset(quote_balance) + elif self.market_center_price < lowest_sell_order['base']['amount'] * (1 - self.spread): + # Cancel highest sell order + self.shift_orders_down(self.sell_orders[0]) def maintain_mountain_mode(self): """ Mountain mode @@ -153,49 +152,43 @@ def maintain_mountain_mode(self): # Todo: Work in progress pass - def allocate_base_asset(self): + def allocate_base_asset(self, base_balance, *args, **kwargs): """ Allocates base asset - + :param base_balance: Amount of the base asset available to use :return: - - bid = market_buy_order - ask = market_sell_order - buy = own_buy_order - sell = own_sell_order - - Mountain mode - Lowest sell price = cp * spread / 2 - Next sell price = previous price + increment - Lowest sell amount = Balance * increment - Next sell amount = last amount + increment - """ - # Todo: Work in progress, this is based on the strategy diagram - # Placeholders for now - bid = 0 - order_size_correct = True - actual_spread = 0 - - if bid: - if order_size_correct: - # Todo: Make order size check function - if self.instant_fill: - if actual_spread >= self.spread + self.increment: - self.place_higher_buy_order(self.buy_orders[0]) - return - else: - if self.highest_buy + self.increment < self.lowest_ask: - pass + # Todo: Work in progress + if self.buy_orders: + # Todo: Make order size check function + if self.order_size_correct(): + pass + # if self.instant_fill: + # if actual_spread >= self.spread + self.increment: + # self.place_higher_buy_order(self.own_buy_orders[0]) + # return + # else: + # if self.highest_buy + self.increment < self.lowest_ask: + # pass else: - # Todo: what order should be canceled? - self.cancel() + self.cancel(self.buy_orders[0]) else: - self.place_lowest_bid() + self.place_highest_buy_order(base_balance) - def allocate_quote_asset(self): + def allocate_quote_asset(self, quote_balance, *args, **kwargs): """ Allocates quote asset """ # Todo: Work in progress + if self.sell_orders: + pass + else: + self.place_lowest_sell_order(quote_balance) + + def is_order_size_correct(self): + """ Checks if the order size is correct + + :return: + """ + # Todo: Work in progress pass def shift_orders_up(self, order): @@ -223,7 +216,7 @@ def place_higher_buy_order(self, order): :param order: Previously highest buy order :return: """ - amount = order['quote']['amount'] * (1 + self.increment) + amount = order['quote']['amount'] price = order['base']['price'] * (1 + self.increment) self.market_buy(amount, price) @@ -242,6 +235,15 @@ def place_higher_sell_order(self, order): self.market_sell(amount, price) + def place_highest_buy_order(self, base_balance): + """ Places buy order closest to the market center price + :param base_balance: Available BASE asset balance + """ + amount = base_balance['amount'] * self.increment + price = self.market_center_price / math.sqrt(1 + self.spread) + + self.market_buy(amount, price) + def place_lower_buy_order(self, order): """ Place lower buy order @@ -270,19 +272,14 @@ def place_lower_sell_order(self, order): self.market_sell(amount, price) - def place_lowest_bid(self): - """ - :return: + def place_lowest_sell_order(self, quote_balance): + """ Places sell order closest to the market center price + :param quote_balance: Available QUOTE asset balance """ - # Todo: Work in progress - pass + amount = quote_balance['amount'] * self.increment + price = self.market_center_price * math.sqrt(1 + self.spread) - def place_highest_ask(self): - """ - :return: - """ - # Todo: Work in progress - pass + self.market_sell(amount, price) def error(self, *args, **kwargs): self.disabled = True From c8f59c658cb69d1892b08761ea00bfcf311b77c1 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 3 Aug 2018 12:32:31 +0300 Subject: [PATCH 0477/1846] Add fee asset option for workers --- dexbot/basestrategy.py | 22 ++++- dexbot/controllers/worker_controller.py | 5 + dexbot/gui.py | 8 +- dexbot/views/edit_worker.py | 7 +- dexbot/views/ui/create_worker_window.ui | 116 +++++++++++++++++++++++- dexbot/views/ui/edit_worker_window.ui | 116 +++++++++++++++++++++++- 6 files changed, 263 insertions(+), 11 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 4c0d36e81..6518f1360 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -14,6 +14,7 @@ import bitsharesapi.exceptions import bitshares.exceptions from bitshares.amount import Amount +from bitshares.amount import Asset from bitshares.market import Market from bitshares.account import Account from bitshares.price import FilledOrder, Order, UpdateCallOrder @@ -103,7 +104,9 @@ def configure(cls, return_base_config=True): ConfigElement("account", "string", "", "Account", "BitShares account name for the bot to operate with", ""), ConfigElement("market", "string", "USD:BTS", "Market", "BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"", - r"[A-Z\.]+[:\/][A-Z\.]+") + r"[A-Z\.]+[:\/][A-Z\.]+"), + ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset', 'Asset to be used to pay transaction fees', + r'[A-Z\.]+') ] if return_base_config: return base_config @@ -170,6 +173,16 @@ def __init__( # Recheck flag - Tell the strategy to check for updated orders self.recheck_orders = False + # Set fee asset + fee_asset_symbol = self.worker.get('fee_asset') + if fee_asset_symbol: + try: + self.fee_asset = Asset(fee_asset_symbol) + except bitshares.exceptions.AssetDoesNotExistsException: + self.fee_asset = Asset('1.3.0') + else: + self.fee_asset = Asset('1.3.0') + # Settings for bitshares instance self.bitshares.bundle = bool(self.worker.get("bundle", False)) @@ -368,7 +381,10 @@ def execute(self): def _cancel(self, orders): try: - self.retry_action(self.bitshares.cancel, orders, account=self.account) + self.retry_action( + self.bitshares.cancel, + orders, account=self.account, fee_asset=self.fee_asset + ) 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 @@ -436,6 +452,7 @@ def market_buy(self, amount, price, return_none=False, *args, **kwargs): Amount(amount=amount, asset=self.market["quote"]), account=self.account.name, returnOrderId="head", + fee_asset=self.fee_asset, *args, **kwargs ) @@ -475,6 +492,7 @@ def market_sell(self, amount, price, return_none=False, *args, **kwargs): Amount(amount=amount, asset=self.market["quote"]), account=self.account.name, returnOrderId="head", + fee_asset=self.fee_asset, *args, **kwargs ) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index ddff2b050..454b9254e 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -187,12 +187,15 @@ def validate_form(self): error_texts = [] base_asset = self.view.base_asset_input.currentText() quote_asset = self.view.quote_asset_input.text() + fee_asset = self.view.fee_asset_input.text() worker_name = self.view.worker_name_input.text() if not self.validate_asset(base_asset): error_texts.append('Field "Base Asset" does not have a valid asset.') if not self.validate_asset(quote_asset): error_texts.append('Field "Quote Asset" does not have a valid asset.') + if not self.validate_asset(fee_asset): + error_texts.append('Field "Fee Asset" does not have a valid asset.') if not self.validate_market(base_asset, quote_asset): error_texts.append("Market {}/{} doesn't exist.".format(base_asset, quote_asset)) if self.mode == 'add': @@ -236,12 +239,14 @@ def handle_save(self): base_asset = self.view.base_asset_input.currentText() quote_asset = self.view.quote_asset_input.text() + fee_asset = self.view.fee_asset_input.text() strategy_module = self.view.strategy_input.currentData() self.view.worker_data = { 'account': account, 'market': '{}/{}'.format(quote_asset, base_asset), 'module': strategy_module, + 'fee_asset': fee_asset, **self.view.strategy_widget.values } self.view.worker_name = self.view.worker_name_input.text() diff --git a/dexbot/gui.py b/dexbot/gui.py index e54ddd1b0..d1543f70b 100644 --- a/dexbot/gui.py +++ b/dexbot/gui.py @@ -1,8 +1,5 @@ import sys -from PyQt5 import Qt -from bitshares import BitShares - from dexbot.config import Config from dexbot.controllers.main_controller import MainController from dexbot.views.worker_list import MainView @@ -10,8 +7,11 @@ from dexbot.views.unlock_wallet import UnlockWalletView from dexbot.views.create_wallet import CreateWalletView +from PyQt5.Qt import QApplication +from bitshares import BitShares + -class App(Qt.QApplication): +class App(QApplication): def __init__(self, sys_argv): super(App, self).__init__(sys_argv) diff --git a/dexbot/views/edit_worker.py b/dexbot/views/edit_worker.py index c48b442c5..47d75b9ef 100644 --- a/dexbot/views/edit_worker.py +++ b/dexbot/views/edit_worker.py @@ -17,8 +17,6 @@ def __init__(self, parent_widget, bitshares_instance, worker_name, config): self.setupUi(self) worker_data = config['workers'][worker_name] - validator = UppercaseValidator(self) - # Todo: Using a model here would be more Qt like # Populate the comboboxes strategies = self.controller.strategies @@ -32,11 +30,14 @@ def __init__(self, parent_widget, bitshares_instance, worker_name, config): 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.fee_asset_input.setText(worker_data.get('fee_asset', 'BTS')) self.account_name.setText(self.controller.get_account(worker_data)) - # Validating assets fields + # Force uppercase to the assets fields + validator = UppercaseValidator(self) self.base_asset_input.setValidator(validator) self.quote_asset_input.setValidator(validator) + self.fee_asset_input.setValidator(validator) # Set signals self.strategy_input.currentTextChanged.connect(lambda: controller.change_strategy_form()) diff --git a/dexbot/views/ui/create_worker_window.ui b/dexbot/views/ui/create_worker_window.ui index 023b0a227..5c3a37e64 100644 --- a/dexbot/views/ui/create_worker_window.ui +++ b/dexbot/views/ui/create_worker_window.ui @@ -7,7 +7,7 @@ 0 0 428 - 345 + 378 @@ -459,6 +459,120 @@ + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Fee asset + + + quote_asset_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Asset to be used to pay transaction fees + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 145 + 0 + + + + + 80 + 16777215 + + + + BTS + + + diff --git a/dexbot/views/ui/edit_worker_window.ui b/dexbot/views/ui/edit_worker_window.ui index 31359790f..19c326fe7 100644 --- a/dexbot/views/ui/edit_worker_window.ui +++ b/dexbot/views/ui/edit_worker_window.ui @@ -7,7 +7,7 @@ 0 0 428 - 314 + 347 @@ -284,6 +284,120 @@ + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Fee asset + + + quote_asset_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Asset to be used to pay transaction fees + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 145 + 0 + + + + + 80 + 16777215 + + + + BTS + + + From a3c4b4920de3be538598e509367ea07f033920f8 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 3 Aug 2018 13:16:43 +0300 Subject: [PATCH 0478/1846] Change fee asset wrap width --- dexbot/views/ui/create_worker_window.ui | 4 ++-- dexbot/views/ui/edit_worker_window.ui | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/views/ui/create_worker_window.ui b/dexbot/views/ui/create_worker_window.ui index 5c3a37e64..7aa606f5d 100644 --- a/dexbot/views/ui/create_worker_window.ui +++ b/dexbot/views/ui/create_worker_window.ui @@ -469,13 +469,13 @@ - 120 + 110 0 - 120 + 110 16777215 diff --git a/dexbot/views/ui/edit_worker_window.ui b/dexbot/views/ui/edit_worker_window.ui index 19c326fe7..acb6ebb10 100644 --- a/dexbot/views/ui/edit_worker_window.ui +++ b/dexbot/views/ui/edit_worker_window.ui @@ -294,13 +294,13 @@ - 120 + 110 0 - 120 + 110 16777215 From 00c16f96a1e9cbff51133f4f191778275e9ad4ef Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 3 Aug 2018 13:18:18 +0300 Subject: [PATCH 0479/1846] Change dexbot version number to 0.5.4 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 37805ae44..662840898 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.5.3' +VERSION = '0.5.4' AUTHOR = 'Codaone Oy' __version__ = VERSION From 6992850bdc27059f71c9c1ea2e4d2a332bb0be60 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 3 Aug 2018 13:26:23 +0300 Subject: [PATCH 0480/1846] Change fee asset label width in the GUI --- dexbot/views/ui/create_worker_window.ui | 4 ++-- dexbot/views/ui/edit_worker_window.ui | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/views/ui/create_worker_window.ui b/dexbot/views/ui/create_worker_window.ui index 0b84b358b..aebc21c62 100644 --- a/dexbot/views/ui/create_worker_window.ui +++ b/dexbot/views/ui/create_worker_window.ui @@ -527,13 +527,13 @@ - 110 + 120 0 - 110 + 120 16777215 diff --git a/dexbot/views/ui/edit_worker_window.ui b/dexbot/views/ui/edit_worker_window.ui index ec7df6aca..3767b532c 100644 --- a/dexbot/views/ui/edit_worker_window.ui +++ b/dexbot/views/ui/edit_worker_window.ui @@ -294,13 +294,13 @@ - 110 + 120 0 - 110 + 120 16777215 From 0357ec8dd9c990cd646a3cf8f97705e34ba328bc Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 3 Aug 2018 13:29:57 +0300 Subject: [PATCH 0481/1846] Fix fee asset tab order in the GUI --- dexbot/views/ui/create_worker_window.ui | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dexbot/views/ui/create_worker_window.ui b/dexbot/views/ui/create_worker_window.ui index aebc21c62..30b92a565 100644 --- a/dexbot/views/ui/create_worker_window.ui +++ b/dexbot/views/ui/create_worker_window.ui @@ -659,10 +659,11 @@ worker_name_input base_asset_input quote_asset_input + fee_asset_input account_input private_key_input - save_button cancel_button + save_button From 76c933cc66cea7cb84c713e56c7e8f08dad6e196 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 3 Aug 2018 13:31:32 +0300 Subject: [PATCH 0482/1846] Fix fee asset uppercase force validator --- dexbot/views/create_worker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/views/create_worker.py b/dexbot/views/create_worker.py index 39d488991..a42a99dcf 100644 --- a/dexbot/views/create_worker.py +++ b/dexbot/views/create_worker.py @@ -14,8 +14,6 @@ def __init__(self, bitshares_instance): self.setupUi(self) - validator = UppercaseValidator(self) - # Todo: Using a model here would be more Qt like # Populate the comboboxes strategies = self.controller.strategies @@ -27,9 +25,11 @@ def __init__(self, bitshares_instance): self.worker_name = controller.get_unique_worker_name() self.worker_name_input.setText(self.worker_name) - # Validating assets fields + # Force uppercase to the assets fields + validator = UppercaseValidator(self) self.base_asset_input.setValidator(validator) self.quote_asset_input.setValidator(validator) + self.fee_asset_input.setValidator(validator) # Set signals self.strategy_input.currentTextChanged.connect(lambda: controller.change_strategy_form()) From 0562959ebf07710ebf1f606b5000e2205826dc9d Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 3 Aug 2018 13:40:49 +0300 Subject: [PATCH 0483/1846] Fix private key validation --- dexbot/controllers/worker_controller.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index ea81a6457..04e97674a 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -206,7 +206,7 @@ def validate_form(self): error_texts.append('Use a different account. "{}" is already in use.'.format(account)) if not self.validate_private_key(account, private_key): error_texts.append('Private key is invalid.') - elif not self.validate_private_key_type(account, private_key): + elif private_key and not self.validate_private_key_type(account, private_key): error_texts.append('Please use active private key.') error_texts.extend(self.view.strategy_widget.strategy_controller.validation_errors()) @@ -227,7 +227,8 @@ def handle_save(self): if self.mode == 'add': # Add the private key to the database private_key = self.view.private_key_input.text() - self.add_private_key(private_key) + if private_key: + self.add_private_key(private_key) account = self.view.account_input.text() else: # Edit From 1fcef6b3813ec9c54c32c6b0f86c4faea3e09356 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 3 Aug 2018 13:41:46 +0300 Subject: [PATCH 0484/1846] Change dexbot version number to 0.5.5 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 662840898..e257fd117 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.5.4' +VERSION = '0.5.5' AUTHOR = 'Codaone Oy' __version__ = VERSION From c5fa287d9440d596054344bd160bc6175eff75f9 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 3 Aug 2018 13:49:55 +0300 Subject: [PATCH 0485/1846] Change strategy form label wrap width in the auto generator --- dexbot/views/strategy_form.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/views/strategy_form.py b/dexbot/views/strategy_form.py index 77daa703d..981937159 100644 --- a/dexbot/views/strategy_form.py +++ b/dexbot/views/strategy_form.py @@ -111,8 +111,8 @@ def add_element(self, option): def _add_label_wrap(self, key): wrap = QtWidgets.QWidget(self.group_box) - wrap.setMinimumSize(QtCore.QSize(110, 0)) - wrap.setMaximumSize(QtCore.QSize(110, 16777215)) + wrap.setMinimumSize(QtCore.QSize(120, 0)) + wrap.setMaximumSize(QtCore.QSize(120, 16777215)) layout = QtWidgets.QHBoxLayout(wrap) layout.setContentsMargins(0, 0, 0, 0) From b55d5ce392b6db1213e20026e2bc89f9be9394ca Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 3 Aug 2018 13:50:20 +0300 Subject: [PATCH 0486/1846] Change dexbot version number to 0.5.6 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index e257fd117..5ac261b83 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.5.5' +VERSION = '0.5.6' AUTHOR = 'Codaone Oy' __version__ = VERSION From d475da7d777e4dd5860f400683970396592bdec3 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 3 Aug 2018 15:57:00 +0300 Subject: [PATCH 0487/1846] WIP Changes to logic --- dexbot/strategies/staggered_orders.py | 144 ++++++++++++++++---------- 1 file changed, 92 insertions(+), 52 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 2b3085c98..83f12ec67 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -87,8 +87,8 @@ def __init__(self, *args, **kwargs): def maintain_strategy(self, *args, **kwargs): """ Logic of the strategy :param args: Order which was added after the bot was started and if there was no market center price + :param args: :param kwargs: - :return: """ # Calculate market center price @@ -101,7 +101,7 @@ def maintain_strategy(self, *args, **kwargs): # Get orders orders = self.orders - # Sort buy and sell orders + # Sort buy and sell orders from biggest to smallest self.buy_orders = self.get_buy_orders('DESC', orders) self.sell_orders = self.get_sell_orders('DESC', orders) @@ -112,7 +112,7 @@ def maintain_strategy(self, *args, **kwargs): if self.sell_orders: lowest_sell_order = self.sell_orders[-1] - # Get account balances + # Get current account balances account_balances = self.total_balance(order_ids=[], return_asset=True) base_balance = account_balances['base'] @@ -133,15 +133,17 @@ def maintain_strategy(self, *args, **kwargs): # Base asset check if base_balance > base_asset_threshold: + # Allocate available funds self.allocate_base_asset(base_balance) - elif self.market_center_price > highest_buy_order['base']['amount'] * (1 + self.spread): + elif self.market_center_price > highest_buy_order['base']['price'] * (1 + self.spread): # Cancel lowest buy order self.shift_orders_up(self.buy_orders[-0]) # Quote asset check if quote_balance > quote_asset_threshold: + # Allocate available funds self.allocate_quote_asset(quote_balance) - elif self.market_center_price < lowest_sell_order['base']['amount'] * (1 - self.spread): + elif self.market_center_price < lowest_sell_order['base']['price'] * (1 - self.spread): # Cancel highest sell order self.shift_orders_down(self.sell_orders[0]) @@ -155,131 +157,169 @@ def maintain_mountain_mode(self): def allocate_base_asset(self, base_balance, *args, **kwargs): """ Allocates base asset :param base_balance: Amount of the base asset available to use - :return: """ # Todo: Work in progress if self.buy_orders: # Todo: Make order size check function - if self.order_size_correct(): - pass - # if self.instant_fill: - # if actual_spread >= self.spread + self.increment: - # self.place_higher_buy_order(self.own_buy_orders[0]) - # return - # else: - # if self.highest_buy + self.increment < self.lowest_ask: - # pass + lowest_buy_order = self.buy_orders[-1] + highest_buy_order = self.buy_orders[0] + + # Check if the order size is correct + # This check doesn't work at this moment. + if self.is_order_size_correct(lowest_buy_order, base_balance): + # Is bot allowed to make orders which might fill immediately + if self.instant_fill: + if self.actual_spread >= self.target_spread + self.increment: + self.place_higher_buy_order(highest_buy_order) + else: + # This was in the diagram, seems wrong. + # Todo: Is highest_sell + increment > upper_bound? + # YES -> increase_order_size() + # NO -> place_higher_sell() // Should this be buy? + pass + else: + # This was in the diagram, is it ok? + # Todo: Is highest_buy + increment < lowest_ask + # YES -> Goes same place where "instant_fill" YES path + # NO -> Goes same place where above mentioned commenting is + pass else: + # Cancel highest buy order self.cancel(self.buy_orders[0]) else: - self.place_highest_buy_order(base_balance) + self.place_lowest_buy_order(base_balance) def allocate_quote_asset(self, quote_balance, *args, **kwargs): """ Allocates quote asset """ # Todo: Work in progress + # Almost same as the allocate_base() with some differences, this is done after that if self.sell_orders: pass else: - self.place_lowest_sell_order(quote_balance) + self.place_highest_sell_order(quote_balance) - def is_order_size_correct(self): + def is_order_size_correct(self, order, balance): """ Checks if the order size is correct - :return: """ - # Todo: Work in progress - pass + # Todo: Work in progress. + order_size = (order['base']['amount'] + balance['amount']) * self.increment + if order_size == order['quote']['amount']: + return True + return False def shift_orders_up(self, order): - """ Removes lowest buy order and places higher buy order - :param order: Lowest buy order - :return: + """ Removes given order and places higher buy order + :param order: Order to be removed """ self.cancel(order) self.place_higher_buy_order(order) def shift_orders_down(self, order): - """ Removes highest sell order and places lower sell order - :param order: Highest sell order - :return: + """ Removes given order and places lower sell order + :param order: Order to be removed """ self.cancel(order) self.place_lower_sell_order(order) def place_higher_buy_order(self, order): """ Place higher buy order - + Mode: MOUNTAIN amount (QUOTE) = lower_buy_order_amount * (1 + increment) price (BASE) = lower_buy_order_price * (1 + increment) :param order: Previously highest buy order - :return: """ - amount = order['quote']['amount'] + amount = order['quote']['amount'] * (1+ self.increment) price = order['base']['price'] * (1 + self.increment) self.market_buy(amount, price) def place_higher_sell_order(self, order): """ Place higher sell order - + Mode: MOUNTAIN amount (QUOTE) = higher_sell_order_amount / (1 + increment) price (BASE) = higher_sell_order_price * (1 + increment) :param order: highest_sell_order - :return: """ amount = order['quote']['amount'] / (1 + self.increment) price = order['base']['price'] * (1 + self.increment) self.market_sell(amount, price) - def place_highest_buy_order(self, base_balance): - """ Places buy order closest to the market center price - :param base_balance: Available BASE asset balance - """ - amount = base_balance['amount'] * self.increment - price = self.market_center_price / math.sqrt(1 + self.spread) - - self.market_buy(amount, price) - def place_lower_buy_order(self, order): """ Place lower buy order - + Mode: MOUNTAIN amount (QUOTE) = lowest_buy_order_amount / (1 + increment) - price (BASE) = lowest_buy_order_price / (1 + increment) + price (BASE) = Order's base price :param order: Previously lowest buy order - :return: """ amount = order['quote']['amount'] / (1 + self.increment) - price = order['base']['price'] / (1 + self.increment) + price = order['base']['price'] self.market_buy(amount, price) def place_lower_sell_order(self, order): """ Place lower sell order - + Mode: MOUNTAIN amount (QUOTE) = higher_sell_order_amount * (1 + increment) price (BASE) = higher_sell_order_price / (1 + increment) :param order: Previously higher sell order - :return: """ amount = order['quote']['amount'] * (1 + self.increment) price = order['base']['price'] / (1 + self.increment) self.market_sell(amount, price) - def place_lowest_sell_order(self, quote_balance): - """ Places sell order closest to the market center price - :param quote_balance: Available QUOTE asset balance + def place_highest_sell_order(self, quote_balance, place_order=True): + """ Places sell order furthest to the market center price + Mode: MOUNTAIN + :param Amount | quote_balance: Available QUOTE asset balance + :param bool | place_order: Default is True, use this to only calculate highest sell order + :return dict | order: Returns highest sell order """ - amount = quote_balance['amount'] * self.increment price = self.market_center_price * math.sqrt(1 + self.spread) + previous_price = price - self.market_sell(amount, price) + while price <= self.upper_bound: + previous_price = price + price = price * (1 + self.increment) + else: + amount = quote_balance['amount'] * self.increment + price = previous_price + amount = amount / price + + if place_order: + self.market_sell(amount, price) + else: + return {"amount": amount, "price": price} + + def place_lowest_buy_order(self, base_balance, place_order=True): + """ Places buy order furthest to the market center price + Mode: MOUNTAIN + :param Amount | base_balance: Available BASE asset balance + :param bool | place_order: Default is True, use this to only calculate lowest buy order + :return dict | order: Returns lowest buy order + """ + price = self.market_center_price / math.sqrt(1 + self.spread) + previous_price = price + + while price >= self.lower_bound: + previous_price = price + price = price / (1 + self.increment) + else: + amount = base_balance['amount'] * self.increment + price = previous_price + amount = amount / price + + if place_order: + self.market_buy(amount, price) + else: + return {"amount": amount, "price": price} def error(self, *args, **kwargs): self.disabled = True From 93bd67a43b238881ad696496a46558ff9b3528b3 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 6 Aug 2018 12:16:21 +0500 Subject: [PATCH 0488/1846] Do not try to buy/sell 0 amount Closes: #120 --- dexbot/basestrategy.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 6518f1360..6aa1a06c4 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -431,6 +431,12 @@ def market_buy(self, amount, price, return_none=False, *args, **kwargs): precision = self.market['base']['precision'] base_amount = truncate(price * amount, precision) + # Do not try to sell with 0 balance + if not base_amount: + self.log.critical('Trying to buy 0') + self.disabled = True + return None + # Make sure we have enough balance for the order if self.balance(self.market['base']) < base_amount: self.log.critical( @@ -471,6 +477,12 @@ def market_sell(self, amount, price, return_none=False, *args, **kwargs): precision = self.market['quote']['precision'] quote_amount = truncate(amount, precision) + # Do not try to sell with 0 balance + if not quote_amount: + self.log.critical('Trying to sell 0') + self.disabled = True + return None + # Make sure we have enough balance for the order if self.balance(self.market['quote']) < quote_amount: self.log.critical( From d2641b11459720cd5c6d3df21f47a61a3d601b75 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 6 Aug 2018 12:26:49 +0500 Subject: [PATCH 0489/1846] Properly handle "Unable to find Object" exception This fixes an error during cancelling orders which prevents fallback cancel behavior when one-by-one cancel should be done. --- dexbot/basestrategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 6518f1360..2797ce549 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -386,7 +386,7 @@ def _cancel(self, orders): orders, account=self.account, fee_asset=self.fee_asset ) except bitsharesapi.exceptions.UnhandledRPCError as e: - if str(e) == 'Assert Exception: maybe_found != nullptr: Unable to find Object': + if str(e).startswith('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 From 0e6745fe1b4f2776dbfeb276a77fe9ff91fb383f Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 6 Aug 2018 13:39:09 +0500 Subject: [PATCH 0490/1846] Properly pass fee_asset as id to python-bitshares Closes: #263 --- dexbot/basestrategy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 6518f1360..3fe8fd1fd 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -383,7 +383,7 @@ def _cancel(self, orders): try: self.retry_action( self.bitshares.cancel, - orders, account=self.account, fee_asset=self.fee_asset + orders, account=self.account, fee_asset=self.fee_asset['id'] ) except bitsharesapi.exceptions.UnhandledRPCError as e: if str(e) == 'Assert Exception: maybe_found != nullptr: Unable to find Object': @@ -452,7 +452,7 @@ def market_buy(self, amount, price, return_none=False, *args, **kwargs): Amount(amount=amount, asset=self.market["quote"]), account=self.account.name, returnOrderId="head", - fee_asset=self.fee_asset, + fee_asset=self.fee_asset['id'], *args, **kwargs ) @@ -492,7 +492,7 @@ def market_sell(self, amount, price, return_none=False, *args, **kwargs): Amount(amount=amount, asset=self.market["quote"]), account=self.account.name, returnOrderId="head", - fee_asset=self.fee_asset, + fee_asset=self.fee_asset['id'], *args, **kwargs ) From 6753b17f500a1b8af5d5e1bc531bd3d476d82f32 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 6 Aug 2018 13:24:26 +0300 Subject: [PATCH 0491/1846] Change dexbot version number to 0.5.7 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 5ac261b83..efafbef00 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.5.6' +VERSION = '0.5.7' AUTHOR = 'Codaone Oy' __version__ = VERSION From ea0cd54cd9c8021ffbb29e6db746bed1665c4bd8 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 6 Aug 2018 13:48:18 +0300 Subject: [PATCH 0492/1846] Remove CLI compiling from releases --- .travis.yml | 2 -- appveyor.yml | 10 +++------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6628b07a3..b97cdc390 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,12 +14,10 @@ install: script: - echo "@TODO - Running tests..." - pyinstaller --distpath dist/$TRAVIS_OS_NAME gui.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-cli-$TRAVIS_OS_NAME-$TRAVIS_TAG.tar.gz dist/$TRAVIS_OS_NAME/DEXBot-cli - tar -czvf dist/DEXBot-gui-$TRAVIS_OS_NAME-$TRAVIS_TAG.tar.gz dist/$TRAVIS_OS_NAME/DEXBot-gui deploy: - provider: releases diff --git a/appveyor.yml b/appveyor.yml index faabc242f..e99c2d046 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -8,7 +8,7 @@ environment: - PYTHON: "C:\\Python35-x64" #---------------------------------# -# build # +# Build # #---------------------------------# build: off @@ -24,7 +24,6 @@ install: after_test: - make package - - '7z a DEXBot-cli-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-cli.exe' - '7z a DEXBot-gui-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe' # @TODO: Run tests.. @@ -32,14 +31,11 @@ test_script: - "echo tests..." artifacts: - - path: DEXBot-cli-win64.zip - name: DEXBot-cli-win64.zip - - path: DEXBot-gui-win64.zip name: DEXBot-gui-win64.zip #---------------------------------# -# deployment # +# Deployment # #---------------------------------# shallow_clone: false @@ -57,7 +53,7 @@ deploy: appveyor_repo_tag: true # deploy on tag push only #---------------------------------# -# notifications # +# Notifications # #---------------------------------# notifications: From 3760e2eb08d090c3f1aac34f1dafc30222c56550 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 6 Aug 2018 13:56:42 +0300 Subject: [PATCH 0493/1846] Change dexbot version number to 0.5.8 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 5ac261b83..304b81ea0 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.5.6' +VERSION = '0.5.8' AUTHOR = 'Codaone Oy' __version__ = VERSION From c1b16897244d90c43c66d590864cdc4d52b56f28 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 6 Aug 2018 14:02:22 +0300 Subject: [PATCH 0494/1846] Fix a typo in BaseStrategy --- dexbot/basestrategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 6aa1a06c4..bf47cb963 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -431,7 +431,7 @@ def market_buy(self, amount, price, return_none=False, *args, **kwargs): precision = self.market['base']['precision'] base_amount = truncate(price * amount, precision) - # Do not try to sell with 0 balance + # Do not try to buy with 0 balance if not base_amount: self.log.critical('Trying to buy 0') self.disabled = True From d5db509cdba3973a0e89de940b279aadcb1f8581 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 6 Aug 2018 14:03:34 +0300 Subject: [PATCH 0495/1846] Change dexbot version number to 0.5.9 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 5ac261b83..222b5e0af 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.5.6' +VERSION = '0.5.9' AUTHOR = 'Codaone Oy' __version__ = VERSION From d9e1cf93451ce84a97332fb0fa131d3d2755f515 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Mon, 6 Aug 2018 14:04:39 +0300 Subject: [PATCH 0496/1846] Change dexbot version number to 0.5.10 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index efafbef00..c0ea7099b 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.5.7' +VERSION = '0.5.10' AUTHOR = 'Codaone Oy' __version__ = VERSION From ec37bd67baf76bbdaa353b8c4d0d9384531447bb Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 6 Aug 2018 15:32:22 +0300 Subject: [PATCH 0497/1846] WIP Change staggered orders --- dexbot/strategies/staggered_orders.py | 40 ++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 83f12ec67..4b0c31056 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -76,6 +76,8 @@ def __init__(self, *args, **kwargs): self.market_center_price = None self.buy_orders = [] self.sell_orders = [] + self.actual_spread = 0 + self.market_spread = 0 # Order expiration time self.expiration = 60 * 60 * 24 * 365 * 5 @@ -92,6 +94,7 @@ def maintain_strategy(self, *args, **kwargs): """ # Calculate market center price + # Todo: Move market_center_price to another place? It will be recalculated on each loop now. self.market_center_price = self.calculate_center_price(suppress_errors=True) # Loop until center price appears on the market @@ -100,17 +103,35 @@ def maintain_strategy(self, *args, **kwargs): # Get orders orders = self.orders + market_orders = self.market.orderbook(1) # Sort buy and sell orders from biggest to smallest self.buy_orders = self.get_buy_orders('DESC', orders) self.sell_orders = self.get_sell_orders('DESC', orders) # Get highest buy and lowest sell prices from orders + highest_buy_price = None + lowest_sell_price = None + if self.buy_orders: highest_buy_order = self.buy_orders[0] + highest_buy_price = self.buy_orders[0]['price'] if self.sell_orders: lowest_sell_order = self.sell_orders[-1] + lowest_sell_price = self.sell_orders[-1].invert().get('price') + + # Calculate actual spread + # Todo: Check the calculation for market_spread and actual_spread. + if lowest_sell_price and highest_buy_price: + self.actual_spread = 1 - (highest_buy_price / lowest_sell_price) + + # Calculate market spread + highest_market_buy = market_orders['bids'][0]['price'] + lowest_market_sell = market_orders['asks'][0]['price'] + + if highest_market_buy and lowest_market_sell: + self.market_spread = 1 - (highest_market_buy / lowest_market_sell) # Get current account balances account_balances = self.total_balance(order_ids=[], return_asset=True) @@ -169,7 +190,9 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): if self.is_order_size_correct(lowest_buy_order, base_balance): # Is bot allowed to make orders which might fill immediately if self.instant_fill: - if self.actual_spread >= self.target_spread + self.increment: + # Todo: Check if actual_spread calculates correct + if self.actual_spread >= self.spread + self.increment: + # Todo: Places lower instead of higher, looks more valley than mountain self.place_higher_buy_order(highest_buy_order) else: # This was in the diagram, seems wrong. @@ -204,10 +227,13 @@ def is_order_size_correct(self, order, balance): :return: """ # Todo: Work in progress. - order_size = (order['base']['amount'] + balance['amount']) * self.increment - if order_size == order['quote']['amount']: - return True - return False + return True + # previous_order_size = (order['base']['amount'] + balance['amount']) * self.increment + # order_size = order['quote']['amount'] + # + # if previous_order_size == order_size: + # return True + # return False def shift_orders_up(self, order): """ Removes given order and places higher buy order @@ -231,8 +257,8 @@ def place_higher_buy_order(self, order): :param order: Previously highest buy order """ - amount = order['quote']['amount'] * (1+ self.increment) - price = order['base']['price'] * (1 + self.increment) + amount = order['quote']['amount'] * (1 + self.increment) + price = order['price'] * (1 + self.increment) self.market_buy(amount, price) From 44933e4fd1934783248c2f40ac112c70175d783a Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 7 Aug 2018 16:02:54 +0300 Subject: [PATCH 0498/1846] Change ConfigElement texts --- dexbot/strategies/staggered_orders.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 4b0c31056..ab06d6dc4 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -32,11 +32,11 @@ def configure(cls, return_base_config=True): 'increment', 'float', 4, 'Increment', 'The percentage difference between staggered orders', (0, None, 2, '%')), ConfigElement( - 'center_price_dynamic', 'bool', True, 'Dynamic center price', - 'Always calculate the middle from the closest market orders', None), + 'center_price_dynamic', 'bool', True, 'Market center price', + 'Begin strategy with center price obtained from the market. Use with mature markets', None), ConfigElement( - 'center_price', 'float', 0, 'Center price', - 'Fixed center price expressed in base asset: base/quote', (0, None, 8, '')), + 'center_price', 'float', 0, 'Manual center price', + 'In an immature market, give a center price manually to begin with. BASE/QUOTE', (0, None, 8, '')), ConfigElement( 'lower_bound', 'float', 1, 'Lower bound', 'The bottom price in the range', (0, None, 8, '')), From f08f62c4c828a359718827d4ab3bb864eeb50245 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 7 Aug 2018 16:03:39 +0300 Subject: [PATCH 0499/1846] WIP Change staggered orders logic --- dexbot/strategies/staggered_orders.py | 106 ++++++++++++++++++-------- 1 file changed, 74 insertions(+), 32 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index ab06d6dc4..41a575a8e 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -66,7 +66,7 @@ def __init__(self, *args, **kwargs): self.worker_name = kwargs.get('name') self.view = kwargs.get('view') self.mode = self.worker['mode'] - self.spread = self.worker['spread'] / 100 + self.target_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'] @@ -107,7 +107,7 @@ def maintain_strategy(self, *args, **kwargs): # Sort buy and sell orders from biggest to smallest self.buy_orders = self.get_buy_orders('DESC', orders) - self.sell_orders = self.get_sell_orders('DESC', orders) + self.sell_orders = self.get_sell_orders('ASC', orders) # Get highest buy and lowest sell prices from orders highest_buy_price = None @@ -118,20 +118,22 @@ def maintain_strategy(self, *args, **kwargs): highest_buy_price = self.buy_orders[0]['price'] if self.sell_orders: - lowest_sell_order = self.sell_orders[-1] - lowest_sell_price = self.sell_orders[-1].invert().get('price') + lowest_sell_order = self.sell_orders[0] + lowest_sell_price = self.sell_orders[0].invert().get('price') # Calculate actual spread # Todo: Check the calculation for market_spread and actual_spread. if lowest_sell_price and highest_buy_price: - self.actual_spread = 1 - (highest_buy_price / lowest_sell_price) + # self.actual_spread = 1 - (highest_buy_price / lowest_sell_price) + self.actual_spread = lowest_sell_price / highest_buy_price - 1 # Calculate market spread highest_market_buy = market_orders['bids'][0]['price'] lowest_market_sell = market_orders['asks'][0]['price'] if highest_market_buy and lowest_market_sell: - self.market_spread = 1 - (highest_market_buy / lowest_market_sell) + # self.market_spread = 1 - (highest_market_buy / lowest_market_sell) + self.market_spread = lowest_market_sell / highest_market_buy - 1 # Get current account balances account_balances = self.total_balance(order_ids=[], return_asset=True) @@ -156,17 +158,17 @@ def maintain_strategy(self, *args, **kwargs): if base_balance > base_asset_threshold: # Allocate available funds self.allocate_base_asset(base_balance) - elif self.market_center_price > highest_buy_order['base']['price'] * (1 + self.spread): + elif self.market_center_price > highest_buy_order['base']['price'] * (1 + self.target_spread): # Cancel lowest buy order - self.shift_orders_up(self.buy_orders[-0]) + self.shift_orders_up(self.buy_orders[-1]) # Quote asset check if quote_balance > quote_asset_threshold: # Allocate available funds self.allocate_quote_asset(quote_balance) - elif self.market_center_price < lowest_sell_order['base']['price'] * (1 - self.spread): + elif self.market_center_price < lowest_sell_order['base']['price'] * (1 - self.target_spread): # Cancel highest sell order - self.shift_orders_down(self.sell_orders[0]) + self.shift_orders_down(self.sell_orders[-1]) def maintain_mountain_mode(self): """ Mountain mode @@ -187,12 +189,11 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): # Check if the order size is correct # This check doesn't work at this moment. - if self.is_order_size_correct(lowest_buy_order, base_balance): + if self.is_order_size_correct(highest_buy_order, base_balance): # Is bot allowed to make orders which might fill immediately if self.instant_fill: # Todo: Check if actual_spread calculates correct - if self.actual_spread >= self.spread + self.increment: - # Todo: Places lower instead of higher, looks more valley than mountain + if self.actual_spread >= self.target_spread + self.increment: self.place_higher_buy_order(highest_buy_order) else: # This was in the diagram, seems wrong. @@ -216,9 +217,35 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): """ Allocates quote asset """ # Todo: Work in progress - # Almost same as the allocate_base() with some differences, this is done after that if self.sell_orders: - pass + # Todo: Make order size check function + lowest_sell_order = self.sell_orders[0] + highest_sell_order = self.sell_orders[-1] + + # Check if the order size is correct + # This check doesn't work at this moment. + if self.is_order_size_correct(highest_sell_order, quote_balance): + # Is bot allowed to make orders which might fill immediately + if self.instant_fill: + # Todo: Check if actual_spread calculates correct + if self.actual_spread >= self.target_spread + self.increment: + self.place_lower_sell_order(lowest_sell_order) + else: + # Todo: Work in progress, seems wrong? + # This was in the diagram, seems wrong. + # # Is highest_sell + increment > upper_bound? + # YES -> increase_order_size() + # NO -> place_higher_sell() // Should this be sell? + pass + else: + # Todo: Work in progress, seems wrong? + # # Is lowest_sell - increment > highest_bid + # YES -> Goes same place where "instant_fill" YES path + # NO -> Goes same place where above mentioned commenting is + pass + else: + # Cancel lowest sell order + self.cancel(self.sell_orders[0]) else: self.place_highest_sell_order(quote_balance) @@ -252,12 +279,12 @@ def shift_orders_down(self, order): def place_higher_buy_order(self, order): """ Place higher buy order Mode: MOUNTAIN - amount (QUOTE) = lower_buy_order_amount * (1 + increment) + amount (QUOTE) = lower_buy_order_amount price (BASE) = lower_buy_order_price * (1 + increment) :param order: Previously highest buy order """ - amount = order['quote']['amount'] * (1 + self.increment) + amount = order['quote']['amount'] price = order['price'] * (1 + self.increment) self.market_buy(amount, price) @@ -270,6 +297,7 @@ def place_higher_sell_order(self, order): :param order: highest_sell_order """ + # Todo: Work in progress. amount = order['quote']['amount'] / (1 + self.increment) price = order['base']['price'] * (1 + self.increment) @@ -278,26 +306,28 @@ def place_higher_sell_order(self, order): def place_lower_buy_order(self, order): """ Place lower buy order Mode: MOUNTAIN - amount (QUOTE) = lowest_buy_order_amount / (1 + increment) - price (BASE) = Order's base price + amount (QUOTE) = lowest_buy_order_amount + price (BASE) = Order's base price / (1 + increment) :param order: Previously lowest buy order """ - amount = order['quote']['amount'] / (1 + self.increment) - price = order['base']['price'] + # Todo: Work in progress. + amount = order['quote']['amount'] + price = order['base']['price'] / (1 + self.increment) self.market_buy(amount, price) def place_lower_sell_order(self, order): """ Place lower sell order Mode: MOUNTAIN - amount (QUOTE) = higher_sell_order_amount * (1 + increment) + amount (QUOTE) = higher_sell_order_amount price (BASE) = higher_sell_order_price / (1 + increment) :param order: Previously higher sell order """ - amount = order['quote']['amount'] * (1 + self.increment) - price = order['base']['price'] / (1 + self.increment) + # Todo: Work in progress. + amount = order['quote']['amount'] + price = order['price'] / (1 + self.increment) self.market_sell(amount, price) @@ -305,19 +335,25 @@ def place_highest_sell_order(self, quote_balance, place_order=True): """ Places sell order furthest to the market center price Mode: MOUNTAIN :param Amount | quote_balance: Available QUOTE asset balance - :param bool | place_order: Default is True, use this to only calculate highest sell order + :param bool | place_order: Default is True, use this to only return highest sell order :return dict | order: Returns highest sell order """ - price = self.market_center_price * math.sqrt(1 + self.spread) + # Todo: Fix edge case where CP is close to upper bound and will go over. + price = self.market_center_price * math.sqrt(1 + self.target_spread) previous_price = price + amount = quote_balance['amount'] * self.increment + previous_amount = amount + while price <= self.upper_bound: previous_price = price + previous_amount = amount + price = price * (1 + self.increment) + amount = amount * (1 + self.increment) else: - amount = quote_balance['amount'] * self.increment + amount = previous_amount price = previous_price - amount = amount / price if place_order: self.market_sell(amount, price) @@ -328,19 +364,25 @@ def place_lowest_buy_order(self, base_balance, place_order=True): """ Places buy order furthest to the market center price Mode: MOUNTAIN :param Amount | base_balance: Available BASE asset balance - :param bool | place_order: Default is True, use this to only calculate lowest buy order + :param bool | place_order: Default is True, use this to only return lowest buy order :return dict | order: Returns lowest buy order """ - price = self.market_center_price / math.sqrt(1 + self.spread) + # Todo: Fix edge case where CP is close to lower bound and will go over. + price = self.market_center_price / math.sqrt(1 + self.target_spread) previous_price = price + amount = base_balance['amount'] * self.increment + previous_amount = amount + while price >= self.lower_bound: previous_price = price + previous_amount = amount + price = price / (1 + self.increment) + amount = amount / (1 + self.increment) else: - amount = base_balance['amount'] * self.increment + amount = previous_amount / price price = previous_price - amount = amount / price if place_order: self.market_buy(amount, price) From 67351cce99812e83d7f81d4eaec211a3c5cfe0c1 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 8 Aug 2018 08:12:43 +0300 Subject: [PATCH 0500/1846] Update installation scripts --- Makefile | 1 - requirements.txt | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Makefile b/Makefile index ac2ac86ba..8ff073d01 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,6 @@ clean-build: 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 diff --git a/requirements.txt b/requirements.txt index d15c84df3..e0e862aec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ pyqt5==5.10 pyqt-distutils==0.7.3 -pyinstaller==3.3.1 -click-datetime==0.2 +pyinstaller==3.3.1 \ No newline at end of file From 8afcb0aa15c09c2d5774305b7178b0e0f6c683f2 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 8 Aug 2018 10:47:11 +0300 Subject: [PATCH 0501/1846] Update project dependecies --- setup.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index 70d945a67..3cc0f71d0 100755 --- a/setup.py +++ b/setup.py @@ -7,13 +7,15 @@ cmd_class = {} console_scripts = ['dexbot-cli = dexbot.cli:main'] install_requires = [ - "bitshares==0.1.19", - "uptick>=0.1.4", - "click", - "sqlalchemy", - "ruamel.yaml>=0.15.37", - "sdnotify", - "appdirs>=1.4.3" + 'bitshares==0.1.19', + 'uptick>=0.1.9', + 'click', + 'sqlalchemy', + 'ruamel.yaml>=0.15.37', + 'sdnotify', + 'appdirs>=1.4.3', + 'pycryptodomex==3.6.4', + 'cryptography>=2.3' ] From 84429d15de6e305561d8a1b5eee061e27e7490c0 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 8 Aug 2018 12:09:57 +0300 Subject: [PATCH 0502/1846] Add openssl including to appveyor --- appveyor.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index e99c2d046..5bc9075e2 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -17,6 +17,8 @@ configuration: Release install: - SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%;C:\MinGW\bin + - SET LIB=C:\OpenSSL-Win64\lib;%LIB% + - SET INCLUDE=C:\OpenSSL-Win64\include;%INCLUDE% - copy c:\MinGW\bin\mingw32-make.exe c:\MinGW\bin\make.exe - copy c:\Python35-x64\python.exe c:\Python35-x64\python3.exe - python --version From 23bd119574e86c95ee6378f2f1936cb07379bbd1 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 8 Aug 2018 12:42:19 +0300 Subject: [PATCH 0503/1846] Move cryptography dependency to requirements.txt --- appveyor.yml | 2 -- requirements.txt | 3 ++- setup.py | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 5bc9075e2..e99c2d046 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -17,8 +17,6 @@ configuration: Release install: - SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%;C:\MinGW\bin - - SET LIB=C:\OpenSSL-Win64\lib;%LIB% - - SET INCLUDE=C:\OpenSSL-Win64\include;%INCLUDE% - copy c:\MinGW\bin\mingw32-make.exe c:\MinGW\bin\make.exe - copy c:\Python35-x64\python.exe c:\Python35-x64\python3.exe - python --version diff --git a/requirements.txt b/requirements.txt index e0e862aec..f6ce5f388 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ pyqt5==5.10 pyqt-distutils==0.7.3 -pyinstaller==3.3.1 \ No newline at end of file +pyinstaller==3.3.1 +cryptography==2.3 \ No newline at end of file diff --git a/setup.py b/setup.py index 3cc0f71d0..2d90561c6 100755 --- a/setup.py +++ b/setup.py @@ -14,8 +14,7 @@ 'ruamel.yaml>=0.15.37', 'sdnotify', 'appdirs>=1.4.3', - 'pycryptodomex==3.6.4', - 'cryptography>=2.3' + 'pycryptodomex==3.6.4' ] From 3cdb4f2a78d7fc2d4b69f566754e963758c018a0 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 8 Aug 2018 12:48:28 +0300 Subject: [PATCH 0504/1846] Add click-datetime to requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index f6ce5f388..e3eab6ed6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ pyqt5==5.10 pyqt-distutils==0.7.3 pyinstaller==3.3.1 +click-datetime==0.2 cryptography==2.3 \ No newline at end of file From 44615ba239ca0a021a225398ef7525179c868914 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 8 Aug 2018 13:02:06 +0300 Subject: [PATCH 0505/1846] Add is_sell_order function --- dexbot/basestrategy.py | 13 +++++++++++-- dexbot/strategies/staggered_orders.py | 1 - 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 68872e0bb..fc160abbe 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -281,7 +281,7 @@ def get_buy_orders(self, sort=None, orders=None): # Find buy orders for order in orders: - if order['base']['symbol'] == self.market['base']['symbol']: + if not self.is_sell_order(order): buy_orders.append(order) if sort: buy_orders = self.sort_orders(buy_orders, sort) @@ -301,7 +301,7 @@ def get_sell_orders(self, sort=None, orders=None): # Find sell orders for order in orders: - if order['base']['symbol'] != self.market['base']['symbol']: + if self.is_sell_order(order): sell_orders.append(order) if sort: @@ -309,6 +309,15 @@ def get_sell_orders(self, sort=None, orders=None): return sell_orders + def is_sell_order(self, order): + """ Checks if the order is Sell order. Returns False if Buy order + :param order: Buy / Sell order + :return: bool: True = Sell order, False = Buy order + """ + if order['base']['symbol'] != self.market['base']['symbol']: + return True + return False + @staticmethod def sort_orders(orders, sort='DESC'): """ Return list of orders sorted ascending or descending diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 41a575a8e..c8fd88fa2 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1,6 +1,5 @@ import math from datetime import datetime -from datetime import timedelta from dexbot.basestrategy import BaseStrategy, ConfigElement from dexbot.qt_queue.idle_queue import idle_add From e3ed926854ec21eabe94e45861d1f759c624d3a8 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 8 Aug 2018 16:18:13 +0300 Subject: [PATCH 0506/1846] Refactor asset total calculation --- dexbot/basestrategy.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index fc160abbe..24b706254 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -612,11 +612,8 @@ def total_balance(self, order_ids=None, return_asset=False): if order_ids is None: # Get all orders from Blockchain - order_ids = [] - - for order in self.orders: - order_ids.append(order['id']) - elif order_ids: + order_ids = [order['id'] for order in self.orders] + if order_ids: orders_balance = self.orders_balance(order_ids) quote += orders_balance['quote'] base += orders_balance['base'] @@ -627,8 +624,8 @@ def total_balance(self, order_ids=None, return_asset=False): return {'quote': quote, 'base': base} - def asset_total_balance(self, return_asset): - """ Returns the whole value of the account as one asset only + def account_total_value(self, return_asset): + """ Returns the total value of the account in given asset :param str return_asset: Asset which is wanted as return :return: float: Value of the account in one asset """ From ec89f12438e1d30330e7f8189cedb770cc10ed9f Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 8 Aug 2018 16:19:48 +0300 Subject: [PATCH 0507/1846] Change asset balance calculation --- dexbot/strategies/staggered_orders.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c8fd88fa2..c56f4b330 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -140,12 +140,16 @@ def maintain_strategy(self, *args, **kwargs): base_balance = account_balances['base'] quote_balance = account_balances['quote'] - total_value_base = self.asset_total_balance(base_balance['symbol']) - total_value_quote = self.asset_total_balance(quote_balance['symbol']) + order_ids = [order['id'] for order in orders] + orders_balance = self.orders_balance(order_ids) + + # Balance per asset from orders and account balance + quote_orders_balance = orders_balance['quote'] + quote_balance['amount'] + base_orders_balance = orders_balance['base'] + base_balance['amount'] # Calculate asset thresholds - base_asset_threshold = total_value_base / 20000 - quote_asset_threshold = total_value_quote / 20000 + base_asset_threshold = base_orders_balance / 20000 + quote_asset_threshold = quote_orders_balance / 20000 # Check boundaries if self.market_center_price > self.upper_bound: From c2d85f21c8a778da05e24b6f460d378a12fbe769 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 8 Aug 2018 16:20:29 +0300 Subject: [PATCH 0508/1846] WIP Change staggered orders logic --- dexbot/strategies/staggered_orders.py | 81 ++++++++++++++++----------- 1 file changed, 49 insertions(+), 32 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c56f4b330..d361b51e4 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -75,7 +75,7 @@ def __init__(self, *args, **kwargs): self.market_center_price = None self.buy_orders = [] self.sell_orders = [] - self.actual_spread = 0 + self.actual_spread = self.target_spread + 1 self.market_spread = 0 # Order expiration time @@ -113,17 +113,16 @@ def maintain_strategy(self, *args, **kwargs): lowest_sell_price = None if self.buy_orders: - highest_buy_order = self.buy_orders[0] highest_buy_price = self.buy_orders[0]['price'] if self.sell_orders: lowest_sell_order = self.sell_orders[0] - lowest_sell_price = self.sell_orders[0].invert().get('price') + # Sell orders are inverted by default, this is reversed for price comparison + lowest_sell_price = lowest_sell_order.invert().get('price') + lowest_sell_order.invert() # Calculate actual spread - # Todo: Check the calculation for market_spread and actual_spread. if lowest_sell_price and highest_buy_price: - # self.actual_spread = 1 - (highest_buy_price / lowest_sell_price) self.actual_spread = lowest_sell_price / highest_buy_price - 1 # Calculate market spread @@ -131,7 +130,6 @@ def maintain_strategy(self, *args, **kwargs): lowest_market_sell = market_orders['asks'][0]['price'] if highest_market_buy and lowest_market_sell: - # self.market_spread = 1 - (highest_market_buy / lowest_market_sell) self.market_spread = lowest_market_sell / highest_market_buy - 1 # Get current account balances @@ -157,21 +155,22 @@ def maintain_strategy(self, *args, **kwargs): elif self.market_center_price < self.lower_bound: self.lower_bound = self.market_center_price - # Base asset check + # BASE asset check if base_balance > base_asset_threshold: # Allocate available funds self.allocate_base_asset(base_balance) - elif self.market_center_price > highest_buy_order['base']['price'] * (1 + self.target_spread): + elif self.market_center_price > highest_buy_price * (1 + self.target_spread): # Cancel lowest buy order self.shift_orders_up(self.buy_orders[-1]) - # Quote asset check + # QUOTE asset check if quote_balance > quote_asset_threshold: # Allocate available funds self.allocate_quote_asset(quote_balance) - elif self.market_center_price < lowest_sell_order['base']['price'] * (1 - self.target_spread): - # Cancel highest sell order - self.shift_orders_down(self.sell_orders[-1]) + elif lowest_sell_price: + if self.market_center_price < lowest_sell_price * (1 - self.target_spread): + # Cancel highest sell order + self.shift_orders_down(self.sell_orders[-1]) def maintain_mountain_mode(self): """ Mountain mode @@ -186,34 +185,33 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): """ # Todo: Work in progress if self.buy_orders: - # Todo: Make order size check function + # Get currently the lowest and highest buy orders lowest_buy_order = self.buy_orders[-1] highest_buy_order = self.buy_orders[0] # Check if the order size is correct - # This check doesn't work at this moment. + # Todo: This check doesn't work at this moment. if self.is_order_size_correct(highest_buy_order, base_balance): # Is bot allowed to make orders which might fill immediately if self.instant_fill: - # Todo: Check if actual_spread calculates correct if self.actual_spread >= self.target_spread + self.increment: self.place_higher_buy_order(highest_buy_order) else: - # This was in the diagram, seems wrong. - # Todo: Is highest_sell + increment > upper_bound? - # YES -> increase_order_size() - # NO -> place_higher_sell() // Should this be buy? - pass + if lowest_buy_order + self.increment < self.lower_bound: + self.increase_order_size(highest_buy_order) + else: + self.place_higher_buy_order(lowest_buy_order) else: # This was in the diagram, is it ok? # Todo: Is highest_buy + increment < lowest_ask # YES -> Goes same place where "instant_fill" YES path - # NO -> Goes same place where above mentioned commenting is + # NO -> Same place as inside instant fill "else" pass else: # Cancel highest buy order self.cancel(self.buy_orders[0]) else: + # Place first buy order to the market self.place_lowest_buy_order(base_balance) def allocate_quote_asset(self, quote_balance, *args, **kwargs): @@ -222,24 +220,22 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): # Todo: Work in progress if self.sell_orders: # Todo: Make order size check function + # Todo: Check that the orders are sorted right lowest_sell_order = self.sell_orders[0] highest_sell_order = self.sell_orders[-1] # Check if the order size is correct # This check doesn't work at this moment. - if self.is_order_size_correct(highest_sell_order, quote_balance): + if self.is_order_size_correct(lowest_sell_order, quote_balance): # Is bot allowed to make orders which might fill immediately if self.instant_fill: - # Todo: Check if actual_spread calculates correct if self.actual_spread >= self.target_spread + self.increment: self.place_lower_sell_order(lowest_sell_order) else: - # Todo: Work in progress, seems wrong? - # This was in the diagram, seems wrong. - # # Is highest_sell + increment > upper_bound? - # YES -> increase_order_size() - # NO -> place_higher_sell() // Should this be sell? - pass + if highest_sell_order['price'] + self.increment > self.upper_bound: + self.increase_order_size(lowest_sell_order) + else: + self.place_higher_sell_order(highest_sell_order) else: # Todo: Work in progress, seems wrong? # # Is lowest_sell - increment > highest_bid @@ -252,6 +248,26 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): else: self.place_highest_sell_order(quote_balance) + def increase_order_size(self, order): + """ Checks if the order is sell or buy order and then replaces it with a bigger one. + :param order: Sell / Buy order + """ + # Todo: Work in progress. pass for now + pass + # Cancel order + self.cancel(order) + + if self.is_sell_order(order): + # Increase sell order size + amount = order['base']['amount'] * (1 + self.increment) + price = order['price'] + self.market_sell(amount, price) + else: + # Increase buy order size + amount = order['quote']['amount'] * (1 + self.increment) + price = order['price'] + self.market_buy(amount, price) + def is_order_size_correct(self, order, balance): """ Checks if the order size is correct :return: @@ -302,7 +318,7 @@ def place_higher_sell_order(self, order): """ # Todo: Work in progress. amount = order['quote']['amount'] / (1 + self.increment) - price = order['base']['price'] * (1 + self.increment) + price = order['price'] * (1 + self.increment) self.market_sell(amount, price) @@ -316,7 +332,7 @@ def place_lower_buy_order(self, order): """ # Todo: Work in progress. amount = order['quote']['amount'] - price = order['base']['price'] / (1 + self.increment) + price = order['price'] / (1 + self.increment) self.market_buy(amount, price) @@ -353,7 +369,8 @@ def place_highest_sell_order(self, quote_balance, place_order=True): previous_amount = amount price = price * (1 + self.increment) - amount = amount * (1 + self.increment) + amount = amount / (1 + self.increment) + print('Amount, Price ', amount, price) else: amount = previous_amount price = previous_price From 34ec7cbf82d51589ba110418d59f967659155144 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 9 Aug 2018 10:15:08 +0300 Subject: [PATCH 0509/1846] Remove instant fill option --- dexbot/strategies/staggered_orders.py | 46 ++----- .../views/ui/forms/staggered_orders_widget.ui | 122 +----------------- 2 files changed, 14 insertions(+), 154 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index d361b51e4..252528d5d 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -41,10 +41,7 @@ def configure(cls, return_base_config=True): 'The bottom price in the range', (0, None, 8, '')), ConfigElement( 'upper_bound', 'float', 1000000, 'Upper bound', - 'The top price in the range', (0, None, 8, '')), - ConfigElement( - 'allow_instant_fill', 'bool', True, 'Allow instant fill', - 'Allow bot to make orders which might fill immediately upon placement', None) + 'The top price in the range', (0, None, 8, '')) ] def __init__(self, *args, **kwargs): @@ -69,7 +66,6 @@ def __init__(self, *args, **kwargs): self.increment = self.worker['increment'] / 100 self.upper_bound = self.worker['upper_bound'] self.lower_bound = self.worker['lower_bound'] - self.instant_fill = self.worker['allow_instant_fill'] # Strategy variables self.market_center_price = None @@ -192,21 +188,13 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): # Check if the order size is correct # Todo: This check doesn't work at this moment. if self.is_order_size_correct(highest_buy_order, base_balance): - # Is bot allowed to make orders which might fill immediately - if self.instant_fill: - if self.actual_spread >= self.target_spread + self.increment: - self.place_higher_buy_order(highest_buy_order) - else: - if lowest_buy_order + self.increment < self.lower_bound: - self.increase_order_size(highest_buy_order) - else: - self.place_higher_buy_order(lowest_buy_order) + if self.actual_spread >= self.target_spread + self.increment: + self.place_higher_buy_order(highest_buy_order) else: - # This was in the diagram, is it ok? - # Todo: Is highest_buy + increment < lowest_ask - # YES -> Goes same place where "instant_fill" YES path - # NO -> Same place as inside instant fill "else" - pass + if lowest_buy_order['price'] + self.increment < self.lower_bound: + self.increase_order_size(highest_buy_order) + else: + self.place_lower_buy_order(lowest_buy_order) else: # Cancel highest buy order self.cancel(self.buy_orders[0]) @@ -227,21 +215,13 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): # Check if the order size is correct # This check doesn't work at this moment. if self.is_order_size_correct(lowest_sell_order, quote_balance): - # Is bot allowed to make orders which might fill immediately - if self.instant_fill: - if self.actual_spread >= self.target_spread + self.increment: - self.place_lower_sell_order(lowest_sell_order) - else: - if highest_sell_order['price'] + self.increment > self.upper_bound: - self.increase_order_size(lowest_sell_order) - else: - self.place_higher_sell_order(highest_sell_order) + if self.actual_spread >= self.target_spread + self.increment: + self.place_lower_sell_order(lowest_sell_order) else: - # Todo: Work in progress, seems wrong? - # # Is lowest_sell - increment > highest_bid - # YES -> Goes same place where "instant_fill" YES path - # NO -> Goes same place where above mentioned commenting is - pass + if highest_sell_order['price'] + self.increment > self.upper_bound: + self.increase_order_size(lowest_sell_order) + else: + self.place_higher_sell_order(highest_sell_order) else: # Cancel lowest sell order self.cancel(self.sell_orders[0]) diff --git a/dexbot/views/ui/forms/staggered_orders_widget.ui b/dexbot/views/ui/forms/staggered_orders_widget.ui index 35311c47d..54b5b26dd 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 449 - 366 + 333 @@ -872,126 +872,6 @@ - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Allow bot to make orders which might fill immediately upon placement - - - ? - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - 5 - - - - - - - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Allow order instant fill - - - false - - - false - - - 8 - - - 0.000000000000000 - - - 999999999.998999953269958 - - - From a8428562d6472c0a2863de1e2c71b117288b700e Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 9 Aug 2018 10:36:27 +0300 Subject: [PATCH 0510/1846] Change order placement functions to simulate order --- dexbot/strategies/staggered_orders.py | 36 +++++++++++++++++++-------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 252528d5d..926030aeb 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -275,66 +275,82 @@ def shift_orders_down(self, order): self.cancel(order) self.place_lower_sell_order(order) - def place_higher_buy_order(self, order): + def place_higher_buy_order(self, order, place_order=True): """ Place higher buy order Mode: MOUNTAIN amount (QUOTE) = lower_buy_order_amount price (BASE) = lower_buy_order_price * (1 + increment) :param order: Previously highest buy order + :param bool | place_order: True = Places order to the market, False = returns amount and price """ amount = order['quote']['amount'] price = order['price'] * (1 + self.increment) - self.market_buy(amount, price) + if place_order: + self.market_buy(amount, price) + else: + return {"amount": amount, "price": price} - def place_higher_sell_order(self, order): + def place_higher_sell_order(self, order, place_order=True): """ Place higher sell order Mode: MOUNTAIN amount (QUOTE) = higher_sell_order_amount / (1 + increment) price (BASE) = higher_sell_order_price * (1 + increment) :param order: highest_sell_order + :param bool | place_order: True = Places order to the market, False = returns amount and price """ # Todo: Work in progress. amount = order['quote']['amount'] / (1 + self.increment) price = order['price'] * (1 + self.increment) - self.market_sell(amount, price) + if place_order: + self.market_sell(amount, price) + else: + return {"amount": amount, "price": price} - def place_lower_buy_order(self, order): + def place_lower_buy_order(self, order, place_order=True): """ Place lower buy order Mode: MOUNTAIN amount (QUOTE) = lowest_buy_order_amount price (BASE) = Order's base price / (1 + increment) :param order: Previously lowest buy order + :param bool | place_order: True = Places order to the market, False = returns amount and price """ # Todo: Work in progress. amount = order['quote']['amount'] price = order['price'] / (1 + self.increment) - self.market_buy(amount, price) + if place_order: + self.market_buy(amount, price) + else: + return {"amount": amount, "price": price} - def place_lower_sell_order(self, order): + def place_lower_sell_order(self, order, place_order=True): """ Place lower sell order Mode: MOUNTAIN amount (QUOTE) = higher_sell_order_amount price (BASE) = higher_sell_order_price / (1 + increment) :param order: Previously higher sell order + :param bool | place_order: True = Places order to the market, False = returns amount and price """ # Todo: Work in progress. amount = order['quote']['amount'] price = order['price'] / (1 + self.increment) - self.market_sell(amount, price) + if place_order: + self.market_sell(amount, price) + else: + return {"amount": amount, "price": price} def place_highest_sell_order(self, quote_balance, place_order=True): """ Places sell order furthest to the market center price Mode: MOUNTAIN :param Amount | quote_balance: Available QUOTE asset balance - :param bool | place_order: Default is True, use this to only return highest sell order + :param bool | place_order: True = Places order to the market, False = returns amount and price :return dict | order: Returns highest sell order """ # Todo: Fix edge case where CP is close to upper bound and will go over. @@ -364,7 +380,7 @@ def place_lowest_buy_order(self, base_balance, place_order=True): """ Places buy order furthest to the market center price Mode: MOUNTAIN :param Amount | base_balance: Available BASE asset balance - :param bool | place_order: Default is True, use this to only return lowest buy order + :param bool | place_order: True = Places order to the market, False = returns amount and price :return dict | order: Returns lowest buy order """ # Todo: Fix edge case where CP is close to lower bound and will go over. From eb8de92e1d32d4b3ff056cc564c522e781aade0f Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 9 Aug 2018 14:28:48 +0300 Subject: [PATCH 0511/1846] Clean up and optimize code --- dexbot/basestrategy.py | 108 ++++++++++++++++---------- dexbot/strategies/staggered_orders.py | 14 ++-- 2 files changed, 75 insertions(+), 47 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index bf44c830d..d6d50b0bb 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -3,6 +3,7 @@ import collections import time import math +import copy from .storage import Storage from .statemachine import StateMachine @@ -190,6 +191,9 @@ def __init__( # will be reset to False after reset only self.disabled = False + # Order expiration time in seconds + self.expiration = 60 * 60 * 24 * 365 * 5 + # A private logger that adds worker identify data to the LogRecord self.log = logging.LoggerAdapter( logging.getLogger('dexbot.per_worker'), @@ -279,7 +283,7 @@ def get_order(order_id, return_none=True): """ Returns the Order object for the order_id :param str|dict order_id: blockchain object id of the order - can be a dict with the id key in it + can be an order 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 """ @@ -292,42 +296,63 @@ def get_order(order_id, return_none=True): return None return order - def get_updated_order(self, order): + def get_updated_order(self, order_id): """ Tries to get the updated order from the API returns None if the order doesn't exist + + :param str|dict order_id: blockchain object id of the order + can be an order dict with the id key in it """ - if not order: - 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 None + if isinstance(order_id, dict): + order_id = order_id['id'] + + # Get the limited order by id + order = None + for limit_order in self.account['limit_orders']: + if order_id == limit_order['id']: + order = limit_order + break + else: + return order + + order = self.get_updated_limit_order(order) + return Order(order, bitshares_instance=self.bitshares) @property - def updated_open_orders(self): - """ - Returns updated open Orders. - account.openorders doesn't return updated values for the order so we calculate the values manually + def updated_orders(self): + """ Returns all open orders as updated orders """ self.account.refresh() - self.account.ensure_full() - limit_orders = self.account['limit_orders'][:] - for o in limit_orders: - 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 + limited_orders = [] + for order in self.account['limit_orders']: + base_asset_id = order['sell_price']['base']['asset_id'] + quote_asset_id = order['sell_price']['quote']['asset_id'] + # Check if the order is in the current market + if not self.is_current_market(base_asset_id, quote_asset_id): + continue + + limited_orders.append(self.get_updated_limit_order(order)) - orders = [ + return [ Order(o, bitshares_instance=self.bitshares) - for o in limit_orders + for o in limited_orders ] - return [o for o in orders if self.worker["market"] == o.market] + @staticmethod + def get_updated_limit_order(limit_order): + """ Returns a modified limit_order so that when passed to Order class, + will return an Order object with updated amount values + :param limit_order: an item of Account['limit_orders'] + :return: dict + """ + o = copy.deepcopy(limit_order) + price = o['sell_price']['base']['amount'] / o['sell_price']['quote']['amount'] + base_amount = o['for_sale'] + quote_amount = base_amount / price + o['sell_price']['base']['amount'] = base_amount + o['sell_price']['quote']['amount'] = quote_amount + return o @property def market(self): @@ -348,10 +373,6 @@ def balance(self, asset): """ return self._account.balance(asset) - @property - def test_mode(self): - return self.config['node'] == "wss://node.testnet.bitshares.eu" - @property def balances(self): """ Return the balances of your worker's account @@ -431,7 +452,7 @@ def market_buy(self, amount, price, return_none=False, *args, **kwargs): precision = self.market['base']['precision'] base_amount = truncate(price * amount, precision) - # Do not try to buy with 0 balance + # Don't try to place an order of size 0 if not base_amount: self.log.critical('Trying to buy 0') self.disabled = True @@ -457,11 +478,13 @@ def market_buy(self, amount, price, return_none=False, *args, **kwargs): price, Amount(amount=amount, asset=self.market["quote"]), account=self.account.name, + expiration=self.expiration, returnOrderId="head", fee_asset=self.fee_asset['id'], *args, **kwargs ) + self.log.debug('Placed buy order {}'.format(buy_transaction)) buy_order = self.get_order(buy_transaction['orderid'], return_none=return_none) if buy_order and buy_order['deleted']: @@ -477,7 +500,7 @@ def market_sell(self, amount, price, return_none=False, *args, **kwargs): precision = self.market['quote']['precision'] quote_amount = truncate(amount, precision) - # Do not try to sell with 0 balance + # Don't try to place an order of size 0 if not quote_amount: self.log.critical('Trying to sell 0') self.disabled = True @@ -503,11 +526,13 @@ def market_sell(self, amount, price, return_none=False, *args, **kwargs): price, Amount(amount=amount, asset=self.market["quote"]), account=self.account.name, + expiration=self.expiration, returnOrderId="head", fee_asset=self.fee_asset['id'], *args, **kwargs ) + self.log.debug('Placed sell order {}'.format(sell_transaction)) sell_order = self.get_order(sell_transaction['orderid'], return_none=return_none) if sell_order and sell_order['deleted']: @@ -527,6 +552,19 @@ def calculate_order_data(self, order, amount, price): order['base'] = base_asset return order + def is_current_market(self, base_asset_id, quote_asset_id): + """ Returns True if given asset id's are of the current market + """ + if quote_asset_id == self.market['quote']['id']: + if base_asset_id == self.market['base']['id']: + return True + return False + if quote_asset_id == self.market['base']['id']: + if base_asset_id == self.market['quote']['id']: + return True + return False + return False + def purge(self): """ Clear all the worker data from the database and cancel all orders """ @@ -538,14 +576,6 @@ def purge(self): def purge_worker_data(worker_name): Storage.clear_worker_data(worker_name) - @staticmethod - def get_order_amount(order, asset_type): - try: - order_amount = order[asset_type]['amount'] - 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 diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index d2c279742..ae834f473 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -56,8 +56,6 @@ def __init__(self, *args, **kwargs): self.increment = self.worker['increment'] / 100 self.upper_bound = self.worker['upper_bound'] self.lower_bound = self.worker['lower_bound'] - # Order expiration time, should be high enough - self.expiration = 60*60*24*365*5 self.last_check = datetime.now() if self['setup_done']: @@ -124,13 +122,13 @@ def init_strategy(self): # Place the buy orders for buy_order in buy_orders: - order = self.market_buy(buy_order['amount'], buy_order['price'], expiration=self.expiration) + order = self.market_buy(buy_order['amount'], buy_order['price']) 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'], expiration=self.expiration) + order = self.market_sell(sell_order['amount'], sell_order['price']) if order: self.save_order(order) @@ -149,11 +147,11 @@ def place_reverse_order(self, order): if order['base']['symbol'] == self.market['base']['symbol']: # Buy order price = order['price'] * (1 + self.spread) amount = order['quote']['amount'] - new_order = self.market_sell(amount, price, expiration=self.expiration) + new_order = self.market_sell(amount, price) else: # Sell order price = (order['price'] ** -1) / (1 + self.spread) amount = order['base']['amount'] - new_order = self.market_buy(amount, price, expiration=self.expiration) + new_order = self.market_buy(amount, price) if new_order: self.remove_order(order) @@ -165,11 +163,11 @@ def place_order(self, order): if order['base']['symbol'] == self.market['base']['symbol']: # Buy order price = order['price'] amount = order['quote']['amount'] - new_order = self.market_buy(amount, price, expiration=self.expiration) + new_order = self.market_buy(amount, price) else: # Sell order price = order['price'] ** -1 amount = order['base']['amount'] - new_order = self.market_sell(amount, price, expiration=self.expiration) + new_order = self.market_sell(amount, price) self.save_order(new_order) From fe8ac0f54d9d8b8f7a49e404f186224241803fab Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Thu, 9 Aug 2018 15:13:40 +0300 Subject: [PATCH 0512/1846] Apply changes from staggered_orders_revision branch --- dexbot/basestrategy.py | 136 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 130 insertions(+), 6 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index d6d50b0bb..85a7a47bd 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -229,13 +229,13 @@ def _calculate_center_price(self, suppress_errors=False): center_price = highest_bid['price'] * math.sqrt(lowest_ask['price'] / highest_bid['price']) return center_price - def calculate_center_price(self, center_price=None, - asset_offset=False, spread=None, order_ids=None, manual_offset=0): + def calculate_center_price(self, center_price=None, asset_offset=False, spread=None, + order_ids=None, manual_offset=0, suppress_errors=False): """ Calculate center price which shifts based on available funds """ if center_price is None: # No center price was given so we simply calculate the center price - calculated_center_price = self._calculate_center_price() + calculated_center_price = self._calculate_center_price(suppress_errors) else: # Center price was given so we only use the calculated center price # for quote to base asset conversion @@ -278,6 +278,80 @@ def orders(self): self.account.refresh() return [o for o in self.account.openorders if self.worker["market"] == o.market and self.account.openorders] + @property + def all_orders(self): + """ Return the worker's open accounts in all markets + """ + self.account.refresh() + return [o for o in self.account.openorders] + + def get_buy_orders(self, sort=None, orders=None): + """ Return buy orders + :param str sort: DESC or ASC will sort the orders accordingly, default None. + :param list orders: List of orders. If None given get all orders from Blockchain. + :return list buy_orders: List of buy orders only. + """ + buy_orders = [] + + if not orders: + orders = self.orders + + # Find buy orders + for order in orders: + if not self.is_sell_order(order): + buy_orders.append(order) + if sort: + buy_orders = self.sort_orders(buy_orders, sort) + + return buy_orders + + def get_sell_orders(self, sort=None, orders=None): + """ Return sell orders + :param str sort: DESC or ASC will sort the orders accordingly, default None. + :param list orders: List of orders. If None given get all orders from Blockchain. + :return list sell_orders: List of sell orders only. + """ + sell_orders = [] + + if not orders: + orders = self.orders + + # Find sell orders + for order in orders: + if self.is_sell_order(order): + sell_orders.append(order) + + if sort: + sell_orders = self.sort_orders(sell_orders, sort) + + return sell_orders + + def is_sell_order(self, order): + """ Checks if the order is Sell order. Returns False if Buy order + :param order: Buy / Sell order + :return: bool: True = Sell order, False = Buy order + """ + if order['base']['symbol'] != self.market['base']['symbol']: + return True + return False + + @staticmethod + def sort_orders(orders, sort='DESC'): + """ Return list of orders sorted ascending or descending + :param list orders: list of orders to be sorted + :param str sort: ASC or DESC. Default DESC + :return list: Sorted list of orders. + """ + if sort.upper() == 'ASC': + reverse = False + elif sort.upper() == 'DESC': + reverse = True + else: + return None + + # Sort orders by price + return sorted(orders, key=lambda order: order['price'], reverse=reverse) + @staticmethod def get_order(order_id, return_none=True): """ Returns the Order object for the order_id @@ -589,15 +663,20 @@ def total_balance(self, order_ids=None, return_asset=False): quote_asset = self.market['quote']['id'] base_asset = self.market['base']['id'] + # Total balance calculation 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 order_ids is None: + # Get all orders from Blockchain + order_ids = [order['id'] for order in self.orders] + if order_ids: + orders_balance = self.orders_balance(order_ids) + quote += orders_balance['quote'] + base += orders_balance['base'] if return_asset: quote = Amount(quote, quote_asset) @@ -605,6 +684,51 @@ def total_balance(self, order_ids=None, return_asset=False): return {'quote': quote, 'base': base} + def account_total_value(self, return_asset): + """ Returns the total value of the account in given asset + :param str return_asset: Asset which is wanted as return + :return: float: Value of the account in one asset + """ + total_value = 0 + + # Total balance calculation + for balance in self.balances: + if balance['symbol'] != return_asset: + # Convert to asset if different + total_value += self.convert_asset(balance['amount'], balance['symbol'], return_asset) + else: + total_value += balance['amount'] + + # Orders balance calculation + for order in self.all_orders: + updated_order = self.get_updated_order(order['id']) + + if not order: + continue + if updated_order['base']['symbol'] == return_asset: + total_value += updated_order['base']['amount'] + else: + total_value += self.convert_asset( + updated_order['base']['amount'], + updated_order['base']['symbol'], + return_asset + ) + + return total_value + + @staticmethod + def convert_asset(from_value, from_asset, to_asset): + """ Converts asset to another based on the latest market value + :param from_value: Amount of the input asset + :param from_asset: Symbol of the input asset + :param to_asset: Symbol of the output asset + :return: Asset converted to another asset as float value + """ + market = Market('{}/{}'.format(from_asset, to_asset)) + ticker = market.ticker() + latest_price = ticker.get('latest', {}).get('price', None) + return from_value * latest_price + def orders_balance(self, order_ids, return_asset=False): if not order_ids: order_ids = [] From 2aa5bb41e26eae0e92749e27b429a482b915af4f Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 10 Aug 2018 08:28:51 +0300 Subject: [PATCH 0513/1846] Add is_buy_order function --- dexbot/basestrategy.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index e316895f2..d919fd42d 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -294,7 +294,7 @@ def get_buy_orders(self, sort=None, orders=None): # Find buy orders for order in orders: - if not self.is_sell_order(order): + if self.is_buy_order(order): buy_orders.append(order) if sort: buy_orders = self.sort_orders(buy_orders, sort) @@ -322,10 +322,19 @@ def get_sell_orders(self, sort=None, orders=None): return sell_orders + def is_buy_order(self, order): + """ Checks if the order is Buy order + :param order: Buy / Sell order + :return: bool: True = Buy order + """ + if order['base']['symbol'] == self.market['base']['symbol']: + return True + return False + def is_sell_order(self, order): - """ Checks if the order is Sell order. Returns False if Buy order + """ Checks if the order is Sell order :param order: Buy / Sell order - :return: bool: True = Sell order, False = Buy order + :return: bool: True = Sell order """ if order['base']['symbol'] != self.market['base']['symbol']: return True From f017fbbf4fba75db759561380c4a7d7e02dc8697 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 10 Aug 2018 08:30:31 +0300 Subject: [PATCH 0514/1846] Remove shift_orders_up and down functions --- dexbot/strategies/staggered_orders.py | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 926030aeb..8d2853ac3 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -157,16 +157,15 @@ def maintain_strategy(self, *args, **kwargs): self.allocate_base_asset(base_balance) elif self.market_center_price > highest_buy_price * (1 + self.target_spread): # Cancel lowest buy order - self.shift_orders_up(self.buy_orders[-1]) + self.cancel(self.buy_orders[-1]) # QUOTE asset check if quote_balance > quote_asset_threshold: # Allocate available funds self.allocate_quote_asset(quote_balance) - elif lowest_sell_price: - if self.market_center_price < lowest_sell_price * (1 - self.target_spread): - # Cancel highest sell order - self.shift_orders_down(self.sell_orders[-1]) + if self.market_center_price < lowest_sell_price * (1 - self.target_spread): + # Cancel highest sell order + self.cancel(self.sell_orders[-1]) def maintain_mountain_mode(self): """ Mountain mode @@ -261,19 +260,6 @@ def is_order_size_correct(self, order, balance): # return True # return False - def shift_orders_up(self, order): - """ Removes given order and places higher buy order - :param order: Order to be removed - """ - self.cancel(order) - self.place_higher_buy_order(order) - - def shift_orders_down(self, order): - """ Removes given order and places lower sell order - :param order: Order to be removed - """ - self.cancel(order) - self.place_lower_sell_order(order) def place_higher_buy_order(self, order, place_order=True): """ Place higher buy order From db7a9854bff8c4e4b36d67d7022e4a06c6034c28 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 10 Aug 2018 15:54:01 +0300 Subject: [PATCH 0515/1846] Change order sorting method --- dexbot/basestrategy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index d919fd42d..6d5cb141e 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -347,9 +347,9 @@ def sort_orders(orders, sort='DESC'): :param str sort: ASC or DESC. Default DESC :return list: Sorted list of orders. """ - if sort.upper() == 'ASC': + if sort == 'ASC': reverse = False - elif sort.upper() == 'DESC': + elif sort == 'DESC': reverse = True else: return None From 2affaedde57066069b61d2de67df316f13106b36 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 10 Aug 2018 15:55:02 +0300 Subject: [PATCH 0516/1846] WIP Change logic of the strategy --- dexbot/strategies/staggered_orders.py | 131 +++++++++++++++++--------- 1 file changed, 87 insertions(+), 44 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 8d2853ac3..24ead0fcb 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -109,16 +109,19 @@ def maintain_strategy(self, *args, **kwargs): lowest_sell_price = None if self.buy_orders: - highest_buy_price = self.buy_orders[0]['price'] + highest_buy_price = self.buy_orders[0].get('price') if self.sell_orders: - lowest_sell_order = self.sell_orders[0] - # Sell orders are inverted by default, this is reversed for price comparison - lowest_sell_price = lowest_sell_order.invert().get('price') - lowest_sell_order.invert() + # Invert sell orders to match same asset as buy orders + sell_orders_inverted = [] + for order in self.sell_orders: + sell_orders_inverted.append(order.invert()) + + self.sell_orders = self.sort_orders(sell_orders_inverted, 'ASC') + lowest_sell_price = self.sell_orders[0].get('price') # Calculate actual spread - if lowest_sell_price and highest_buy_price: + if highest_buy_price and lowest_sell_price: self.actual_spread = lowest_sell_price / highest_buy_price - 1 # Calculate market spread @@ -163,7 +166,7 @@ def maintain_strategy(self, *args, **kwargs): if quote_balance > quote_asset_threshold: # Allocate available funds self.allocate_quote_asset(quote_balance) - if self.market_center_price < lowest_sell_price * (1 - self.target_spread): + elif self.market_center_price < lowest_sell_price * (1 - self.target_spread): # Cancel highest sell order self.cancel(self.sell_orders[-1]) @@ -186,12 +189,12 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): # Check if the order size is correct # Todo: This check doesn't work at this moment. - if self.is_order_size_correct(highest_buy_order, base_balance): + if self.is_order_size_correct(highest_buy_order): if self.actual_spread >= self.target_spread + self.increment: self.place_higher_buy_order(highest_buy_order) else: - if lowest_buy_order['price'] + self.increment < self.lower_bound: - self.increase_order_size(highest_buy_order) + if lowest_buy_order['price'] + (1 + self.increment) < self.lower_bound: + self.increase_order_size() else: self.place_lower_buy_order(lowest_buy_order) else: @@ -213,12 +216,12 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): # Check if the order size is correct # This check doesn't work at this moment. - if self.is_order_size_correct(lowest_sell_order, quote_balance): + if self.is_order_size_correct(lowest_sell_order): if self.actual_spread >= self.target_spread + self.increment: self.place_lower_sell_order(lowest_sell_order) else: - if highest_sell_order['price'] + self.increment > self.upper_bound: - self.increase_order_size(lowest_sell_order) + if highest_sell_order['price'] + (1 + self.increment) > self.upper_bound: + self.increase_order_size() else: self.place_higher_sell_order(highest_sell_order) else: @@ -227,39 +230,83 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): else: self.place_highest_sell_order(quote_balance) - def increase_order_size(self, order): - """ Checks if the order is sell or buy order and then replaces it with a bigger one. - :param order: Sell / Buy order + def increase_order_size(self): + """ + Checks which order should be increased in size and replaces it + with a maximum size order, according to global limits. Logic + depends on mode in question """ - # Todo: Work in progress. pass for now + # Todo: Work in progress. pass - # Cancel order - self.cancel(order) - - if self.is_sell_order(order): - # Increase sell order size - amount = order['base']['amount'] * (1 + self.increment) - price = order['price'] - self.market_sell(amount, price) - else: - # Increase buy order size - amount = order['quote']['amount'] * (1 + self.increment) - price = order['price'] - self.market_buy(amount, price) - - def is_order_size_correct(self, order, balance): + # Mountain mode: + # if self.mode == 'mountain': + # asset = 0 + # quote = 0 + # base = 0 + # if asset == quote: + # """ + # Starting from lowest order, for each order, see if it is approximately + # maximum size. + # If it is, move on to next. + # If not, cancel it and replace with maximum size order. Then return. + # If highest_sell_order is reached, increase it to maximum size + # + # Maximum size is: + # as many quote as the order below + # and + # as many quote * (1 + increment) as the order above + # When making an order, it must not exceed either of these limits, but be + # made according to the more limiting criteria. + # """ + # elif asset == base: + # """ + # Starting from highest order, for each order, see if it is approximately + # maximum size. + # If it is, move on to next. + # If not, cancel it and replace with maximum size order. Then return. + # If highest_sell_order is reached, increase it to maximum size + # + # Maximum size is: + # as many base as the order above + # and + # as many base * (1 + increment) as the order below + # When making an order, it must not exceed either of these limits, but be + # made according to the more limiting criteria. + # """ + # + # elif self.mode == 'valley': + # pass + # elif self.mode == 'neutral': + # pass + # elif self.mode == 'buy_slope': + # pass + # elif self.mode == 'sell_slope': + # pass + + def is_order_size_correct(self, order): """ Checks if the order size is correct :return: """ # Todo: Work in progress. return True - # previous_order_size = (order['base']['amount'] + balance['amount']) * self.increment - # order_size = order['quote']['amount'] - # - # if previous_order_size == order_size: - # return True - # return False + # if self.is_sell_order(order): + # # Order is the only sell order, and size must be calculated like initializing + # if highest_sell_order == lowest_sell_order: + # if order[size] =~ place_highest_sell_order(total_balance, place_order=False).[size]: # accuracy 0.1% + # return True + # else: + # return False + # elif order == highest_sell_order: + # if order[size] == place_higher_sell_order(order_index - 1, place_order=False).[size] # accuracy 0.1% + # return True + # else: + # return False + # elif order == lowest_sell_order: + # if order[size] == place_lower_sell_order(order_index + 1, place_order=False).[size] # accuracy 0.1% + # return True + # else: + # return False def place_higher_buy_order(self, order, place_order=True): """ Place higher buy order @@ -287,7 +334,6 @@ def place_higher_sell_order(self, order, place_order=True): :param order: highest_sell_order :param bool | place_order: True = Places order to the market, False = returns amount and price """ - # Todo: Work in progress. amount = order['quote']['amount'] / (1 + self.increment) price = order['price'] * (1 + self.increment) @@ -299,14 +345,13 @@ def place_higher_sell_order(self, order, place_order=True): def place_lower_buy_order(self, order, place_order=True): """ Place lower buy order Mode: MOUNTAIN - amount (QUOTE) = lowest_buy_order_amount + amount (QUOTE) = lowest_buy_order_amount * (1 + increment) price (BASE) = Order's base price / (1 + increment) :param order: Previously lowest buy order :param bool | place_order: True = Places order to the market, False = returns amount and price """ - # Todo: Work in progress. - amount = order['quote']['amount'] + amount = order['quote']['amount'] * (1 + self.increment) price = order['price'] / (1 + self.increment) if place_order: @@ -323,7 +368,6 @@ def place_lower_sell_order(self, order, place_order=True): :param order: Previously higher sell order :param bool | place_order: True = Places order to the market, False = returns amount and price """ - # Todo: Work in progress. amount = order['quote']['amount'] price = order['price'] / (1 + self.increment) @@ -352,7 +396,6 @@ def place_highest_sell_order(self, quote_balance, place_order=True): price = price * (1 + self.increment) amount = amount / (1 + self.increment) - print('Amount, Price ', amount, price) else: amount = previous_amount price = previous_price From 53d5f7d94a4dff4d78de169eb4f5538589087495 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 13 Aug 2018 13:59:13 +0300 Subject: [PATCH 0517/1846] Refactor is_order_correct_size function --- dexbot/strategies/staggered_orders.py | 92 ++++++++++++++++++++------- 1 file changed, 69 insertions(+), 23 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 24ead0fcb..707234e2a 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -189,7 +189,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): # Check if the order size is correct # Todo: This check doesn't work at this moment. - if self.is_order_size_correct(highest_buy_order): + if self.is_order_size_correct(highest_buy_order, self.buy_orders): if self.actual_spread >= self.target_spread + self.increment: self.place_higher_buy_order(highest_buy_order) else: @@ -216,7 +216,7 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): # Check if the order size is correct # This check doesn't work at this moment. - if self.is_order_size_correct(lowest_sell_order): + if self.is_order_size_correct(lowest_sell_order, self.sell_orders): if self.actual_spread >= self.target_spread + self.increment: self.place_lower_sell_order(lowest_sell_order) else: @@ -284,29 +284,75 @@ def increase_order_size(self): # pass def is_order_size_correct(self, order): + def is_order_size_correct(self, order, orders): """ Checks if the order size is correct - :return: + + :param order: Order closest to the center price from buy or sell side + :param orders: List of buy or sell orders + :return: bool | True = Order is correct size or within the threshold + False = Order is not right size """ - # Todo: Work in progress. - return True - - # if self.is_sell_order(order): - # # Order is the only sell order, and size must be calculated like initializing - # if highest_sell_order == lowest_sell_order: - # if order[size] =~ place_highest_sell_order(total_balance, place_order=False).[size]: # accuracy 0.1% - # return True - # else: - # return False - # elif order == highest_sell_order: - # if order[size] == place_higher_sell_order(order_index - 1, place_order=False).[size] # accuracy 0.1% - # return True - # else: - # return False - # elif order == lowest_sell_order: - # if order[size] == place_lower_sell_order(order_index + 1, place_order=False).[size] # accuracy 0.1% - # return True - # else: - # return False + order_size = order['quote']['amount'] + threshold = 0.001 + upper_threshold = order_size * (1 + threshold) + lower_threshold = order_size - (1 + threshold) + + if self.is_sell_order(order): + lowest_sell_order = orders[0] + highest_sell_order = orders[-1] + + # Order is the only sell order, and size must be calculated like initializing + if lowest_sell_order == highest_sell_order: + total_balance = self.total_balance(orders) + highest_sell_order = self.place_highest_sell_order(total_balance, place_order=False) + + # Check if the old order is same size with accuracy of 0.1% + if lower_threshold <= highest_sell_order['amount'] <= upper_threshold: + return True + return False + elif order == highest_sell_order: + order_index = orders.index(order) + higher_sell_order = self.place_higher_sell_order(order_index - 1, place_order=False) + + if lower_threshold <= higher_sell_order['amount'] <= upper_threshold: + return True + return False + elif order == lowest_sell_order: + order_index = orders.index(order) + lower_sell_order = self.place_lower_sell_order(order_index + 1, place_order=False) + + if lower_threshold <= lower_sell_order['amount'] <= upper_threshold: + return True + return False + elif self.is_buy_order(order): + lowest_buy_order = orders[-1] + highest_buy_order = orders[0] + + # Order is the only buy order, and size must be calculated like initializing + if highest_buy_order == lowest_buy_order: + total_balance = self.total_balance(orders) + lowest_buy_order = self.place_lowest_buy_order(total_balance, place_order=False) + + # Check if the old order is same size with accuracy of 0.1% + if lower_threshold <= lowest_buy_order['amount'] <= upper_threshold: + return True + return False + elif order == lowest_buy_order: + order_index = orders.index(order) + lower_buy_order = self.place_lower_buy_order(order_index - 1, place_order=False) + + if lower_threshold <= lower_buy_order['amount'] <= upper_threshold: + return True + return False + elif order == highest_buy_order: + order_index = orders.index(order) + higher_buy_order = self.place_higher_buy_order(order_index + 1, place_order=False) + + if lower_threshold <= higher_buy_order['amount'] <= upper_threshold: + return True + return False + + return False def place_higher_buy_order(self, order, place_order=True): """ Place higher buy order From 81ce987ad04ef579faf0c06cf661c7cb1ad9bc7b Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 13 Aug 2018 15:27:55 +0300 Subject: [PATCH 0518/1846] WIP Change staggered orders logic --- dexbot/strategies/staggered_orders.py | 122 ++++++++++++++++---------- 1 file changed, 77 insertions(+), 45 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 707234e2a..b6e9ad9b7 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -192,11 +192,10 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): if self.is_order_size_correct(highest_buy_order, self.buy_orders): if self.actual_spread >= self.target_spread + self.increment: self.place_higher_buy_order(highest_buy_order) + elif lowest_buy_order['price'] / (1 + self.increment) < self.lower_bound: + self.increase_order_sizes() else: - if lowest_buy_order['price'] + (1 + self.increment) < self.lower_bound: - self.increase_order_size() - else: - self.place_lower_buy_order(lowest_buy_order) + self.place_lower_buy_order(lowest_buy_order) else: # Cancel highest buy order self.cancel(self.buy_orders[0]) @@ -219,61 +218,94 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): if self.is_order_size_correct(lowest_sell_order, self.sell_orders): if self.actual_spread >= self.target_spread + self.increment: self.place_lower_sell_order(lowest_sell_order) + elif highest_sell_order['price'] * (1 + self.increment) > self.upper_bound: + self.increase_order_sizes('quote') else: - if highest_sell_order['price'] + (1 + self.increment) > self.upper_bound: - self.increase_order_size() - else: - self.place_higher_sell_order(highest_sell_order) + self.place_higher_sell_order(highest_sell_order) else: # Cancel lowest sell order self.cancel(self.sell_orders[0]) else: self.place_highest_sell_order(quote_balance) - def increase_order_size(self): + # Todo: Check completely + def increase_order_sizes(self, asset): + """ Checks which order should be increased in size and replaces it + with a maximum size order, according to global limits. Logic + depends on mode in question """ - Checks which order should be increased in size and replaces it - with a maximum size order, according to global limits. Logic - depends on mode in question - """ - # Todo: Work in progress. pass # Mountain mode: # if self.mode == 'mountain': - # asset = 0 - # quote = 0 - # base = 0 # if asset == quote: - # """ - # Starting from lowest order, for each order, see if it is approximately - # maximum size. - # If it is, move on to next. - # If not, cancel it and replace with maximum size order. Then return. - # If highest_sell_order is reached, increase it to maximum size + # """ Starting from lowest order, for each order, see if it is approximately + # maximum size. + # If it is, move on to next. + # If not, cancel it and replace with maximum size order. Then return. + # If highest_sell_order is reached, increase it to maximum size # - # Maximum size is: - # as many quote as the order below - # and - # as many quote * (1 + increment) as the order above - # When making an order, it must not exceed either of these limits, but be - # made according to the more limiting criteria. + # Maximum size is: + # as many quote as the order below + # and + # as many quote * (1 + increment) as the order above + # When making an order, it must not exceed either of these limits, but be + # made according to the more limiting criteria. # """ + # # get orders and amounts to be compared + # higher_order_number = 1 + # observe_order_number = 0 + # lower_order_number = 0 + # + # can_be_increased = False + # + # # see if order size can be increased + # while not can_be_increased: + # higher_order = self.sell_orders[higher_order_number] + # observe_order = self.sell_orders[observe_order_number] + # if observe_order_number == 0: + # lower_order = self.buy_order[0] + # else: + # lower_order = self.sell_orders[lower_order_number] + # observe_order_amount = observe_order['quote']['amount'] + # limit_from_below = lower_order['quote']['amount'] + # limit_from_above = higher_order['quote']['amount'] * (1 + self.increment) + # + # if limit_from_below >= observe_order_amount * (1 + self.increment / 10) <= limit_from_above: + # can_be_increased = True + # else: + # observe_order_number += 1 + # higher_order_number = observe_order_number + 1 + # lower_order_number = observe_order_number - 1 + # continue + # + # # calculate new order size and make order + # + # if limit_from_above > limit_from_below: + # new_order_amount = limit_from_below + # else: + # new_order_amount = limit_from_above + # + # if quote_balance - reserve_quote_amount < new_order_amount - observe_order_amount: + # new_order_amount = observe_order_amount + quote_balance - reserve_quote_amount + # + # price = observe_order['price'] + # self.cancel(observe_order) + # self.market_sell(new_order_amount, price) + # # elif asset == base: - # """ - # Starting from highest order, for each order, see if it is approximately - # maximum size. - # If it is, move on to next. - # If not, cancel it and replace with maximum size order. Then return. - # If highest_sell_order is reached, increase it to maximum size + # """ Starting from highest order, for each order, see if it is approximately + # maximum size. + # If it is, move on to next. + # If not, cancel it and replace with maximum size order. Then return. + # If highest_sell_order is reached, increase it to maximum size # - # Maximum size is: - # as many base as the order above - # and - # as many base * (1 + increment) as the order below - # When making an order, it must not exceed either of these limits, but be - # made according to the more limiting criteria. + # Maximum size is: + # as many base as the order above + # and + # as many base * (1 + increment) as the order below + # When making an order, it must not exceed either of these limits, but be + # made according to the more limiting criteria. # """ - # # elif self.mode == 'valley': # pass # elif self.mode == 'neutral': @@ -282,8 +314,8 @@ def increase_order_size(self): # pass # elif self.mode == 'sell_slope': # pass + # return None - def is_order_size_correct(self, order): def is_order_size_correct(self, order, orders): """ Checks if the order size is correct @@ -391,13 +423,13 @@ def place_higher_sell_order(self, order, place_order=True): def place_lower_buy_order(self, order, place_order=True): """ Place lower buy order Mode: MOUNTAIN - amount (QUOTE) = lowest_buy_order_amount * (1 + increment) + amount (QUOTE) = lowest_buy_order_amount price (BASE) = Order's base price / (1 + increment) :param order: Previously lowest buy order :param bool | place_order: True = Places order to the market, False = returns amount and price """ - amount = order['quote']['amount'] * (1 + self.increment) + amount = order['quote']['amount'] price = order['price'] / (1 + self.increment) if place_order: From 068f4a513d1222da60da6c9fb96e26271ccf3cc6 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 13 Aug 2018 15:28:12 +0300 Subject: [PATCH 0519/1846] Update is_order_size_correct function --- dexbot/strategies/staggered_orders.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index b6e9ad9b7..bf6f576c2 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -325,9 +325,9 @@ def is_order_size_correct(self, order, orders): False = Order is not right size """ order_size = order['quote']['amount'] - threshold = 0.001 + threshold = self.increment / 10 upper_threshold = order_size * (1 + threshold) - lower_threshold = order_size - (1 + threshold) + lower_threshold = order_size - (order_size * threshold) if self.is_sell_order(order): lowest_sell_order = orders[0] @@ -335,7 +335,7 @@ def is_order_size_correct(self, order, orders): # Order is the only sell order, and size must be calculated like initializing if lowest_sell_order == highest_sell_order: - total_balance = self.total_balance(orders) + total_balance = self.total_balance(orders, return_asset=True) highest_sell_order = self.place_highest_sell_order(total_balance, place_order=False) # Check if the old order is same size with accuracy of 0.1% @@ -344,14 +344,14 @@ def is_order_size_correct(self, order, orders): return False elif order == highest_sell_order: order_index = orders.index(order) - higher_sell_order = self.place_higher_sell_order(order_index - 1, place_order=False) + higher_sell_order = self.place_higher_sell_order(orders[order_index - 1], place_order=False) if lower_threshold <= higher_sell_order['amount'] <= upper_threshold: return True return False elif order == lowest_sell_order: order_index = orders.index(order) - lower_sell_order = self.place_lower_sell_order(order_index + 1, place_order=False) + lower_sell_order = self.place_lower_sell_order(orders[order_index + 1], place_order=False) if lower_threshold <= lower_sell_order['amount'] <= upper_threshold: return True @@ -362,8 +362,8 @@ def is_order_size_correct(self, order, orders): # Order is the only buy order, and size must be calculated like initializing if highest_buy_order == lowest_buy_order: - total_balance = self.total_balance(orders) - lowest_buy_order = self.place_lowest_buy_order(total_balance, place_order=False) + total_balance = self.total_balance(orders, return_asset=True) + lowest_buy_order = self.place_lowest_buy_order(total_balance['base'], place_order=False) # Check if the old order is same size with accuracy of 0.1% if lower_threshold <= lowest_buy_order['amount'] <= upper_threshold: @@ -371,14 +371,14 @@ def is_order_size_correct(self, order, orders): return False elif order == lowest_buy_order: order_index = orders.index(order) - lower_buy_order = self.place_lower_buy_order(order_index - 1, place_order=False) + lower_buy_order = self.place_lower_buy_order(orders[order_index - 1], place_order=False) if lower_threshold <= lower_buy_order['amount'] <= upper_threshold: return True return False elif order == highest_buy_order: order_index = orders.index(order) - higher_buy_order = self.place_higher_buy_order(order_index + 1, place_order=False) + higher_buy_order = self.place_higher_buy_order(orders[order_index + 1], place_order=False) if lower_threshold <= higher_buy_order['amount'] <= upper_threshold: return True From a58e17fe5f82fc0052edcd82fb9d9866f2d2f030 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 14 Aug 2018 07:23:05 +0300 Subject: [PATCH 0520/1846] Update lower threshold 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 bf6f576c2..887460849 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -327,7 +327,7 @@ def is_order_size_correct(self, order, orders): order_size = order['quote']['amount'] threshold = self.increment / 10 upper_threshold = order_size * (1 + threshold) - lower_threshold = order_size - (order_size * threshold) + lower_threshold = order_size / (1 + threshold) if self.is_sell_order(order): lowest_sell_order = orders[0] From 47b595a5bdededd844ed6c6c774d2a24f04026a5 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 14 Aug 2018 16:54:24 +0300 Subject: [PATCH 0521/1846] WIP Change staggered orders logic --- dexbot/strategies/staggered_orders.py | 53 ++++++++++++++++----------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 887460849..fa26e0380 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -3,6 +3,7 @@ from dexbot.basestrategy import BaseStrategy, ConfigElement from dexbot.qt_queue.idle_queue import idle_add +from dexbot.helper import truncate class Strategy(BaseStrategy): @@ -112,13 +113,9 @@ def maintain_strategy(self, *args, **kwargs): highest_buy_price = self.buy_orders[0].get('price') if self.sell_orders: - # Invert sell orders to match same asset as buy orders - sell_orders_inverted = [] - for order in self.sell_orders: - sell_orders_inverted.append(order.invert()) - - self.sell_orders = self.sort_orders(sell_orders_inverted, 'ASC') + self.sell_orders[0].invert() lowest_sell_price = self.sell_orders[0].get('price') + self.sell_orders[0].invert() # Calculate actual spread if highest_buy_price and lowest_sell_price: @@ -188,12 +185,12 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): highest_buy_order = self.buy_orders[0] # Check if the order size is correct - # Todo: This check doesn't work at this moment. if self.is_order_size_correct(highest_buy_order, self.buy_orders): if self.actual_spread >= self.target_spread + self.increment: self.place_higher_buy_order(highest_buy_order) elif lowest_buy_order['price'] / (1 + self.increment) < self.lower_bound: - self.increase_order_sizes() + # Todo: Work in progress. + self.increase_order_sizes('base') else: self.place_lower_buy_order(lowest_buy_order) else: @@ -208,17 +205,15 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): """ # Todo: Work in progress if self.sell_orders: - # Todo: Make order size check function - # Todo: Check that the orders are sorted right lowest_sell_order = self.sell_orders[0] highest_sell_order = self.sell_orders[-1] # Check if the order size is correct - # This check doesn't work at this moment. if self.is_order_size_correct(lowest_sell_order, self.sell_orders): if self.actual_spread >= self.target_spread + self.increment: - self.place_lower_sell_order(lowest_sell_order) + self.place_lower_sell_order(lowest_sell_order.invert()) elif highest_sell_order['price'] * (1 + self.increment) > self.upper_bound: + # Todo: Work in progress. self.increase_order_sizes('quote') else: self.place_higher_sell_order(highest_sell_order) @@ -324,19 +319,20 @@ def is_order_size_correct(self, order, orders): :return: bool | True = Order is correct size or within the threshold False = Order is not right size """ - order_size = order['quote']['amount'] - threshold = self.increment / 10 - upper_threshold = order_size * (1 + threshold) - lower_threshold = order_size / (1 + threshold) - if self.is_sell_order(order): + order_size = order['base']['amount'] + threshold = self.increment / 10 + upper_threshold = order_size * (1 + threshold) + lower_threshold = order_size / (1 + threshold) + lowest_sell_order = orders[0] highest_sell_order = orders[-1] # Order is the only sell order, and size must be calculated like initializing if lowest_sell_order == highest_sell_order: total_balance = self.total_balance(orders, return_asset=True) - highest_sell_order = self.place_highest_sell_order(total_balance, place_order=False) + # Todo: Take initial market_center_price here when making calculations + highest_sell_order = self.place_highest_sell_order(total_balance['quote'], place_order=False) # Check if the old order is same size with accuracy of 0.1% if lower_threshold <= highest_sell_order['amount'] <= upper_threshold: @@ -357,12 +353,18 @@ def is_order_size_correct(self, order, orders): return True return False elif self.is_buy_order(order): + order_size = order['quote']['amount'] + threshold = self.increment / 10 + upper_threshold = order_size * (1 + threshold) + lower_threshold = order_size / (1 + threshold) + lowest_buy_order = orders[-1] highest_buy_order = orders[0] # Order is the only buy order, and size must be calculated like initializing if highest_buy_order == lowest_buy_order: total_balance = self.total_balance(orders, return_asset=True) + # Todo: Take initial market_center_price here when making calculations lowest_buy_order = self.place_lowest_buy_order(total_balance['base'], place_order=False) # Check if the old order is same size with accuracy of 0.1% @@ -454,7 +456,7 @@ def place_lower_sell_order(self, order, place_order=True): else: return {"amount": amount, "price": price} - def place_highest_sell_order(self, quote_balance, place_order=True): + def place_highest_sell_order(self, quote_balance, place_order=True, market_center_price=None): """ Places sell order furthest to the market center price Mode: MOUNTAIN :param Amount | quote_balance: Available QUOTE asset balance @@ -462,7 +464,10 @@ def place_highest_sell_order(self, quote_balance, place_order=True): :return dict | order: Returns highest sell order """ # Todo: Fix edge case where CP is close to upper bound and will go over. - price = self.market_center_price * math.sqrt(1 + self.target_spread) + if not market_center_price: + market_center_price = self.market_center_price + + price = market_center_price * math.sqrt(1 + self.target_spread) previous_price = price amount = quote_balance['amount'] * self.increment @@ -475,12 +480,15 @@ def place_highest_sell_order(self, quote_balance, place_order=True): price = price * (1 + self.increment) amount = amount / (1 + self.increment) else: - amount = previous_amount + # Todo: Fix precision to match wanted asset + precision = self.market['quote']['precision'] + amount = int(float(previous_amount) * 10 ** precision) / (10 ** precision) price = previous_price if place_order: self.market_sell(amount, price) else: + amount = truncate(amount, precision) return {"amount": amount, "price": price} def place_lowest_buy_order(self, base_balance, place_order=True): @@ -504,7 +512,10 @@ def place_lowest_buy_order(self, base_balance, place_order=True): price = price / (1 + self.increment) amount = amount / (1 + self.increment) else: + precision = self.market['base']['precision'] amount = previous_amount / price + amount = int(float(amount) * 10 ** precision) / (10 ** precision) + price = previous_price if place_order: From 680c11efd33d15715b0109b6a35008b3cc082fa6 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 14 Aug 2018 19:02:45 +0500 Subject: [PATCH 0522/1846] Switch to ui autogeneration for relative orders --- dexbot/controllers/strategy_controller.py | 9 + dexbot/controllers/worker_controller.py | 2 +- dexbot/strategies/relative_orders.py | 5 +- .../views/ui/forms/relative_orders_widget.ui | 866 ------------------ 4 files changed, 13 insertions(+), 869 deletions(-) delete mode 100644 dexbot/views/ui/forms/relative_orders_widget.ui diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index c4ff42275..e9c143653 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -90,6 +90,9 @@ def __init__(self, view, configure, worker_controller, worker_data): self.view.strategy_widget.relative_order_size_input.toggled.connect( self.onchange_relative_order_size_input ) + self.view.strategy_widget.center_price_dynamic_input.toggled.connect( + self.onchange_center_price_dynamic_input + ) # Do this after the event connecting super().__init__(view, configure, worker_controller, worker_data) @@ -103,6 +106,12 @@ def onchange_relative_order_size_input(self, checked): else: self.order_size_input_to_static() + def onchange_center_price_dynamic_input(self, checked): + if checked: + self.view.strategy_widget.center_price_input.setDisabled(True) + else: + self.view.strategy_widget.center_price_input.setDisabled(False) + def order_size_input_to_relative(self): self.view.strategy_widget.amount_input.setSuffix('%') self.view.strategy_widget.amount_input.setDecimals(2) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index 04e97674a..305f58e07 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -27,7 +27,7 @@ def strategies(self): strategies = collections.OrderedDict() strategies['dexbot.strategies.relative_orders'] = { 'name': 'Relative Orders', - 'form_module': 'dexbot.views.ui.forms.relative_orders_widget_ui' + 'form_module': '' } strategies['dexbot.strategies.staggered_orders'] = { 'name': 'Staggered Orders', diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index e33a37c49..1acd75896 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -14,11 +14,12 @@ def configure(cls, return_base_config=True): ConfigElement('relative_order_size', 'bool', False, 'Relative order size', 'Amount is expressed as a percentage of the account balance of quote/base asset', None), ConfigElement('amount', 'float', 1, 'Amount', - 'Fixed order size, expressed in quote asset, unless "relative order size" selected', (0, None, 8, '')), + 'Fixed order size, expressed in quote asset, unless "relative order size" selected', + (0, None, 8, '')), ConfigElement('center_price_dynamic', 'bool', True, 'Dynamic center price', 'Always calculate the middle from the closest market orders', None), ConfigElement('center_price', 'float', 0, 'Center price', - 'Fixed center price expressed in base asset: base/quote', (0, None, 8, '%')), + 'Fixed center price expressed in base asset: base/quote', (0, None, 8, '')), ConfigElement('center_price_offset', 'bool', False, 'Center price offset based on asset balances', 'Automatically adjust orders up or down based on the imbalance of your assets', None), ConfigElement('spread', 'float', 5, 'Spread', diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui deleted file mode 100644 index a8345a88f..000000000 --- a/dexbot/views/ui/forms/relative_orders_widget.ui +++ /dev/null @@ -1,866 +0,0 @@ - - - Form - - - - 0 - 0 - 439 - 291 - - - - Form - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Worker Parameters - - - - 6 - - - 9 - - - 9 - - - 9 - - - 9 - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Order size - - - amount_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Fixed order size, expressed in quote asset, unless "relative order size" selected - - - ? - - - 5 - - - - - - - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 8 - - - 1000000000.000000000000000 - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Amount is expressed as a percentage of the account balance of quote/base asset - - - ? - - - 5 - - - - - - - - - - Relative order size - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Center Price - - - center_price_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Fixed center price expressed in base asset: base/quote - - - ? - - - 5 - - - - - - - - - - false - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - false - - - false - - - 8 - - - 0.000000000000000 - - - 999999999.998999953269958 - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Always calculate the middle from the closest market orders - - - ? - - - 5 - - - - - - - - - - - 0 - 0 - - - - Update center price from closest market orders - - - true - - - false - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Automatically adjust orders up or down based on the imbalance of your assets - - - ? - - - 5 - - - - - - - - - - Center price offset based on asset balances - - - - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - QAbstractSpinBox::UpDownArrows - - - false - - - % - - - -50.000000000000000 - - - 100.000000000000000 - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - - - - Manual center price offset - - - true - - - manual_offset_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Manually adjust orders up or down. Works independently of other offsets and doesn't override them - - - ? - - - 5 - - - - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Spread - - - spread_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - The percentage difference between buy and sell - - - ? - - - 5 - - - - - - - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - % - - - 100000.000000000000000 - - - 5.000000000000000 - - - - - - - - - - - - center_price_dynamic_input - clicked(bool) - center_price_input - setDisabled(bool) - - - 284 - 129 - - - 208 - 99 - - - - - From ef178c912b27c1315b60ed1e0dbd6edd3419a05a Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 15 Aug 2018 15:35:15 +0300 Subject: [PATCH 0523/1846] Add initial_market_center_price --- dexbot/strategies/staggered_orders.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index fa26e0380..b67f6a81e 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -70,6 +70,7 @@ def __init__(self, *args, **kwargs): # Strategy variables self.market_center_price = None + self.initial_market_center_price = None self.buy_orders = [] self.sell_orders = [] self.actual_spread = self.target_spread + 1 @@ -97,6 +98,10 @@ def maintain_strategy(self, *args, **kwargs): if not self.market_center_price: return + # Save initial market center price, which is used to make sure that first order is still correct + if not self.initial_market_center_price: + self.initial_market_center_price = self.market_center_price + # Get orders orders = self.orders market_orders = self.market.orderbook(1) @@ -331,8 +336,9 @@ def is_order_size_correct(self, order, orders): # Order is the only sell order, and size must be calculated like initializing if lowest_sell_order == highest_sell_order: total_balance = self.total_balance(orders, return_asset=True) - # Todo: Take initial market_center_price here when making calculations - highest_sell_order = self.place_highest_sell_order(total_balance['quote'], place_order=False) + highest_sell_order = self.place_highest_sell_order(total_balance['quote'], + place_order=False, + market_center_price=self.initial_market_center_price) # Check if the old order is same size with accuracy of 0.1% if lower_threshold <= highest_sell_order['amount'] <= upper_threshold: @@ -364,8 +370,9 @@ def is_order_size_correct(self, order, orders): # Order is the only buy order, and size must be calculated like initializing if highest_buy_order == lowest_buy_order: total_balance = self.total_balance(orders, return_asset=True) - # Todo: Take initial market_center_price here when making calculations - lowest_buy_order = self.place_lowest_buy_order(total_balance['base'], place_order=False) + lowest_buy_order = self.place_lowest_buy_order(total_balance['base'], + place_order=False, + market_center_price=self.initial_market_center_price) # Check if the old order is same size with accuracy of 0.1% if lower_threshold <= lowest_buy_order['amount'] <= upper_threshold: @@ -491,15 +498,19 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente amount = truncate(amount, precision) return {"amount": amount, "price": price} - def place_lowest_buy_order(self, base_balance, place_order=True): + def place_lowest_buy_order(self, base_balance, place_order=True, market_center_price=None): """ Places buy order furthest to the market center price Mode: MOUNTAIN :param Amount | base_balance: Available BASE asset balance :param bool | place_order: True = Places order to the market, False = returns amount and price + :param float | market_center_price: Optional market center price, used to to check order :return dict | order: Returns lowest buy order """ # Todo: Fix edge case where CP is close to lower bound and will go over. - price = self.market_center_price / math.sqrt(1 + self.target_spread) + if not market_center_price: + market_center_price = self.market_center_price + + price = market_center_price / math.sqrt(1 + self.target_spread) previous_price = price amount = base_balance['amount'] * self.increment From ff16b66950dff4049e6334f7fb0b3824b664ac8a Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 15 Aug 2018 15:36:10 +0300 Subject: [PATCH 0524/1846] Refactor order sorting --- dexbot/strategies/staggered_orders.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index b67f6a81e..ca927709a 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -106,9 +106,9 @@ def maintain_strategy(self, *args, **kwargs): orders = self.orders market_orders = self.market.orderbook(1) - # Sort buy and sell orders from biggest to smallest + # Sort orders so that order with index 0 is closest to the center price and -1 is furthers self.buy_orders = self.get_buy_orders('DESC', orders) - self.sell_orders = self.get_sell_orders('ASC', orders) + self.sell_orders = self.get_sell_orders('DESC', orders) # Get highest buy and lowest sell prices from orders highest_buy_price = None @@ -118,13 +118,13 @@ def maintain_strategy(self, *args, **kwargs): highest_buy_price = self.buy_orders[0].get('price') if self.sell_orders: - self.sell_orders[0].invert() lowest_sell_price = self.sell_orders[0].get('price') - self.sell_orders[0].invert() + # Invert the sell price to BASE + lowest_sell_price = lowest_sell_price ** -1 # Calculate actual spread if highest_buy_price and lowest_sell_price: - self.actual_spread = lowest_sell_price / highest_buy_price - 1 + self.actual_spread = (lowest_sell_price / highest_buy_price) - 1 # Calculate market spread highest_market_buy = market_orders['bids'][0]['price'] From 04e1804eb875b34d6bf7fc7a0a7a109695fe46f3 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 15 Aug 2018 15:36:48 +0300 Subject: [PATCH 0525/1846] Fix highest_sell_order price check --- dexbot/strategies/staggered_orders.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index ca927709a..7abe1b88f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -212,12 +212,14 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): if self.sell_orders: lowest_sell_order = self.sell_orders[0] highest_sell_order = self.sell_orders[-1] + highest_sell_order_price = (highest_sell_order['price'] ** -1) # Check if the order size is correct if self.is_order_size_correct(lowest_sell_order, self.sell_orders): if self.actual_spread >= self.target_spread + self.increment: - self.place_lower_sell_order(lowest_sell_order.invert()) - elif highest_sell_order['price'] * (1 + self.increment) > self.upper_bound: + # Place order closer to the center price + self.place_lower_sell_order(lowest_sell_order) + elif highest_sell_order_price * (1 + self.increment) > self.upper_bound: # Todo: Work in progress. self.increase_order_sizes('quote') else: From bbb562f95dea4ba35314a0af62b9f358b187681e Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 15 Aug 2018 15:37:34 +0300 Subject: [PATCH 0526/1846] WIP Change inverted buy orders to sell orders --- dexbot/strategies/staggered_orders.py | 45 +++++++++++++-------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 7abe1b88f..e4083306a 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -183,7 +183,6 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): """ Allocates base asset :param base_balance: Amount of the base asset available to use """ - # Todo: Work in progress if self.buy_orders: # Get currently the lowest and highest buy orders lowest_buy_order = self.buy_orders[-1] @@ -192,6 +191,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): # Check if the order size is correct if self.is_order_size_correct(highest_buy_order, self.buy_orders): if self.actual_spread >= self.target_spread + self.increment: + # Place order closer to the center price self.place_higher_buy_order(highest_buy_order) elif lowest_buy_order['price'] / (1 + self.increment) < self.lower_bound: # Todo: Work in progress. @@ -208,7 +208,6 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): def allocate_quote_asset(self, quote_balance, *args, **kwargs): """ Allocates quote asset """ - # Todo: Work in progress if self.sell_orders: lowest_sell_order = self.sell_orders[0] highest_sell_order = self.sell_orders[-1] @@ -326,12 +325,16 @@ def is_order_size_correct(self, order, orders): :return: bool | True = Order is correct size or within the threshold False = Order is not right size """ + # Calculate threshold + order_size = order['quote']['amount'] if self.is_sell_order(order): order_size = order['base']['amount'] - threshold = self.increment / 10 - upper_threshold = order_size * (1 + threshold) - lower_threshold = order_size / (1 + threshold) + threshold = self.increment / 10 + upper_threshold = order_size * (1 + threshold) + lower_threshold = order_size / (1 + threshold) + + if self.is_sell_order(order): lowest_sell_order = orders[0] highest_sell_order = orders[-1] @@ -361,11 +364,6 @@ def is_order_size_correct(self, order, orders): return True return False elif self.is_buy_order(order): - order_size = order['quote']['amount'] - threshold = self.increment / 10 - upper_threshold = order_size * (1 + threshold) - lower_threshold = order_size / (1 + threshold) - lowest_buy_order = orders[-1] highest_buy_order = orders[0] @@ -411,25 +409,25 @@ def place_higher_buy_order(self, order, place_order=True): if place_order: self.market_buy(amount, price) - else: - return {"amount": amount, "price": price} + + return {"amount": amount, "price": price} def place_higher_sell_order(self, order, place_order=True): """ Place higher sell order Mode: MOUNTAIN - amount (QUOTE) = higher_sell_order_amount / (1 + increment) + amount (BASE) = higher_sell_order_amount / (1 + increment) price (BASE) = higher_sell_order_price * (1 + increment) :param order: highest_sell_order :param bool | place_order: True = Places order to the market, False = returns amount and price """ - amount = order['quote']['amount'] / (1 + self.increment) - price = order['price'] * (1 + self.increment) + amount = order['base']['amount'] / (1 + self.increment) + price = (order['price'] ** -1) * (1 + self.increment) if place_order: self.market_sell(amount, price) - else: - return {"amount": amount, "price": price} + + return {"amount": amount, "price": price} def place_lower_buy_order(self, order, place_order=True): """ Place lower buy order @@ -451,25 +449,26 @@ def place_lower_buy_order(self, order, place_order=True): def place_lower_sell_order(self, order, place_order=True): """ Place lower sell order Mode: MOUNTAIN - amount (QUOTE) = higher_sell_order_amount + amount (BASE) = higher_sell_order_amount price (BASE) = higher_sell_order_price / (1 + increment) :param order: Previously higher sell order :param bool | place_order: True = Places order to the market, False = returns amount and price """ - amount = order['quote']['amount'] - price = order['price'] / (1 + self.increment) + amount = order['base']['amount'] + price = (order['price'] ** -1) / (1 + self.increment) if place_order: self.market_sell(amount, price) - else: - return {"amount": amount, "price": price} + + return {"amount": amount, "price": price} def place_highest_sell_order(self, quote_balance, place_order=True, market_center_price=None): """ Places sell order furthest to the market center price Mode: MOUNTAIN :param Amount | quote_balance: Available QUOTE asset balance :param bool | place_order: True = Places order to the market, False = returns amount and price + :param float | market_center_price: Optional market center price, used to to check order :return dict | order: Returns highest sell order """ # Todo: Fix edge case where CP is close to upper bound and will go over. @@ -489,7 +488,6 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente price = price * (1 + self.increment) amount = amount / (1 + self.increment) else: - # Todo: Fix precision to match wanted asset precision = self.market['quote']['precision'] amount = int(float(previous_amount) * 10 ** precision) / (10 ** precision) price = previous_price @@ -497,7 +495,6 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente if place_order: self.market_sell(amount, price) else: - amount = truncate(amount, precision) return {"amount": amount, "price": price} def place_lowest_buy_order(self, base_balance, place_order=True, market_center_price=None): From d114c9d354589697837009cd7a1f81e2d6d4313f Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 15 Aug 2018 16:01:45 +0300 Subject: [PATCH 0527/1846] Change relative orders ConfigElement order --- dexbot/strategies/relative_orders.py | 12 ++++++------ dexbot/views/strategy_form.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 1acd75896..ee8ec04e0 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -11,19 +11,19 @@ class Strategy(BaseStrategy): @classmethod def configure(cls, return_base_config=True): return BaseStrategy.configure(return_base_config) + [ - ConfigElement('relative_order_size', 'bool', False, 'Relative order size', - 'Amount is expressed as a percentage of the account balance of quote/base asset', None), ConfigElement('amount', 'float', 1, 'Amount', 'Fixed order size, expressed in quote asset, unless "relative order size" selected', (0, None, 8, '')), - ConfigElement('center_price_dynamic', 'bool', True, 'Dynamic center price', - 'Always calculate the middle from the closest market orders', None), + ConfigElement('relative_order_size', 'bool', False, 'Relative order size', + 'Amount is expressed as a percentage of the account balance of quote/base asset', None), + ConfigElement('spread', 'float', 5, 'Spread', + 'The percentage difference between buy and sell', (0, 100, 2, '%')), ConfigElement('center_price', 'float', 0, 'Center price', 'Fixed center price expressed in base asset: base/quote', (0, None, 8, '')), + ConfigElement('center_price_dynamic', 'bool', True, 'Update center price from closest market orders', + 'Always calculate the middle from the closest market orders', None), ConfigElement('center_price_offset', 'bool', False, 'Center price offset based on asset balances', 'Automatically adjust orders up or down based on the imbalance of your assets', None), - ConfigElement('spread', 'float', 5, 'Spread', - 'The percentage difference between buy and sell', (0, 100, 2, '%')), ConfigElement('manual_offset', 'float', 0, 'Manual center price offset', "Manually adjust orders up or down. " "Works independently of other offsets and doesn't override them", (-50, 100, 2, '%')) diff --git a/dexbot/views/strategy_form.py b/dexbot/views/strategy_form.py index 981937159..099d57d43 100644 --- a/dexbot/views/strategy_form.py +++ b/dexbot/views/strategy_form.py @@ -116,7 +116,7 @@ def _add_label_wrap(self, key): layout = QtWidgets.QHBoxLayout(wrap) layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) + layout.setSpacing(6) element_name = ''.join([key, '_wrap']) wrap.setObjectName(element_name) From 49f2d3dff0e00c564123a4a006044e94db4efeff Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 15 Aug 2018 16:20:14 +0300 Subject: [PATCH 0528/1846] Fix amount value displaying problem in relative orders options --- dexbot/controllers/strategy_controller.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index e9c143653..a02d024cc 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -117,13 +117,11 @@ def order_size_input_to_relative(self): self.view.strategy_widget.amount_input.setDecimals(2) self.view.strategy_widget.amount_input.setMaximum(100.00) self.view.strategy_widget.amount_input.setMinimumWidth(170) - self.view.strategy_widget.amount_input.setValue(10.00) 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) def validation_errors(self): error_texts = [] From b6dbe254c419bbfc971e9af31d52e8367c427c93 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 15 Aug 2018 16:21:23 +0300 Subject: [PATCH 0529/1846] Change dexbot version number to 0.5.11 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index c0ea7099b..c36a50f13 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.5.10' +VERSION = '0.5.11' AUTHOR = 'Codaone Oy' __version__ = VERSION From ecc749ef8fe6ebc1d1223b963fec2d3faf4de65c Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 15 Aug 2018 16:25:53 +0300 Subject: [PATCH 0530/1846] Fix typo in basestrategy --- dexbot/basestrategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 85a7a47bd..ac7227b2a 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -273,7 +273,7 @@ def calculate_center_price(self, center_price=None, asset_offset=False, spread=N @property def orders(self): - """ Return the worker's open accounts in the current market + """ Return the account's open orders in the current market """ self.account.refresh() return [o for o in self.account.openorders if self.worker["market"] == o.market and self.account.openorders] From 9eac583bd1869c9e505aac6a9eb2e0baa3131362 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 15 Aug 2018 16:27:31 +0300 Subject: [PATCH 0531/1846] Change dexbot version number to 0.5.12 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index c0ea7099b..5e9b6061e 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.5.10' +VERSION = '0.5.12' AUTHOR = 'Codaone Oy' __version__ = VERSION From c9f139e393caad042b70344a3954fd095d37e420 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 14 Aug 2018 19:03:46 +0500 Subject: [PATCH 0532/1846] Implement different triggers to reset orders - Always reset orders whether it was a filled, expired or manually cancelled order detected - Reset on partial fill (with % threshold, optional) - Allow to define custom order expiration time - Reset orders on market trade (optional) - Reset orders on center price change threshold (optional) - Also disable log message about correct orders to remove log noise - Writing of a trade log entry is disabled because we cannot properly distinguish an expired order from a filled one Closes: #226, #270 --- dexbot/controllers/strategy_controller.py | 41 ++++++++ dexbot/strategies/relative_orders.py | 119 +++++++++++++++++++--- 2 files changed, 147 insertions(+), 13 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index a02d024cc..b8f2a3068 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -93,6 +93,15 @@ def __init__(self, view, configure, worker_controller, worker_data): self.view.strategy_widget.center_price_dynamic_input.toggled.connect( self.onchange_center_price_dynamic_input ) + self.view.strategy_widget.reset_on_partial_fill_input.toggled.connect( + self.onchange_reset_on_partial_fill_input + ) + self.view.strategy_widget.reset_on_price_change_input.toggled.connect( + self.onchange_reset_on_price_change_input + ) + self.view.strategy_widget.custom_expiration_input.toggled.connect( + self.onchange_custom_expiration_input + ) # Do this after the event connecting super().__init__(view, configure, worker_controller, worker_data) @@ -100,6 +109,15 @@ def __init__(self, view, configure, worker_controller, worker_data): if not self.view.strategy_widget.center_price_dynamic_input.isChecked(): self.view.strategy_widget.center_price_input.setDisabled(False) + if not self.view.strategy_widget.reset_on_partial_fill_input.isChecked(): + self.view.strategy_widget.partial_fill_threshold_input.setDisabled(True) + + if not self.view.strategy_widget.reset_on_price_change_input.isChecked(): + self.view.strategy_widget.price_change_threshold_input.setDisabled(True) + + if not self.view.strategy_widget.custom_expiration_input.isChecked(): + self.view.strategy_widget.expiration_time_input.setDisabled(True) + def onchange_relative_order_size_input(self, checked): if checked: self.order_size_input_to_relative() @@ -109,8 +127,31 @@ def onchange_relative_order_size_input(self, checked): def onchange_center_price_dynamic_input(self, checked): if checked: self.view.strategy_widget.center_price_input.setDisabled(True) + self.view.strategy_widget.reset_on_price_change_input.setDisabled(False) + if self.view.strategy_widget.reset_on_price_change_input.isChecked(): + self.view.strategy_widget.price_change_threshold_input.setDisabled(False) else: self.view.strategy_widget.center_price_input.setDisabled(False) + self.view.strategy_widget.reset_on_price_change_input.setDisabled(True) + self.view.strategy_widget.price_change_threshold_input.setDisabled(True) + + def onchange_reset_on_partial_fill_input(self, checked): + if checked: + self.view.strategy_widget.partial_fill_threshold_input.setDisabled(False) + else: + self.view.strategy_widget.partial_fill_threshold_input.setDisabled(True) + + def onchange_reset_on_price_change_input(self, checked): + if checked: + self.view.strategy_widget.price_change_threshold_input.setDisabled(False) + else: + self.view.strategy_widget.price_change_threshold_input.setDisabled(True) + + def onchange_custom_expiration_input(self, checked): + if checked: + self.view.strategy_widget.expiration_time_input.setDisabled(False) + else: + self.view.strategy_widget.expiration_time_input.setDisabled(True) def order_size_input_to_relative(self): self.view.strategy_widget.amount_input.setSuffix('%') diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index ee8ec04e0..e2249990d 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -1,8 +1,11 @@ import math +from datetime import datetime +from datetime import timedelta from dexbot.basestrategy import BaseStrategy, ConfigElement from dexbot.qt_queue.idle_queue import idle_add +from bitshares.price import FilledOrder class Strategy(BaseStrategy): """ Relative Orders strategy @@ -26,14 +29,33 @@ def configure(cls, return_base_config=True): 'Automatically adjust orders up or down based on the imbalance of your assets', None), ConfigElement('manual_offset', 'float', 0, 'Manual center price offset', "Manually adjust orders up or down. " - "Works independently of other offsets and doesn't override them", (-50, 100, 2, '%')) + "Works independently of other offsets and doesn't override them", (-50, 100, 2, '%')), + ConfigElement('reset_on_partial_fill', 'bool', True, 'Reset orders on partial fill', + 'Reset orders when buy or sell order is partially filled', None), + ConfigElement('partial_fill_threshold', 'float', 30, 'Fill threshold', + 'Fill threshold to reset orders', (0, 100, 2, '%')), + ConfigElement('reset_on_market_trade', 'bool', False, 'Reset orders on market trade', + 'Reset orders when detected a market trade', None), + ConfigElement('reset_on_price_change', 'bool', False, 'Reset orders on center price change', + 'Reset orders when center price is changed more than threshold', None), + ConfigElement('price_change_threshold', 'float', 2, 'Price change threshold', + 'Define center price threshold to react on', (0, 100, 2, '%')), + ConfigElement('custom_expiration', 'bool', False, 'Custom expiration', + 'Override order expiration time to trigger a reset', None), + ConfigElement('expiration_time', 'int', 157680000, 'Order expiration time', + 'Define custom order expiration time to force orders reset more often, seconds', + (30, 157680000, '')) ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.log.info("Initializing Relative Orders") + # Tick counter + self.counter = 0 + # Define Callbacks + self.ontick += self.tick self.onMarketUpdate += self.check_orders self.onAccount += self.check_orders @@ -52,6 +74,18 @@ def __init__(self, *args, **kwargs): self.manual_offset = self.worker.get('manual_offset', 0) / 100 self.order_size = float(self.worker.get('amount', 1)) self.spread = self.worker.get('spread') / 100 + self.is_reset_on_partial_fill = self.worker.get('reset_on_partial_fill', True) + self.partial_fill_threshold = self.worker.get('partial_fill_threshold', 30) / 100 + self.is_reset_on_market_trade = self.worker.get('reset_on_market_trade', False) + self.is_reset_on_price_change = self.worker.get('reset_on_price_change', False) + self.price_change_threshold = self.worker.get('price_change_threshold', 2) / 100 + self.is_custom_expiration = self.worker.get('custom_expiration', False) + + if self.is_custom_expiration: + self.expiration = self.worker.get('expiration_time', self.expiration) + + self.last_check = datetime.now() + self.min_check_interval = 8 self.buy_price = None self.sell_price = None @@ -59,12 +93,27 @@ 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() + + # Check for conflicting settings + if self.is_reset_on_price_change and not self.is_center_price_dynamic: + self.log.error('reset_on_price_change requires Dynamic Center Price') + self.disabled = True + self.update_orders() def error(self, *args, **kwargs): self.cancel_all() self.disabled = True + def tick(self, d): + """ Ticks come in on every block. We need to periodically check orders because cancelled orders + do not triggers a market_update event + """ + if (self.is_reset_on_price_change and not + self.counter % 8): + self.log.debug('checking orders by tick threshold') + self.check_orders('tick') + self.counter += 1 + @property def amount_quote(self): """ Get quote amount, calculate if order size is relative @@ -108,7 +157,7 @@ def calculate_order_prices(self): self.sell_price = self.center_price * math.sqrt(1 + self.spread) def update_orders(self): - self.log.info('Change detected, updating orders') + #self.log.debug('Change detected, updating orders') # Recalculate buy and sell order prices self.calculate_order_prices() @@ -142,33 +191,77 @@ def update_orders(self): if len(order_ids) < 2 and not self.disabled: self.update_orders() - def check_orders(self, *args, **kwargs): + def check_orders(self, event, *args, **kwargs): """ Tests if the orders need updating """ + delta = datetime.now() - self.last_check + + # Only allow to check orders whether minimal time passed + if delta < timedelta(seconds=self.min_check_interval): + self.log.debug('Ignoring market_update event as min_check_interval is not passed') + return + orders = self.fetch_orders() + # Detect complete fill, order expiration, manual cancel, or just init + need_update = False if not orders: - self.update_orders() + need_update = True else: - orders_changed = False - # Loop trough the orders and look for changes for order_id, order in orders.items(): current_order = self.get_order(order_id) if not current_order: - orders_changed = True - self.write_order_log(self.worker_name, order) + need_update = True + self.log.debug('Could not found order on the market, it was filled, expired or cancelled') + # FIXME: writing a log entry is disabled because we cannot distinguish an expired order + # from filled + #self.write_order_log(self.worker_name, order) + elif self.is_reset_on_partial_fill: + # Detect partially filled orders; + # on fresh order 'for_sale' is always equal to ['base']['amount'] + if current_order['for_sale']['amount'] != current_order['base']['amount']: + diff_abs = current_order['base']['amount'] - current_order['for_sale']['amount'] + diff_rel = diff_abs / current_order['base']['amount'] + if diff_rel >= self.partial_fill_threshold: + need_update = True + self.log.info('Partially filled order detected, filled {:.2%}'.format(diff_rel)) + # FIXME: need to write trade operation; possible race condition may occur: while + # we're updating order it may be filled futher so trade log entry will not + # be correct + + if (self.is_reset_on_market_trade and + isinstance(event, FilledOrder)): + self.log.debug('Market trade detected, updating orders') + need_update = True + + if self.is_reset_on_price_change: + center_price = self.calculate_center_price( + None, + self.is_asset_offset, + self.spread, + self['order_ids'], + self.manual_offset + ) + diff = (self.center_price - center_price) / self.center_price + diff = abs(diff) + if diff >= self.price_change_threshold: + self.log.debug('Center price changed, updating orders. Diff: {:.2%}'.format(diff)) + need_update = True - if orders_changed: - self.update_orders() - else: - self.log.info("Orders correct on market") + if need_update: + self.update_orders() + else: + pass + #self.log.debug("Orders correct on market") if self.view: self.update_gui_profit() self.update_gui_slider() + self.last_check = datetime.now() + # GUI updaters def update_gui_profit(self): # Fixme: profit calculation doesn't work this way, figure out a better way to do this. From 99e2b1431f46133e5b4e584564c4381d24da965b Mon Sep 17 00:00:00 2001 From: Ian Haywood Date: Thu, 16 Aug 2018 13:15:03 +1000 Subject: [PATCH 0533/1846] use pkg_resources to allow auto-discovery of third-party strategies --- dexbot/cli_conf.py | 13 ++++++++++++- dexbot/controllers/worker_controller.py | 4 ++++ dexbot/helper.py | 24 ++++++++++++++++++++++++ dexbot/views/strategy_form.py | 19 +++++++++++++------ 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index fedb27d46..e2c79ea95 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -23,8 +23,8 @@ from dexbot.whiptail import get_whiptail from dexbot.basestrategy import BaseStrategy +import dexbot.helper -# FIXME: auto-discovery of strategies would be cool but can't figure out a way STRATEGIES = [ {'tag': 'relative', 'class': 'dexbot.strategies.relative_orders', @@ -33,6 +33,17 @@ 'class': 'dexbot.strategies.staggered_orders', 'name': 'Staggered Orders'}] +tags_so_far = {'stagger', 'relative'} +for desc, module in dexbot.helper.find_external_strategies(): + tag = desc.split()[0].lower() + # make sure tag is unique + i = 1 + while tag in tags_so_far: + tag = tag+str(i) + i += 1 + tags_so_far.add(tag) + STRATEGIES.append({'tag': tag, 'class': module, 'name': desc}) + SYSTEMD_SERVICE_NAME = os.path.expanduser( "~/.local/share/systemd/user/dexbot.service") diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index 04e97674a..180bddbfe 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -3,6 +3,7 @@ from dexbot.views.errors import gui_error from dexbot.config import Config +from dexbot.helper import find_external_strategies from dexbot.views.notice import NoticeDialog from dexbot.views.confirmation import ConfirmationDialog from dexbot.views.strategy_form import StrategyFormWidget @@ -33,6 +34,9 @@ def strategies(self): 'name': 'Staggered Orders', 'form_module': 'dexbot.views.ui.forms.staggered_orders_widget_ui' } + for desc, module in find_external_strategies(): + strategies[module] = {'name': desc, 'form_module': module} + # if there is no UI form in the module then GUI will gracefully revert to auto-ui return strategies @classmethod diff --git a/dexbot/helper.py b/dexbot/helper.py index db00b1081..ab9df8402 100644 --- a/dexbot/helper.py +++ b/dexbot/helper.py @@ -3,6 +3,7 @@ import shutil import errno import logging +import importlib from appdirs import user_data_dir from dexbot import APP_NAME, AUTHOR @@ -57,3 +58,26 @@ def initialize_orders_log(): if not file: logger.info("worker_name;ID;operation_type;base_asset;base_amount;quote_asset;quote_amount;timestamp") + + +try: + # unfortunately setuptools is only "kinda-sorta" a standard module + # it's available on pretty much any modern Python system, but some embedded Pythons may not have it + # so we make it a soft-dependency + import pkg_resources + + def find_external_strategies(): + """Use setuptools introspection to find third-party strategies the user may have installed. + Packages that provide a strategy should export a setuptools "entry point" (see setuptools docs) + with group "dexbot.strategy", "name" is the display name of the strategy. + Only set the module not any attribute (because it would always be a class called "Strategy") + If you want a handwritten graphical UI, define "Ui_Form" and "StrategyController" in the same module + + yields a 2-tuple: description, module name""" + for entry_point in pkg_resources.iter_entry_points("dexbot.strategy"): + yield (entry_point.name, entry_point.module_name) + +except ImportError: + # our system doesn't have setuptools, so no way to find external strategies + def find_external_strategies(): + return [] diff --git a/dexbot/views/strategy_form.py b/dexbot/views/strategy_form.py index 981937159..745b4e7dc 100644 --- a/dexbot/views/strategy_form.py +++ b/dexbot/views/strategy_form.py @@ -35,17 +35,24 @@ def __init__(self, controller, strategy_module, worker_config=None): class_name = ''.join([class_name, 'Controller']) try: - # Try to get the controller + # Try to get the controller from the internal set strategy_controller = getattr( dexbot.controllers.strategy_controller, class_name ) except AttributeError: - # The controller doesn't exist, use the default controller - strategy_controller = getattr( - dexbot.controllers.strategy_controller, - 'StrategyController' - ) + try: + # look in the strategy module itself (external strategies may do this) + strategy_controller = getattr( + importlib.import_module(strategy_module), + 'StrategyController' + ) + except AttributeError: + # The controller doesn't exist, use the default controller + strategy_controller = getattr( + dexbot.controllers.strategy_controller, + 'StrategyController' + ) self.strategy_controller = strategy_controller(self, configure, controller, worker_config) From fae970b1e7d0f1e748b4ca0faf40962382de682d Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 16 Aug 2018 16:14:30 +0500 Subject: [PATCH 0534/1846] Avoid buy/sell 0 in relative_orders.py --- dexbot/strategies/relative_orders.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index e2249990d..567d19cb1 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -167,28 +167,33 @@ def update_orders(self): self.clear_orders() order_ids = [] + expected_num_orders = 0 amount_base = self.amount_base amount_quote = self.amount_quote # Buy Side - buy_order = self.market_buy(amount_base, self.buy_price, True) - if buy_order: - self.save_order(buy_order) - order_ids.append(buy_order['id']) + if amount_base: + buy_order = self.market_buy(amount_base, self.buy_price, True) + if buy_order: + self.save_order(buy_order) + order_ids.append(buy_order['id']) + expected_num_orders += 1 # Sell Side - sell_order = self.market_sell(amount_quote, self.sell_price, True) - if sell_order: - self.save_order(sell_order) - order_ids.append(sell_order['id']) + if amount_quote: + sell_order = self.market_sell(amount_quote, self.sell_price, True) + if sell_order: + self.save_order(sell_order) + order_ids.append(sell_order['id']) + expected_num_orders += 1 self['order_ids'] = order_ids self.log.info("Done placing orders") # Some orders weren't successfully created, redo them - if len(order_ids) < 2 and not self.disabled: + if len(order_ids) < expected_num_orders and not self.disabled: self.update_orders() def check_orders(self, event, *args, **kwargs): From ce36d71a9f1000355401dce6aa9c25df04226be1 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 17 Aug 2018 10:41:24 +0300 Subject: [PATCH 0535/1846] Fix crash when calculating center price with bid 0 --- dexbot/basestrategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 6d5cb141e..e93f646eb 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -207,7 +207,7 @@ def _calculate_center_price(self, suppress_errors=False): ticker = self.market.ticker() highest_bid = ticker.get("highestBid") lowest_ask = ticker.get("lowestAsk") - if not highest_bid: + if highest_bid is None or highest_bid == 0.0: if not suppress_errors: self.log.critical( "Cannot estimate center price, there is no highest bid." From 0797d2b28adcb545548839ecabd39ef1af676441 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 17 Aug 2018 10:42:47 +0300 Subject: [PATCH 0536/1846] Change lower sell order amount --- dexbot/strategies/staggered_orders.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index e4083306a..d35501102 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -64,6 +64,7 @@ def __init__(self, *args, **kwargs): self.view = kwargs.get('view') self.mode = self.worker['mode'] self.target_spread = self.worker['spread'] / 100 + self.center_price = self.worker['center_price'] self.increment = self.worker['increment'] / 100 self.upper_bound = self.worker['upper_bound'] self.lower_bound = self.worker['lower_bound'] @@ -449,13 +450,13 @@ def place_lower_buy_order(self, order, place_order=True): def place_lower_sell_order(self, order, place_order=True): """ Place lower sell order Mode: MOUNTAIN - amount (BASE) = higher_sell_order_amount + amount (BASE) = higher_sell_order_amount * (1 + increment) price (BASE) = higher_sell_order_price / (1 + increment) :param order: Previously higher sell order :param bool | place_order: True = Places order to the market, False = returns amount and price """ - amount = order['base']['amount'] + amount = order['base']['amount'] * (1 + self.increment) price = (order['price'] ** -1) / (1 + self.increment) if place_order: From 331a4e9e042d6e0a5866521d076e05fc559fd23c Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Fri, 17 Aug 2018 14:47:19 +0300 Subject: [PATCH 0537/1846] Change dexbot version number to 0.5.13 --- dexbot/__init__.py | 2 +- dexbot/helper.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index c0ea7099b..c8b3af190 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.5.10' +VERSION = '0.5.13' AUTHOR = 'Codaone Oy' __version__ = VERSION diff --git a/dexbot/helper.py b/dexbot/helper.py index ab9df8402..8812cd7c4 100644 --- a/dexbot/helper.py +++ b/dexbot/helper.py @@ -3,7 +3,6 @@ import shutil import errno import logging -import importlib from appdirs import user_data_dir from dexbot import APP_NAME, AUTHOR @@ -61,7 +60,7 @@ def initialize_orders_log(): try: - # unfortunately setuptools is only "kinda-sorta" a standard module + # Unfortunately setuptools is only "kinda-sorta" a standard module # it's available on pretty much any modern Python system, but some embedded Pythons may not have it # so we make it a soft-dependency import pkg_resources @@ -78,6 +77,6 @@ def find_external_strategies(): yield (entry_point.name, entry_point.module_name) except ImportError: - # our system doesn't have setuptools, so no way to find external strategies + # Our system doesn't have setuptools, so no way to find external strategies def find_external_strategies(): return [] From cf99c75e626b63629d14924a9073391ec87a6360 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 17 Aug 2018 15:29:58 +0300 Subject: [PATCH 0538/1846] Change few comments and remove unused import --- dexbot/strategies/staggered_orders.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index d35501102..910ec87ce 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -3,7 +3,6 @@ from dexbot.basestrategy import BaseStrategy, ConfigElement from dexbot.qt_queue.idle_queue import idle_add -from dexbot.helper import truncate class Strategy(BaseStrategy): @@ -11,7 +10,6 @@ class Strategy(BaseStrategy): @classmethod def configure(cls, return_base_config=True): - # Todo: - Modes don't list in worker add / edit # Todo: - Add other modes modes = [ ('mountain', 'Mountain'), @@ -92,7 +90,6 @@ def maintain_strategy(self, *args, **kwargs): """ # Calculate market center price - # Todo: Move market_center_price to another place? It will be recalculated on each loop now. self.market_center_price = self.calculate_center_price(suppress_errors=True) # Loop until center price appears on the market @@ -143,7 +140,6 @@ def maintain_strategy(self, *args, **kwargs): order_ids = [order['id'] for order in orders] orders_balance = self.orders_balance(order_ids) - # Balance per asset from orders and account balance quote_orders_balance = orders_balance['quote'] + quote_balance['amount'] base_orders_balance = orders_balance['base'] + base_balance['amount'] @@ -181,8 +177,10 @@ def maintain_mountain_mode(self): pass def allocate_base_asset(self, base_balance, *args, **kwargs): - """ Allocates base asset + """ Allocates available base asset as buy orders. :param base_balance: Amount of the base asset available to use + :param args: + :param kwargs: """ if self.buy_orders: # Get currently the lowest and highest buy orders @@ -203,15 +201,19 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): # Cancel highest buy order self.cancel(self.buy_orders[0]) else: - # Place first buy order to the market + # Place first buy order as close to the lower bound as possible self.place_lowest_buy_order(base_balance) def allocate_quote_asset(self, quote_balance, *args, **kwargs): - """ Allocates quote asset + """ Allocates available quote asset as sell orders. + :param quote_balance: Amount of the base asset available to use + :param args: + :param kwargs: """ if self.sell_orders: lowest_sell_order = self.sell_orders[0] highest_sell_order = self.sell_orders[-1] + # Sell price is inverted so it can be compared to the upper bound highest_sell_order_price = (highest_sell_order['price'] ** -1) # Check if the order size is correct @@ -228,6 +230,7 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): # Cancel lowest sell order self.cancel(self.sell_orders[0]) else: + # Place first order as close to the upper bound as possible self.place_highest_sell_order(quote_balance) # Todo: Check completely From a249f5691ca06e919eb6fa6c71f733d25d02c5cf Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 17 Aug 2018 15:30:27 +0300 Subject: [PATCH 0539/1846] Add calculation for fee reserve --- dexbot/strategies/staggered_orders.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 910ec87ce..34cf91125 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -137,6 +137,17 @@ def maintain_strategy(self, *args, **kwargs): base_balance = account_balances['base'] quote_balance = account_balances['quote'] + # Reserve transaction fee equivalent in BTS + ticker = self.market.ticker() + core_exchange_rate = ticker['core_exchange_rate'] + # Todo: order_creation_fee(BTS) = 0.01 for now + quote_fee_reserve = 0.01 * core_exchange_rate['quote']['amount'] * 100 + base_fee_reserve = 0.01 * core_exchange_rate['base']['amount'] * 100 + + base_balance['amount'] = base_balance['amount'] - base_fee_reserve + quote_balance['amount'] = quote_balance['amount'] - quote_fee_reserve + + # Balance per asset from orders and account balance order_ids = [order['id'] for order in orders] orders_balance = self.orders_balance(order_ids) From 5aa37c329ad7c646cc333cc7faa59470d3392138 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 17 Aug 2018 15:51:08 +0300 Subject: [PATCH 0540/1846] WIP Add increase order sizes buy / sell side --- dexbot/strategies/staggered_orders.py | 208 +++++++++++++++----------- 1 file changed, 123 insertions(+), 85 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 34cf91125..933bfe5f0 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -90,6 +90,7 @@ def maintain_strategy(self, *args, **kwargs): """ # Calculate market center price + # Todo: Use user's center price if included self.market_center_price = self.calculate_center_price(suppress_errors=True) # Loop until center price appears on the market @@ -204,8 +205,8 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): # Place order closer to the center price self.place_higher_buy_order(highest_buy_order) elif lowest_buy_order['price'] / (1 + self.increment) < self.lower_bound: - # Todo: Work in progress. - self.increase_order_sizes('base') + # Lower bound has been reached and now will start allocating rest of the base balance. + self.increase_order_sizes('base', base_balance, self.buy_orders) else: self.place_lower_buy_order(lowest_buy_order) else: @@ -233,8 +234,8 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): # Place order closer to the center price self.place_lower_sell_order(lowest_sell_order) elif highest_sell_order_price * (1 + self.increment) > self.upper_bound: - # Todo: Work in progress. - self.increase_order_sizes('quote') + # Upper bound has been reached and now will start allocating rest of the quote balance. + self.increase_order_sizes('quote', quote_balance, self.sell_orders) else: self.place_higher_sell_order(highest_sell_order) else: @@ -245,92 +246,129 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): self.place_highest_sell_order(quote_balance) # Todo: Check completely - def increase_order_sizes(self, asset): + def increase_order_sizes(self, asset, asset_balance, orders): + # Todo: Change asset or separate buy / sell in different functions? """ Checks which order should be increased in size and replaces it with a maximum size order, according to global limits. Logic depends on mode in question + + :param str | asset: 'base' or 'quote', depending if checking sell or buy + :param float | asset_balance: Balance of the account + :param list | orders: List of buy or sell orders + :return None """ - pass # Mountain mode: - # if self.mode == 'mountain': - # if asset == quote: - # """ Starting from lowest order, for each order, see if it is approximately - # maximum size. - # If it is, move on to next. - # If not, cancel it and replace with maximum size order. Then return. - # If highest_sell_order is reached, increase it to maximum size - # - # Maximum size is: - # as many quote as the order below - # and - # as many quote * (1 + increment) as the order above - # When making an order, it must not exceed either of these limits, but be - # made according to the more limiting criteria. - # """ - # # get orders and amounts to be compared - # higher_order_number = 1 - # observe_order_number = 0 - # lower_order_number = 0 - # - # can_be_increased = False - # - # # see if order size can be increased - # while not can_be_increased: - # higher_order = self.sell_orders[higher_order_number] - # observe_order = self.sell_orders[observe_order_number] - # if observe_order_number == 0: - # lower_order = self.buy_order[0] - # else: - # lower_order = self.sell_orders[lower_order_number] - # observe_order_amount = observe_order['quote']['amount'] - # limit_from_below = lower_order['quote']['amount'] - # limit_from_above = higher_order['quote']['amount'] * (1 + self.increment) - # - # if limit_from_below >= observe_order_amount * (1 + self.increment / 10) <= limit_from_above: - # can_be_increased = True - # else: - # observe_order_number += 1 - # higher_order_number = observe_order_number + 1 - # lower_order_number = observe_order_number - 1 - # continue - # - # # calculate new order size and make order - # - # if limit_from_above > limit_from_below: - # new_order_amount = limit_from_below - # else: - # new_order_amount = limit_from_above - # - # if quote_balance - reserve_quote_amount < new_order_amount - observe_order_amount: - # new_order_amount = observe_order_amount + quote_balance - reserve_quote_amount - # - # price = observe_order['price'] - # self.cancel(observe_order) - # self.market_sell(new_order_amount, price) - # - # elif asset == base: - # """ Starting from highest order, for each order, see if it is approximately - # maximum size. - # If it is, move on to next. - # If not, cancel it and replace with maximum size order. Then return. - # If highest_sell_order is reached, increase it to maximum size - # - # Maximum size is: - # as many base as the order above - # and - # as many base * (1 + increment) as the order below - # When making an order, it must not exceed either of these limits, but be - # made according to the more limiting criteria. - # """ - # elif self.mode == 'valley': - # pass - # elif self.mode == 'neutral': - # pass - # elif self.mode == 'buy_slope': - # pass - # elif self.mode == 'sell_slope': - # pass - # return None + if self.mode == 'mountain': + # Todo: Sell side works fine? + if asset == 'quote': + """ Starting from the lowest SELL order. For each order, see if it is approximately + maximum size. + If it is, move on to next. + If not, cancel it and replace with maximum size order. Then return. + If highest_sell_order is reached, increase it to maximum size + + Maximum size is: + as many quote as the order below + and + as many quote * (1 + increment) as the order above + When making an order, it must not exceed either of these limits, but be + made according to the more limiting criteria. + """ + # Get orders and amounts to be compared + for order in orders: + order_index = orders.index(order) + order_amount = order['base']['amount'] + + # This check prevents choosing order with index lower than the list length + if order_index == 0: + # In case checking the first order, use highest BUY order in comparison + lower_order = self.buy_orders[0] + lower_bound = lower_order['quote']['amount'] + else: + lower_order = orders[order_index - 1] + lower_bound = lower_order['base']['amount'] + + higher_order = orders[order_index] + + # This check prevents choosing order with index higher than the list length + if order_index + 1 < len(orders): + higher_order = orders[order_index + 1] + + higher_bound = higher_order['base']['amount'] * (1 + self.increment) + + if lower_bound >= order_amount * (1 + self.increment / 10) <= higher_bound: + # Calculate new order size and place the order to the market + new_order_amount = higher_bound + + if higher_bound > lower_bound: + new_order_amount = lower_bound + + if asset_balance < new_order_amount - order_amount: + new_order_amount = order_amount + asset_balance + + price = (order['price'] ** -1) + self.cancel(order) + self.market_sell(new_order_amount, price) + elif asset == 'base': + # Todo: Work in progress + """ Starting from the highest BUY order, for each order, see if it is approximately + maximum size. + If it is, move on to next. + If not, cancel it and replace with maximum size order. Then return. + If lowest_buy_order is reached, increase it to maximum size + + Maximum size is: + as many quote as the order above + and + as many quote * (1 + increment) as the order below + When making an order, it must not exceed either of these limits, but be + made according to the more limiting criteria. + """ + # Get orders and amounts to be compared + for order in orders: + order_index = orders.index(order) + order_amount = order['quote']['amount'] + + # This check prevents choosing order with index lower than the list length + if order_index == 0: + # In case checking the first order, use lowest SELL order in comparison + higher_order = self.sell_orders[0] + higher_bound = higher_order['base']['amount'] + else: + higher_order = orders[order_index - 1] + higher_bound = higher_order['quote']['amount'] + + # Lower order + lower_order = orders[order_index] + + # This check prevents choosing order with index higher than the list length + if order_index + 1 < len(orders): + lower_order = orders[order_index + 1] + + lower_bound = lower_order['quote']['amount'] * (1 + self.increment) + + if lower_bound >= order_amount * (1 + self.increment / 10) <= higher_bound: + # Calculate new order size and place the order to the market + amount = higher_bound + + if higher_bound > lower_bound: + amount = lower_bound + + if asset_balance < amount - order_amount: + amount = order_amount + asset_balance + + price = order['price'] + self.cancel(order) + self.market_buy(amount, price) + elif self.mode == 'valley': + pass + elif self.mode == 'neutral': + pass + elif self.mode == 'buy_slope': + pass + elif self.mode == 'sell_slope': + pass + return None def is_order_size_correct(self, order, orders): """ Checks if the order size is correct From 7b6d2188244bca3082c952f5a87841a5fb04c7ae Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 16 Aug 2018 16:46:26 +0500 Subject: [PATCH 0541/1846] Preserve --configfile when running `cli.py configure` When editing a worker Basestrategy reads worker config but it used default config path. Fix this by passing correct config. --- dexbot/cli_conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index e2c79ea95..e2ca4c9b1 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -217,13 +217,13 @@ def configure_dexbot(config, ctx): worker_name = whiptail.menu("Select worker to edit", [(i, i) for i in workers]) config['workers'][worker_name] = configure_worker(whiptail, config['workers'][worker_name]) - strategy = BaseStrategy(worker_name, bitshares_instance=bitshares_instance) + strategy = BaseStrategy(worker_name, bitshares_instance=bitshares_instance, config=config) strategy.purge() elif action == 'DEL': worker_name = whiptail.menu("Select worker to delete", [(i, i) for i in workers]) del config['workers'][worker_name] - strategy = BaseStrategy(worker_name, bitshares_instance=bitshares_instance) + strategy = BaseStrategy(worker_name, bitshares_instance=bitshares_instance, config=config) strategy.purge() elif action == 'NEW': txt = whiptail.prompt("Your name for the new worker") From 590a159deb79a8ab6cb5d6e3c940a24418deff02 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 19 Aug 2018 13:16:35 +0500 Subject: [PATCH 0542/1846] Be aware of fee_reserve when placing lowest_buy and highest_sell Without this highest_sell or lowest_buy may be constantly cancelled and placed back because is_order_size_correct() will not threat this order as correct sized. --- dexbot/strategies/staggered_orders.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 933bfe5f0..6e26c7459 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -74,6 +74,8 @@ def __init__(self, *args, **kwargs): self.sell_orders = [] self.actual_spread = self.target_spread + 1 self.market_spread = 0 + self.base_fee_reserve = None + self.quote_fee_reserve = None # Order expiration time self.expiration = 60 * 60 * 24 * 365 * 5 @@ -142,11 +144,11 @@ def maintain_strategy(self, *args, **kwargs): ticker = self.market.ticker() core_exchange_rate = ticker['core_exchange_rate'] # Todo: order_creation_fee(BTS) = 0.01 for now - quote_fee_reserve = 0.01 * core_exchange_rate['quote']['amount'] * 100 - base_fee_reserve = 0.01 * core_exchange_rate['base']['amount'] * 100 + self.quote_fee_reserve = 0.01 * core_exchange_rate['quote']['amount'] * 100 + self.base_fee_reserve = 0.01 * core_exchange_rate['base']['amount'] * 100 - base_balance['amount'] = base_balance['amount'] - base_fee_reserve - quote_balance['amount'] = quote_balance['amount'] - quote_fee_reserve + base_balance['amount'] = base_balance['amount'] - self.base_fee_reserve + quote_balance['amount'] = quote_balance['amount'] - self.quote_fee_reserve # Balance per asset from orders and account balance order_ids = [order['id'] for order in orders] @@ -394,7 +396,8 @@ def is_order_size_correct(self, order, orders): # Order is the only sell order, and size must be calculated like initializing if lowest_sell_order == highest_sell_order: total_balance = self.total_balance(orders, return_asset=True) - highest_sell_order = self.place_highest_sell_order(total_balance['quote'], + quote_balance = total_balance['quote'] - self.quote_fee_reserve + highest_sell_order = self.place_highest_sell_order(quote_balance, place_order=False, market_center_price=self.initial_market_center_price) @@ -423,7 +426,8 @@ def is_order_size_correct(self, order, orders): # Order is the only buy order, and size must be calculated like initializing if highest_buy_order == lowest_buy_order: total_balance = self.total_balance(orders, return_asset=True) - lowest_buy_order = self.place_lowest_buy_order(total_balance['base'], + base_balance = total_balance['base'] - self.base_fee_reserve + lowest_buy_order = self.place_lowest_buy_order(base_balance, place_order=False, market_center_price=self.initial_market_center_price) From e6c92e0c486008b981282020073e11d4e6d9441c Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 19 Aug 2018 15:42:48 +0500 Subject: [PATCH 0543/1846] Cancel orders that exceed boundaries This version is based on Marko Paasila code but improved further to try a batch cancel of orders and fallback to single order cancel in case of error with following return() to avoid GUI freezes. --- dexbot/basestrategy.py | 9 ++++-- dexbot/strategies/staggered_orders.py | 41 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index e93f646eb..146b7968a 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -480,8 +480,10 @@ def _cancel(self, orders): return True - def cancel(self, orders): + def cancel(self, orders, batch_only=False): """ Cancel specific order(s) + :param list orders: list of orders to cancel + :param bool batch_only: try cancel orders only in batch mode without one-by-one fallback """ if not isinstance(orders, (list, set, tuple)): orders = [orders] @@ -489,10 +491,13 @@ def cancel(self, orders): orders = [order['id'] for order in orders if 'id' in order] success = self._cancel(orders) - if not success and len(orders) > 1: + if not success and batch_only: + return False + if not success and len(orders) > 1 and not batch_only: # One of the order cancels failed, cancel the orders one by one for order in orders: self._cancel(order) + return True def cancel_all(self): """ Cancel all orders of the worker's account diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 6e26c7459..af7c487e2 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -167,6 +167,11 @@ def maintain_strategy(self, *args, **kwargs): elif self.market_center_price < self.lower_bound: self.lower_bound = self.market_center_price + # Remove orders that exceed boundaries + success = self.remove_outside_orders(self.sell_orders, self.buy_orders) + if not success: + return + # BASE asset check if base_balance > base_asset_threshold: # Allocate available funds @@ -183,6 +188,42 @@ def maintain_strategy(self, *args, **kwargs): # Cancel highest sell order self.cancel(self.sell_orders[-1]) + def remove_outside_orders(self, sell_orders, buy_orders): + """ Remove orders that exceed boundaries + :param list | sell_orders: our sell orders + :param list | buy_orders: our buy orders + """ + orders_to_cancel = [] + + # Remove sell orders that exceed boundaries + for order in sell_orders: + order_price = order['price'] ** -1 + if order_price > self.upper_bound: + self.log.debug('Cancelling sell order outside range: %s', order_price) + orders_to_cancel.append(order) + + # Remove buy orders that exceed boundaries + for order in buy_orders: + order_price = order['price'] + if order_price < self.lower_bound: + self.log.debug('Cancelling buy order outside range: %s', order_price) + orders_to_cancel.append(order) + + if orders_to_cancel: + # We are trying to cancel all orders in one try + success = self.cancel(orders_to_cancel, batch_only=True) + # Batch cancel failed, repeat cancelling only one order + if success: + return True + else: + self.log.debug('Batch cancel failed, failing back to cancelling single order') + self.cancel(orders_to_cancel[0]) + # To avoid GUI hanging cancel only one order and let switch to another worker + return False + + else: + return True + def maintain_mountain_mode(self): """ Mountain mode This structure is not final, but an idea was that each mode has separate function which runs the loop. From 02ddbd8d4da4231e84f6a7bf24991751a36b7e86 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 20 Aug 2018 11:59:30 +0300 Subject: [PATCH 0544/1846] Refactor by removing unnecessary else statement --- dexbot/strategies/staggered_orders.py | 34 +++++++++++++-------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index af7c487e2..0d2c663ab 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -585,15 +585,15 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente price = price * (1 + self.increment) amount = amount / (1 + self.increment) - else: - precision = self.market['quote']['precision'] - amount = int(float(previous_amount) * 10 ** precision) / (10 ** precision) - price = previous_price - if place_order: - self.market_sell(amount, price) - else: - return {"amount": amount, "price": price} + precision = self.market['quote']['precision'] + amount = int(float(previous_amount) * 10 ** precision) / (10 ** precision) + price = previous_price + + if place_order: + self.market_sell(amount, price) + else: + return {"amount": amount, "price": price} def place_lowest_buy_order(self, base_balance, place_order=True, market_center_price=None): """ Places buy order furthest to the market center price @@ -619,17 +619,17 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p price = price / (1 + self.increment) amount = amount / (1 + self.increment) - else: - precision = self.market['base']['precision'] - amount = previous_amount / price - amount = int(float(amount) * 10 ** precision) / (10 ** precision) - price = previous_price + precision = self.market['base']['precision'] + amount = previous_amount / price + amount = int(float(amount) * 10 ** precision) / (10 ** precision) - if place_order: - self.market_buy(amount, price) - else: - return {"amount": amount, "price": price} + price = previous_price + + if place_order: + self.market_buy(amount, price) + else: + return {"amount": amount, "price": price} def error(self, *args, **kwargs): self.disabled = True From d906d0c7e8e08c0718e2d71e62673e43c09e3568 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Mon, 20 Aug 2018 12:05:04 +0300 Subject: [PATCH 0545/1846] Improved place_highest_sell_order() calculation to better use available funds --- dexbot/strategies/staggered_orders.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 933bfe5f0..ce7765162 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -152,8 +152,8 @@ def maintain_strategy(self, *args, **kwargs): order_ids = [order['id'] for order in orders] orders_balance = self.orders_balance(order_ids) - quote_orders_balance = orders_balance['quote'] + quote_balance['amount'] - base_orders_balance = orders_balance['base'] + base_balance['amount'] + self.quote_orders_balance = orders_balance['quote'] + quote_balance['amount'] + self.base_orders_balance = orders_balance['base'] + base_balance['amount'] # Calculate asset thresholds base_asset_threshold = base_orders_balance / 20000 @@ -530,19 +530,22 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente price = market_center_price * math.sqrt(1 + self.target_spread) previous_price = price + orders_sum = 0 amount = quote_balance['amount'] * self.increment previous_amount = amount while price <= self.upper_bound: + orders_sum += previous_amount previous_price = price previous_amount = amount price = price * (1 + self.increment) amount = amount / (1 + self.increment) else: + order_size = previous_amount * (self.quote_orders_balance / orders_sum) precision = self.market['quote']['precision'] - amount = int(float(previous_amount) * 10 ** precision) / (10 ** precision) + amount = int(float(order_size) * 10 ** precision) / (10 ** precision) price = previous_price if place_order: From f9bc2fab8a8451771bb76c6f9c0daa0dd6d98772 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Mon, 20 Aug 2018 12:30:59 +0300 Subject: [PATCH 0546/1846] Improved first order calculation and added earlier fixes to amount calculations --- dexbot/strategies/staggered_orders.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 7afc90640..4240928a5 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -376,10 +376,10 @@ def increase_order_sizes(self, asset, asset_balance, orders): if order_index == 0: # In case checking the first order, use lowest SELL order in comparison higher_order = self.sell_orders[0] - higher_bound = higher_order['base']['amount'] + higher_bound = higher_order['base']['amount'] * (1 + self.increment) else: higher_order = orders[order_index - 1] - higher_bound = higher_order['quote']['amount'] + higher_bound = higher_order['quote']['amount'] * (1 + self.increment) # Lower order lower_order = orders[order_index] @@ -388,19 +388,19 @@ def increase_order_sizes(self, asset, asset_balance, orders): if order_index + 1 < len(orders): lower_order = orders[order_index + 1] - lower_bound = lower_order['quote']['amount'] * (1 + self.increment) + lower_bound = lower_order['quote']['amount'] * (1 + self.increment) * (1 + self.increment) if lower_bound >= order_amount * (1 + self.increment / 10) <= higher_bound: # Calculate new order size and place the order to the market amount = higher_bound + price = order['price'] if higher_bound > lower_bound: amount = lower_bound - if asset_balance < amount - order_amount: - amount = order_amount + asset_balance + if (asset_balance * price) < amount - order_amount: + amount = order_amount + (asset_balance * price) - price = order['price'] self.cancel(order) self.market_buy(amount, price) elif self.mode == 'valley': @@ -612,19 +612,22 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p price = market_center_price / math.sqrt(1 + self.target_spread) previous_price = price + orders_sum = 0 amount = base_balance['amount'] * self.increment previous_amount = amount while price >= self.lower_bound: + orders_sum += previous_amount previous_price = price previous_amount = amount price = price / (1 + self.increment) amount = amount / (1 + self.increment) else: + order_size = previous_amount * (self.base_orders_balance / orders_sum) precision = self.market['base']['precision'] - amount = previous_amount / price + amount = order_size / price amount = int(float(amount) * 10 ** precision) / (10 ** precision) price = previous_price From 2f1fd3d063fbacf4f59154fe9f518a1b38467e01 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 20 Aug 2018 14:12:10 +0300 Subject: [PATCH 0547/1846] WIP Changes to logic --- dexbot/strategies/staggered_orders.py | 44 +++++++++++++++++---------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 0d2c663ab..aa614fe78 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -112,8 +112,8 @@ def maintain_strategy(self, *args, **kwargs): self.sell_orders = self.get_sell_orders('DESC', orders) # Get highest buy and lowest sell prices from orders - highest_buy_price = None - lowest_sell_price = None + highest_buy_price = 0 + lowest_sell_price = 0 if self.buy_orders: highest_buy_price = self.buy_orders[0].get('price') @@ -123,10 +123,6 @@ def maintain_strategy(self, *args, **kwargs): # Invert the sell price to BASE lowest_sell_price = lowest_sell_price ** -1 - # Calculate actual spread - if highest_buy_price and lowest_sell_price: - self.actual_spread = (lowest_sell_price / highest_buy_price) - 1 - # Calculate market spread highest_market_buy = market_orders['bids'][0]['price'] lowest_market_sell = market_orders['asks'][0]['price'] @@ -244,6 +240,11 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): # Check if the order size is correct if self.is_order_size_correct(highest_buy_order, self.buy_orders): + # Calculate actual spread + lowest_sell_price = self.sell_orders[0]['price'] ** -1 + highest_buy_price = highest_buy_order['price'] + self.actual_spread = (lowest_sell_price / highest_buy_price) - 1 + if self.actual_spread >= self.target_spread + self.increment: # Place order closer to the center price self.place_higher_buy_order(highest_buy_order) @@ -259,6 +260,12 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): # Place first buy order as close to the lower bound as possible self.place_lowest_buy_order(base_balance) + # Finally get all the orders again, in case there has been changes + orders = self.orders + + self.buy_orders = self.get_buy_orders('DESC', orders) + self.sell_orders = self.get_sell_orders('DESC', orders) + def allocate_quote_asset(self, quote_balance, *args, **kwargs): """ Allocates available quote asset as sell orders. :param quote_balance: Amount of the base asset available to use @@ -273,6 +280,11 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): # Check if the order size is correct if self.is_order_size_correct(lowest_sell_order, self.sell_orders): + # Calculate actual spread + lowest_sell_price = lowest_sell_order['price'] ** -1 + highest_buy_price = self.buy_orders[0]['price'] + self.actual_spread = (lowest_sell_price / highest_buy_price) - 1 + if self.actual_spread >= self.target_spread + self.increment: # Place order closer to the center price self.place_lower_sell_order(lowest_sell_order) @@ -302,7 +314,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): """ # Mountain mode: if self.mode == 'mountain': - # Todo: Sell side works fine? + # Todo: Work in progress. if asset == 'quote': """ Starting from the lowest SELL order. For each order, see if it is approximately maximum size. @@ -339,7 +351,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): higher_bound = higher_order['base']['amount'] * (1 + self.increment) - if lower_bound >= order_amount * (1 + self.increment / 10) <= higher_bound: + if lower_bound > order_amount * (1 + self.increment / 10) < higher_bound: # Calculate new order size and place the order to the market new_order_amount = higher_bound @@ -376,10 +388,10 @@ def increase_order_sizes(self, asset, asset_balance, orders): if order_index == 0: # In case checking the first order, use lowest SELL order in comparison higher_order = self.sell_orders[0] - higher_bound = higher_order['base']['amount'] + higher_bound = higher_order['base']['amount'] * (1 + self.increment) else: higher_order = orders[order_index - 1] - higher_bound = higher_order['quote']['amount'] + higher_bound = higher_order['quote']['amount'] * (1 + self.increment) # Lower order lower_order = orders[order_index] @@ -388,19 +400,19 @@ def increase_order_sizes(self, asset, asset_balance, orders): if order_index + 1 < len(orders): lower_order = orders[order_index + 1] - lower_bound = lower_order['quote']['amount'] * (1 + self.increment) + lower_bound = lower_order['quote']['amount'] - if lower_bound >= order_amount * (1 + self.increment / 10) <= higher_bound: + if lower_bound > order_amount * (1 + self.increment / 10) < higher_bound: # Calculate new order size and place the order to the market amount = higher_bound + price = order['price'] if higher_bound > lower_bound: amount = lower_bound - if asset_balance < amount - order_amount: - amount = order_amount + asset_balance + if (asset_balance * price) < amount - order_amount: + amount = order_amount + (asset_balance * price) - price = order['price'] self.cancel(order) self.market_buy(amount, price) elif self.mode == 'valley': @@ -414,7 +426,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): return None def is_order_size_correct(self, order, orders): - """ Checks if the order size is correct + """ Checks if the order is big enough. Oversized orders are allowed to enable manual manipulation :param order: Order closest to the center price from buy or sell side :param orders: List of buy or sell orders From c490d809f6844af62abd602b8278dc055519c095 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Mon, 20 Aug 2018 15:24:21 +0300 Subject: [PATCH 0548/1846] Fix a few small but significant errors --- dexbot/strategies/staggered_orders.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 6df877798..34f5bcb74 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -402,7 +402,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): if order_index + 1 < len(orders): lower_order = orders[order_index + 1] - lower_bound = lower_order['quote']['amount'] * (1 + self.increment) * (1 + self.increment) + lower_bound = lower_order['quote']['amount'] * (1 + self.increment) if lower_bound > order_amount * (1 + self.increment / 10) < higher_bound: # Calculate new order size and place the order to the market @@ -636,12 +636,12 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p previous_price = price previous_amount = amount - price = price / (1 + self.increment) + price = price (1 + self.increment) amount = amount / (1 + self.increment) order_size = previous_amount * (self.base_orders_balance / orders_sum) precision = self.market['base']['precision'] - amount = order_size / price + amount = order_size * price amount = int(float(amount) * 10 ** precision) / (10 ** precision) price = previous_price From 3c7f2f5ed3c019c659fd0fb7aebe91839a8d4f04 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Mon, 20 Aug 2018 15:28:11 +0300 Subject: [PATCH 0549/1846] Fix improper fix --- 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 34f5bcb74..594b5b5cd 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -636,7 +636,7 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p previous_price = price previous_amount = amount - price = price (1 + self.increment) + price = price / (1 + self.increment) amount = amount / (1 + self.increment) order_size = previous_amount * (self.base_orders_balance / orders_sum) From f08400ee4d7f6dd198764a7f90b22e67846df71f Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Tue, 21 Aug 2018 07:33:55 +0300 Subject: [PATCH 0550/1846] Update staggered_orders.py --- 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 594b5b5cd..5f33ec20e 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -402,7 +402,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): if order_index + 1 < len(orders): lower_order = orders[order_index + 1] - lower_bound = lower_order['quote']['amount'] * (1 + self.increment) + lower_bound = lower_order['quote']['amount'] if lower_bound > order_amount * (1 + self.increment / 10) < higher_bound: # Calculate new order size and place the order to the market From fe3dfdca041dd2c08938abf1dfb16940b126d493 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Tue, 21 Aug 2018 09:00:20 +0300 Subject: [PATCH 0551/1846] Add replacing orders after canceling one that is too smalll This was a logical flaw where the closest order was removed because it was too small, and an order on the opposite side was unintentionally made --- dexbot/strategies/staggered_orders.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 5f33ec20e..e80cdfbc3 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -258,6 +258,8 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): else: # Cancel highest buy order self.cancel(self.buy_orders[0]) + self.place_higher_buy_order(self.buy_orders[0]) # Shouldn't [0] be the new "highest"? + else: # Place first buy order as close to the lower bound as possible self.place_lowest_buy_order(base_balance) @@ -298,6 +300,8 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): else: # Cancel lowest sell order self.cancel(self.sell_orders[0]) + self.place_lower_sell_order(self.sell_orders[0]) + else: # Place first order as close to the upper bound as possible self.place_highest_sell_order(quote_balance) From 578af993187c2d4afc749ef61e8e2eaa5ae8d292 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Tue, 21 Aug 2018 09:15:27 +0300 Subject: [PATCH 0552/1846] Balance checks for place_some_order() Fixes the error where bigger orders than what available assets allow were being placed. --- dexbot/strategies/staggered_orders.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index e80cdfbc3..e5831dd7d 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -522,6 +522,8 @@ def place_higher_buy_order(self, order, place_order=True): """ amount = order['quote']['amount'] price = order['price'] * (1 + self.increment) + if amount / price > base_balance['amount']: + amount = base_balance['amount'] * price if place_order: self.market_buy(amount, price) @@ -539,6 +541,8 @@ def place_higher_sell_order(self, order, place_order=True): """ amount = order['base']['amount'] / (1 + self.increment) price = (order['price'] ** -1) * (1 + self.increment) + if amount > quote_balance['amount']: + amount : quote_balance['amount'] if place_order: self.market_sell(amount, price) @@ -556,6 +560,8 @@ def place_lower_buy_order(self, order, place_order=True): """ amount = order['quote']['amount'] price = order['price'] / (1 + self.increment) + if amount / price > base_balance['amount']: + amount = base_balance['amount'] * price if place_order: self.market_buy(amount, price) @@ -573,6 +579,8 @@ def place_lower_sell_order(self, order, place_order=True): """ amount = order['base']['amount'] * (1 + self.increment) price = (order['price'] ** -1) / (1 + self.increment) + if amount > quote_balance['amount']: + amount = quote_balance['amount'] if place_order: self.market_sell(amount, price) From 474c1c0e151f59154605f027cb69865f5cedd921 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 21 Aug 2018 12:11:32 +0300 Subject: [PATCH 0553/1846] Refactor basestrategy amount rename quote_amount --- dexbot/basestrategy.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 146b7968a..8f4cc2b38 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -514,10 +514,10 @@ def pause(self): self.cancel_all() self.clear_orders() - def market_buy(self, amount, price, return_none=False, *args, **kwargs): + def market_buy(self, quote_amount, price, return_none=False, *args, **kwargs): symbol = self.market['base']['symbol'] precision = self.market['base']['precision'] - base_amount = truncate(price * amount, precision) + base_amount = truncate(price * quote_amount, precision) # Do not try to buy with 0 balance if not base_amount: @@ -543,7 +543,7 @@ def market_buy(self, amount, price, return_none=False, *args, **kwargs): buy_transaction = self.retry_action( self.market.buy, price, - Amount(amount=amount, asset=self.market["quote"]), + Amount(amount=quote_amount, asset=self.market["quote"]), account=self.account.name, returnOrderId="head", fee_asset=self.fee_asset['id'], @@ -555,15 +555,15 @@ def market_buy(self, amount, price, return_none=False, *args, **kwargs): if buy_order and 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) + buy_order = self.calculate_order_data(buy_order, quote_amount, price) self.recheck_orders = True return buy_order - def market_sell(self, amount, price, return_none=False, *args, **kwargs): + def market_sell(self, quote_amount, price, return_none=False, *args, **kwargs): symbol = self.market['quote']['symbol'] precision = self.market['quote']['precision'] - quote_amount = truncate(amount, precision) + quote_amount = truncate(quote_amount, precision) # Do not try to sell with 0 balance if not quote_amount: @@ -575,7 +575,7 @@ def market_sell(self, amount, price, return_none=False, *args, **kwargs): if self.balance(self.market['quote']) < quote_amount: self.log.critical( "Insufficient sell balance, needed {} {}".format( - amount, symbol) + quote_amount, symbol) ) self.disabled = True return None @@ -589,7 +589,7 @@ def market_sell(self, amount, price, return_none=False, *args, **kwargs): sell_transaction = self.retry_action( self.market.sell, price, - Amount(amount=amount, asset=self.market["quote"]), + Amount(amount=quote_amount, asset=self.market["quote"]), account=self.account.name, returnOrderId="head", fee_asset=self.fee_asset['id'], @@ -601,7 +601,7 @@ def market_sell(self, amount, price, return_none=False, *args, **kwargs): if sell_order and 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 = self.calculate_order_data(sell_order, quote_amount, price) sell_order.invert() self.recheck_orders = True From 77a35821bfc1c446092591d99137aa6634a7a406 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 21 Aug 2018 13:13:23 +0300 Subject: [PATCH 0554/1846] Merge pull request #284 --- dexbot/strategies/staggered_orders.py | 54 +++++++++++++++------------ 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index e5831dd7d..2192e3161 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -78,6 +78,8 @@ def __init__(self, *args, **kwargs): self.quote_fee_reserve = None self.quote_orders_balance = 0 self.base_orders_balance = 0 + self.quote_balance = 0 + self.base_balance = 0 # Order expiration time self.expiration = 60 * 60 * 24 * 365 * 5 @@ -135,8 +137,8 @@ def maintain_strategy(self, *args, **kwargs): # Get current account balances account_balances = self.total_balance(order_ids=[], return_asset=True) - base_balance = account_balances['base'] - quote_balance = account_balances['quote'] + self.base_balance = account_balances['base'] + self.quote_balance = account_balances['quote'] # Reserve transaction fee equivalent in BTS ticker = self.market.ticker() @@ -145,15 +147,15 @@ def maintain_strategy(self, *args, **kwargs): self.quote_fee_reserve = 0.01 * core_exchange_rate['quote']['amount'] * 100 self.base_fee_reserve = 0.01 * core_exchange_rate['base']['amount'] * 100 - base_balance['amount'] = base_balance['amount'] - self.base_fee_reserve - quote_balance['amount'] = quote_balance['amount'] - self.quote_fee_reserve + self.base_balance['amount'] = self.base_balance['amount'] - self.base_fee_reserve + self.quote_balance['amount'] = self.quote_balance['amount'] - self.quote_fee_reserve # Balance per asset from orders and account balance order_ids = [order['id'] for order in orders] orders_balance = self.orders_balance(order_ids) - self.quote_orders_balance = orders_balance['quote'] + quote_balance['amount'] - self.base_orders_balance = orders_balance['base'] + base_balance['amount'] + self.quote_orders_balance = orders_balance['quote'] + self.quote_balance['amount'] + self.base_orders_balance = orders_balance['base'] + self.base_balance['amount'] # Calculate asset thresholds base_asset_threshold = self.base_orders_balance / 20000 @@ -171,17 +173,17 @@ def maintain_strategy(self, *args, **kwargs): return # BASE asset check - if base_balance > base_asset_threshold: + if self.base_balance > base_asset_threshold: # Allocate available funds - self.allocate_base_asset(base_balance) + self.allocate_base_asset(self.base_balance) elif self.market_center_price > highest_buy_price * (1 + self.target_spread): # Cancel lowest buy order self.cancel(self.buy_orders[-1]) # QUOTE asset check - if quote_balance > quote_asset_threshold: + if self.quote_balance > quote_asset_threshold: # Allocate available funds - self.allocate_quote_asset(quote_balance) + self.allocate_quote_asset(self.quote_balance) elif self.market_center_price < lowest_sell_price * (1 - self.target_spread): # Cancel highest sell order self.cancel(self.sell_orders[-1]) @@ -256,15 +258,17 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): else: self.place_lower_buy_order(lowest_buy_order) else: - # Cancel highest buy order - self.cancel(self.buy_orders[0]) - self.place_higher_buy_order(self.buy_orders[0]) # Shouldn't [0] be the new "highest"? - + # Cancel highest buy order and immediately replace it with new one. + self.cancel(highest_buy_order) + # Todo: This can be changed so that it creates new highest immediately. Balance needs to be recalculated + if len(self.buy_orders) > 0: + self.place_higher_buy_order(self.buy_orders[1]) else: # Place first buy order as close to the lower bound as possible self.place_lowest_buy_order(base_balance) # Finally get all the orders again, in case there has been changes + # Todo: Is this necessary? orders = self.orders self.buy_orders = self.get_buy_orders('DESC', orders) @@ -300,8 +304,10 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): else: # Cancel lowest sell order self.cancel(self.sell_orders[0]) - self.place_lower_sell_order(self.sell_orders[0]) - + self.place_lower_sell_order(lowest_sell_order) + # Todo: This can be changed so that it creates new lowest immediately. Balance needs to be recalculated + if len(self.sell_orders) > 0: + self.place_lower_sell_order(self.sell_orders[1]) else: # Place first order as close to the upper bound as possible self.place_highest_sell_order(quote_balance) @@ -522,8 +528,8 @@ def place_higher_buy_order(self, order, place_order=True): """ amount = order['quote']['amount'] price = order['price'] * (1 + self.increment) - if amount / price > base_balance['amount']: - amount = base_balance['amount'] * price + if amount / price > self.base_balance['amount']: + amount = self.base_balance['amount'] * price if place_order: self.market_buy(amount, price) @@ -541,8 +547,8 @@ def place_higher_sell_order(self, order, place_order=True): """ amount = order['base']['amount'] / (1 + self.increment) price = (order['price'] ** -1) * (1 + self.increment) - if amount > quote_balance['amount']: - amount : quote_balance['amount'] + if amount > self.quote_balance['amount']: + amount = self.quote_balance['amount'] if place_order: self.market_sell(amount, price) @@ -560,8 +566,8 @@ def place_lower_buy_order(self, order, place_order=True): """ amount = order['quote']['amount'] price = order['price'] / (1 + self.increment) - if amount / price > base_balance['amount']: - amount = base_balance['amount'] * price + if amount / price > self.base_balance['amount']: + amount = self.base_balance['amount'] * price if place_order: self.market_buy(amount, price) @@ -579,8 +585,8 @@ def place_lower_sell_order(self, order, place_order=True): """ amount = order['base']['amount'] * (1 + self.increment) price = (order['price'] ** -1) / (1 + self.increment) - if amount > quote_balance['amount']: - amount = quote_balance['amount'] + if amount > self.quote_balance['amount']: + amount = self.quote_balance['amount'] if place_order: self.market_sell(amount, price) From 5fe961e76da98496d33dc5be16887354b261c541 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 21 Aug 2018 14:39:54 +0300 Subject: [PATCH 0555/1846] Refactor place_lowest_buy_order amount --- dexbot/strategies/staggered_orders.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 2192e3161..4450f8cab 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -128,6 +128,7 @@ def maintain_strategy(self, *args, **kwargs): lowest_sell_price = lowest_sell_price ** -1 # Calculate market spread + # Todo: Market spread is calculated but never used. Is this needed? highest_market_buy = market_orders['bids'][0]['price'] lowest_market_sell = market_orders['asks'][0]['price'] @@ -657,9 +658,10 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p price = price / (1 + self.increment) amount = amount / (1 + self.increment) - order_size = previous_amount * (self.base_orders_balance / orders_sum) - precision = self.market['base']['precision'] - amount = order_size * price + precision = self.market['quote']['precision'] + amount = previous_amount * (self.base_orders_balance / orders_sum) + # amount / price = amount in QUOTE + amount = amount / price amount = int(float(amount) * 10 ** precision) / (10 ** precision) price = previous_price From 5a735c60fcc88fd198ddaa7a1325494afb7c0654 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 21 Aug 2018 16:01:16 +0300 Subject: [PATCH 0556/1846] Add bootstrapping variable and checks --- dexbot/strategies/staggered_orders.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 4450f8cab..cebe7f3db 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -68,6 +68,7 @@ def __init__(self, *args, **kwargs): self.lower_bound = self.worker['lower_bound'] # Strategy variables + self.bootstrapping = False # Set default True / False? self.market_center_price = None self.initial_market_center_price = None self.buy_orders = [] @@ -178,16 +179,18 @@ def maintain_strategy(self, *args, **kwargs): # Allocate available funds self.allocate_base_asset(self.base_balance) elif self.market_center_price > highest_buy_price * (1 + self.target_spread): - # Cancel lowest buy order - self.cancel(self.buy_orders[-1]) + if not self.bootstrapping: + # Cancel lowest buy order + self.cancel(self.buy_orders[-1]) # QUOTE asset check if self.quote_balance > quote_asset_threshold: # Allocate available funds self.allocate_quote_asset(self.quote_balance) elif self.market_center_price < lowest_sell_price * (1 - self.target_spread): - # Cancel highest sell order - self.cancel(self.sell_orders[-1]) + if not self.bootstrapping: + # Cancel highest sell order + self.cancel(self.sell_orders[-1]) def remove_outside_orders(self, sell_orders, buy_orders): """ Remove orders that exceed boundaries From df0a9865ffa20f99706fd094945baff8d0d22044 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 20 Aug 2018 23:06:53 +0500 Subject: [PATCH 0557/1846] Add debug messages for new staggered_orders.py --- dexbot/strategies/staggered_orders.py | 56 ++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 2192e3161..55e04f54e 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -178,6 +178,7 @@ def maintain_strategy(self, *args, **kwargs): self.allocate_base_asset(self.base_balance) elif self.market_center_price > highest_buy_price * (1 + self.target_spread): # Cancel lowest buy order + self.log.debug('Cancelling lowest buy order in maintain_strategy') self.cancel(self.buy_orders[-1]) # QUOTE asset check @@ -186,6 +187,7 @@ def maintain_strategy(self, *args, **kwargs): self.allocate_quote_asset(self.quote_balance) elif self.market_center_price < lowest_sell_price * (1 - self.target_spread): # Cancel highest sell order + self.log.debug('Cancelling highest sell order in maintain_strategy') self.cancel(self.sell_orders[-1]) def remove_outside_orders(self, sell_orders, buy_orders): @@ -237,6 +239,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): :param args: :param kwargs: """ + self.log.debug('Need to allocate base: %s', base_balance) if self.buy_orders: # Get currently the lowest and highest buy orders lowest_buy_order = self.buy_orders[-1] @@ -251,13 +254,18 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): if self.actual_spread >= self.target_spread + self.increment: # Place order closer to the center price + self.log.debug('Placing higher buy order; actaul spread: %s, target + increment: %s', + self.actual_spread, self.target_spread + self.increment) self.place_higher_buy_order(highest_buy_order) elif lowest_buy_order['price'] / (1 + self.increment) < self.lower_bound: # Lower bound has been reached and now will start allocating rest of the base balance. + self.log.debug('Increasing orders sizes for base asset') self.increase_order_sizes('base', base_balance, self.buy_orders) else: + self.log.debug('Placing lower order than lowest_buy_order') self.place_lower_buy_order(lowest_buy_order) else: + self.log.debug('Order size is not correct, cancelling highest buy order in allocate_base_asset()') # Cancel highest buy order and immediately replace it with new one. self.cancel(highest_buy_order) # Todo: This can be changed so that it creates new highest immediately. Balance needs to be recalculated @@ -265,6 +273,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): self.place_higher_buy_order(self.buy_orders[1]) else: # Place first buy order as close to the lower bound as possible + self.log.debug('Placing first buy order') self.place_lowest_buy_order(base_balance) # Finally get all the orders again, in case there has been changes @@ -280,6 +289,7 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): :param args: :param kwargs: """ + self.log.debug('Need to allocate quote: %s', quote_balance) if self.sell_orders: lowest_sell_order = self.sell_orders[0] highest_sell_order = self.sell_orders[-1] @@ -295,6 +305,8 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): if self.actual_spread >= self.target_spread + self.increment: # Place order closer to the center price + self.log.debug('Placing lower sell order; actaul spread: %s, target + increment: %s', + self.actual_spread, self.target_spread + self.increment) self.place_lower_sell_order(lowest_sell_order) elif highest_sell_order_price * (1 + self.increment) > self.upper_bound: # Upper bound has been reached and now will start allocating rest of the quote balance. @@ -303,6 +315,7 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): self.place_higher_sell_order(highest_sell_order) else: # Cancel lowest sell order + self.log.debug('Order size is not correct, cancelling lowest sell order in allocate_quote_asset') self.cancel(self.sell_orders[0]) self.place_lower_sell_order(lowest_sell_order) # Todo: This can be changed so that it creates new lowest immediately. Balance needs to be recalculated @@ -374,6 +387,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): new_order_amount = order_amount + asset_balance price = (order['price'] ** -1) + self.log.debug('Cancelling sell order in increase_order_sizes(), mode mountain') self.cancel(order) self.market_sell(new_order_amount, price) elif asset == 'base': @@ -426,6 +440,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): amount = order_amount + (asset_balance * price) self.cancel(order) + self.log.debug('Cancelling buy order in increase_order_sizes(), mode mountain') self.market_buy(amount, price) elif self.mode == 'valley': pass @@ -453,6 +468,7 @@ def is_order_size_correct(self, order, orders): threshold = self.increment / 10 upper_threshold = order_size * (1 + threshold) lower_threshold = order_size / (1 + threshold) + #self.log.debug('lower_threshold: %s, upper_threshold: %s', lower_threshold, upper_threshold) if self.is_sell_order(order): lowest_sell_order = orders[0] @@ -469,20 +485,30 @@ def is_order_size_correct(self, order, orders): # Check if the old order is same size with accuracy of 0.1% if lower_threshold <= highest_sell_order['amount'] <= upper_threshold: return True - return False + else: + self.log.debug('lower_threshold <= highest_sell_order <= upper_threshold: %s <= %s <= %s', + lower_threshold, highest_sell_order['amount'], upper_threshold) + return False elif order == highest_sell_order: order_index = orders.index(order) higher_sell_order = self.place_higher_sell_order(orders[order_index - 1], place_order=False) if lower_threshold <= higher_sell_order['amount'] <= upper_threshold: return True - return False + else: + self.log.debug('lower_threshold <= higher_sell_order <= upper_threshold: %s <= %s <= %s', + lower_threshold, higher_sell_order['amount'], upper_threshold) + return False elif order == lowest_sell_order: order_index = orders.index(order) lower_sell_order = self.place_lower_sell_order(orders[order_index + 1], place_order=False) if lower_threshold <= lower_sell_order['amount'] <= upper_threshold: return True + else: + self.log.debug('lower_threshold <= lower_sell_order <= upper_threshold: %s <= %s <= %s', + lower_threshold, lower_sell_order['amount'], upper_threshold) + return False return False elif self.is_buy_order(order): lowest_buy_order = orders[-1] @@ -499,21 +525,30 @@ def is_order_size_correct(self, order, orders): # Check if the old order is same size with accuracy of 0.1% if lower_threshold <= lowest_buy_order['amount'] <= upper_threshold: return True - return False + else: + self.log.debug('lower_threshold <= lowest_buy_order <= upper_threshold: %s <= %s <= %s', + lower_threshold, lowest_buy_order['amount'], upper_threshold) + return False elif order == lowest_buy_order: order_index = orders.index(order) lower_buy_order = self.place_lower_buy_order(orders[order_index - 1], place_order=False) if lower_threshold <= lower_buy_order['amount'] <= upper_threshold: return True - return False + else: + self.log.debug('lower_threshold <= lower_buy_order <= upper_threshold: %s <= %s <=%s', + lower_threshold, lower_buy_order['amount'], upper_threshold) + return False elif order == highest_buy_order: order_index = orders.index(order) higher_buy_order = self.place_higher_buy_order(orders[order_index + 1], place_order=False) if lower_threshold <= higher_buy_order['amount'] <= upper_threshold: return True - return False + else: + self.log.debug('lower_threshold <= higher_buy_order <= upper_threshold: %s <= %s <=%s', + lower_threshold, higher_buy_order['amount'], upper_threshold) + return False return False @@ -530,6 +565,8 @@ def place_higher_buy_order(self, order, place_order=True): price = order['price'] * (1 + self.increment) if amount / price > self.base_balance['amount']: amount = self.base_balance['amount'] * price + self.log.debug('Correcting order amount in place_higher_buy_order from: %s to %s', + order['quote']['amount'], amount) if place_order: self.market_buy(amount, price) @@ -549,6 +586,8 @@ def place_higher_sell_order(self, order, place_order=True): price = (order['price'] ** -1) * (1 + self.increment) if amount > self.quote_balance['amount']: amount = self.quote_balance['amount'] + self.log.debug('Correcting order amount in place_higher_sell_order from: %s to %s', + order['base']['amount'], amount) if place_order: self.market_sell(amount, price) @@ -568,6 +607,8 @@ def place_lower_buy_order(self, order, place_order=True): price = order['price'] / (1 + self.increment) if amount / price > self.base_balance['amount']: amount = self.base_balance['amount'] * price + self.log.debug('Correcting order amount in place_lower_buy_order from: %s to %s', + order['quote']['amount'], amount) if place_order: self.market_buy(amount, price) @@ -584,9 +625,12 @@ def place_lower_sell_order(self, order, place_order=True): :param bool | place_order: True = Places order to the market, False = returns amount and price """ amount = order['base']['amount'] * (1 + self.increment) + initial_amount = amount price = (order['price'] ** -1) / (1 + self.increment) if amount > self.quote_balance['amount']: amount = self.quote_balance['amount'] + self.log.debug('Correcting order amount in place_lower_sell_order from: %s to %s', + initial_amount, amount) if place_order: self.market_sell(amount, price) @@ -601,6 +645,7 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente :param float | market_center_price: Optional market center price, used to to check order :return dict | order: Returns highest sell order """ + self.log.debug('quote_balance in place_highest_sell_order: %s', quote_balance) # Todo: Fix edge case where CP is close to upper bound and will go over. if not market_center_price: market_center_price = self.market_center_price @@ -638,6 +683,7 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p :param float | market_center_price: Optional market center price, used to to check order :return dict | order: Returns lowest buy order """ + self.log.debug('base_balance in place_highest_sell_order: %s', base_balance) # Todo: Fix edge case where CP is close to lower bound and will go over. if not market_center_price: market_center_price = self.market_center_price From ccb7f624762cd1139b162ddd77cd5a1899b0bb8b Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 22 Aug 2018 00:04:31 +0500 Subject: [PATCH 0558/1846] Do not try to place orders with limited amount Limited orders aren't passing is_order_size_correct() check, so don't place them. Instead just do nothing. This allows to perform intial order placement cleanly. --- dexbot/strategies/staggered_orders.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 55e04f54e..a72d1e5bc 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -564,9 +564,8 @@ def place_higher_buy_order(self, order, place_order=True): amount = order['quote']['amount'] price = order['price'] * (1 + self.increment) if amount / price > self.base_balance['amount']: - amount = self.base_balance['amount'] * price - self.log.debug('Correcting order amount in place_higher_buy_order from: %s to %s', - order['quote']['amount'], amount) + self.log.debug('Not enough balance to place_higher_buy_order') + place_order=False if place_order: self.market_buy(amount, price) @@ -585,9 +584,8 @@ def place_higher_sell_order(self, order, place_order=True): amount = order['base']['amount'] / (1 + self.increment) price = (order['price'] ** -1) * (1 + self.increment) if amount > self.quote_balance['amount']: - amount = self.quote_balance['amount'] - self.log.debug('Correcting order amount in place_higher_sell_order from: %s to %s', - order['base']['amount'], amount) + self.log.debug('Not enough balance to place_higher_sell_order') + place_order=False if place_order: self.market_sell(amount, price) @@ -606,9 +604,8 @@ def place_lower_buy_order(self, order, place_order=True): amount = order['quote']['amount'] price = order['price'] / (1 + self.increment) if amount / price > self.base_balance['amount']: - amount = self.base_balance['amount'] * price - self.log.debug('Correcting order amount in place_lower_buy_order from: %s to %s', - order['quote']['amount'], amount) + self.log.debug('Not enough balance to place_lower_buy_order') + place_order=False if place_order: self.market_buy(amount, price) @@ -628,9 +625,8 @@ def place_lower_sell_order(self, order, place_order=True): initial_amount = amount price = (order['price'] ** -1) / (1 + self.increment) if amount > self.quote_balance['amount']: - amount = self.quote_balance['amount'] - self.log.debug('Correcting order amount in place_lower_sell_order from: %s to %s', - initial_amount, amount) + self.log.debug('Not enough balance to place_lower_sell_order') + place_order=False if place_order: self.market_sell(amount, price) From afa90bb14cd4abd47e168801f6578418ce4e72de Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 21 Aug 2018 21:52:45 +0500 Subject: [PATCH 0559/1846] Remove order double-placement in allocate_quote_asset() --- dexbot/strategies/staggered_orders.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index a72d1e5bc..b044f8b3a 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -317,7 +317,6 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): # Cancel lowest sell order self.log.debug('Order size is not correct, cancelling lowest sell order in allocate_quote_asset') self.cancel(self.sell_orders[0]) - self.place_lower_sell_order(lowest_sell_order) # Todo: This can be changed so that it creates new lowest immediately. Balance needs to be recalculated if len(self.sell_orders) > 0: self.place_lower_sell_order(self.sell_orders[1]) From 4317e805cd76760d8cdb5f06af472da4a7bdb7e6 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 21 Aug 2018 22:27:51 +0500 Subject: [PATCH 0560/1846] Handle situation when there are sell/buy orders without opposite --- dexbot/strategies/staggered_orders.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index b044f8b3a..846e89a21 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -240,7 +240,10 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): :param kwargs: """ self.log.debug('Need to allocate base: %s', base_balance) - if self.buy_orders: + if self.buy_orders and not self.sell_orders: + self.log.debug('Buy orders without sell orders') + return + elif self.buy_orders: # Get currently the lowest and highest buy orders lowest_buy_order = self.buy_orders[-1] highest_buy_order = self.buy_orders[0] @@ -290,7 +293,10 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): :param kwargs: """ self.log.debug('Need to allocate quote: %s', quote_balance) - if self.sell_orders: + if self.sell_orders and not self.buy_orders: + self.log.debug('Sell orders without buy orders') + return + elif self.sell_orders: lowest_sell_order = self.sell_orders[0] highest_sell_order = self.sell_orders[-1] # Sell price is inverted so it can be compared to the upper bound From 727f14f72ae51e1408e9201dc423fe6ae572a6bb Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 21 Aug 2018 22:55:51 +0500 Subject: [PATCH 0561/1846] Improve replacement logic for lowest_buy and highest_sell --- dexbot/strategies/staggered_orders.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 846e89a21..ddd331b4d 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -271,9 +271,15 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): self.log.debug('Order size is not correct, cancelling highest buy order in allocate_base_asset()') # Cancel highest buy order and immediately replace it with new one. self.cancel(highest_buy_order) - # Todo: This can be changed so that it creates new highest immediately. Balance needs to be recalculated - if len(self.buy_orders) > 0: + # We have several orders + if len(self.buy_orders) > 1: self.place_higher_buy_order(self.buy_orders[1]) + # Length is 1, we have only one order which is lowest_buy_order + else: + # We need to obtain total available base balance + total_balance = self.total_balance([], return_asset=True) + base_balance = total_balance['base'] - self.base_fee_reserve + self.place_lowest_buy_order(self.base_orders_balance) else: # Place first buy order as close to the lower bound as possible self.log.debug('Placing first buy order') @@ -323,9 +329,14 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): # Cancel lowest sell order self.log.debug('Order size is not correct, cancelling lowest sell order in allocate_quote_asset') self.cancel(self.sell_orders[0]) - # Todo: This can be changed so that it creates new lowest immediately. Balance needs to be recalculated - if len(self.sell_orders) > 0: + # We have several orders + if len(self.sell_orders) > 1: self.place_lower_sell_order(self.sell_orders[1]) + # Length is 1, we have only one order which is highest_sell_order + else: + total_balance = self.total_balance([], return_asset=True) + quote_balance = total_balance['quote'] - self.quote_fee_reserve + self.place_highest_sell_order(quote_balance) else: # Place first order as close to the upper bound as possible self.place_highest_sell_order(quote_balance) From 88edcd516d9831b1cf9e9dfff9ae689eda27f87b Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 22 Aug 2018 00:42:52 +0500 Subject: [PATCH 0562/1846] Fix new_order_amount calculation in increase_order_sizes() File "/home/vvk/devel/DEXBot/dexbot/strategies/staggered_orders.py", line 189, in maintain_strategy self.allocate_quote_asset(self.quote_balance) File "/home/vvk/devel/DEXBot/dexbot/strategies/staggered_orders.py", line 327, in allocate_quote_asset self.increase_order_sizes('quote', quote_balance, self.sell_orders) File "/home/vvk/devel/DEXBot/dexbot/strategies/staggered_orders.py", line 405, in increase_order_sizes new_order_amount = order_amount + asset_balance TypeError: unsupported operand type(s) for +: 'float' and 'Amount' --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index ddd331b4d..89537e83a 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -349,7 +349,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): depends on mode in question :param str | asset: 'base' or 'quote', depending if checking sell or buy - :param float | asset_balance: Balance of the account + :param Amount | asset_balance: Balance of the account :param list | orders: List of buy or sell orders :return None """ @@ -400,7 +400,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): new_order_amount = lower_bound if asset_balance < new_order_amount - order_amount: - new_order_amount = order_amount + asset_balance + new_order_amount = order_amount + asset_balance['amount'] price = (order['price'] ** -1) self.log.debug('Cancelling sell order in increase_order_sizes(), mode mountain') From 83acea89a28e982b44c7813986af6354e8e7f15e Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 22 Aug 2018 09:03:20 +0300 Subject: [PATCH 0563/1846] Refactor string formating --- dexbot/strategies/staggered_orders.py | 46 +++++++++++++-------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 13f557a5b..46b1051f9 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -205,14 +205,14 @@ def remove_outside_orders(self, sell_orders, buy_orders): for order in sell_orders: order_price = order['price'] ** -1 if order_price > self.upper_bound: - self.log.debug('Cancelling sell order outside range: %s', order_price) + self.log.debug('Cancelling sell order outside range: {}'.format(order_price)) orders_to_cancel.append(order) # Remove buy orders that exceed boundaries for order in buy_orders: order_price = order['price'] if order_price < self.lower_bound: - self.log.debug('Cancelling buy order outside range: %s', order_price) + self.log.debug('Cancelling buy order outside range: {}'.format(order_price)) orders_to_cancel.append(order) if orders_to_cancel: @@ -243,7 +243,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): :param args: :param kwargs: """ - self.log.debug('Need to allocate base: %s', base_balance) + self.log.debug('Need to allocate base: {}'.format(base_balance)) if self.buy_orders and not self.sell_orders: self.log.debug('Buy orders without sell orders') return @@ -261,8 +261,8 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): if self.actual_spread >= self.target_spread + self.increment: # Place order closer to the center price - self.log.debug('Placing higher buy order; actaul spread: %s, target + increment: %s', - self.actual_spread, self.target_spread + self.increment) + self.log.debug('Placing higher buy order; actual spread: {}, target + increment: {}'.format( + self.actual_spread, self.target_spread + self.increment)) self.place_higher_buy_order(highest_buy_order) elif lowest_buy_order['price'] / (1 + self.increment) < self.lower_bound: # Lower bound has been reached and now will start allocating rest of the base balance. @@ -302,7 +302,7 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): :param args: :param kwargs: """ - self.log.debug('Need to allocate quote: %s', quote_balance) + self.log.debug('Need to allocate quote: {}'.format(quote_balance)) if self.sell_orders and not self.buy_orders: self.log.debug('Sell orders without buy orders') return @@ -321,8 +321,8 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): if self.actual_spread >= self.target_spread + self.increment: # Place order closer to the center price - self.log.debug('Placing lower sell order; actaul spread: %s, target + increment: %s', - self.actual_spread, self.target_spread + self.increment) + self.log.debug('Placing lower sell order; actual spread: {}, target + increment: {}'.format( + self.actual_spread, self.target_spread + self.increment)) self.place_lower_sell_order(lowest_sell_order) elif highest_sell_order_price * (1 + self.increment) > self.upper_bound: # Upper bound has been reached and now will start allocating rest of the quote balance. @@ -488,7 +488,7 @@ def is_order_size_correct(self, order, orders): threshold = self.increment / 10 upper_threshold = order_size * (1 + threshold) lower_threshold = order_size / (1 + threshold) - # self.log.debug('lower_threshold: %s, upper_threshold: %s', lower_threshold, upper_threshold) + # self.log.debug('lower_threshold: {}, upper_threshold: {}'.format(lower_threshold, upper_threshold)) if self.is_sell_order(order): lowest_sell_order = orders[0] @@ -506,8 +506,8 @@ def is_order_size_correct(self, order, orders): if lower_threshold <= highest_sell_order['amount'] <= upper_threshold: return True else: - self.log.debug('lower_threshold <= highest_sell_order <= upper_threshold: %s <= %s <= %s', - lower_threshold, highest_sell_order['amount'], upper_threshold) + self.log.debug('lower_threshold <= highest_sell_order <= upper_threshold: {} <= {} <= {}'.format( + lower_threshold, highest_sell_order['amount'], upper_threshold)) return False elif order == highest_sell_order: order_index = orders.index(order) @@ -516,8 +516,8 @@ def is_order_size_correct(self, order, orders): if lower_threshold <= higher_sell_order['amount'] <= upper_threshold: return True else: - self.log.debug('lower_threshold <= higher_sell_order <= upper_threshold: %s <= %s <= %s', - lower_threshold, higher_sell_order['amount'], upper_threshold) + self.log.debug('lower_threshold <= higher_sell_order <= upper_threshold: {} <= {} <= {}'.format( + lower_threshold, higher_sell_order['amount'], upper_threshold)) return False elif order == lowest_sell_order: order_index = orders.index(order) @@ -526,8 +526,8 @@ def is_order_size_correct(self, order, orders): if lower_threshold <= lower_sell_order['amount'] <= upper_threshold: return True else: - self.log.debug('lower_threshold <= lower_sell_order <= upper_threshold: %s <= %s <= %s', - lower_threshold, lower_sell_order['amount'], upper_threshold) + self.log.debug('lower_threshold <= lower_sell_order <= upper_threshold: {} <= {} <= {}'.format( + lower_threshold, lower_sell_order['amount'], upper_threshold)) return False return False elif self.is_buy_order(order): @@ -546,8 +546,8 @@ def is_order_size_correct(self, order, orders): if lower_threshold <= lowest_buy_order['amount'] <= upper_threshold: return True else: - self.log.debug('lower_threshold <= lowest_buy_order <= upper_threshold: %s <= %s <= %s', - lower_threshold, lowest_buy_order['amount'], upper_threshold) + self.log.debug('lower_threshold <= lowest_buy_order <= upper_threshold: {} <= {} <= {}'.format( + lower_threshold, lowest_buy_order['amount'], upper_threshold)) return False elif order == lowest_buy_order: order_index = orders.index(order) @@ -556,8 +556,8 @@ def is_order_size_correct(self, order, orders): if lower_threshold <= lower_buy_order['amount'] <= upper_threshold: return True else: - self.log.debug('lower_threshold <= lower_buy_order <= upper_threshold: %s <= %s <=%s', - lower_threshold, lower_buy_order['amount'], upper_threshold) + self.log.debug('lower_threshold <= lower_buy_order <= upper_threshold: {} <= {} <= {}'.format( + lower_threshold, lower_buy_order['amount'], upper_threshold)) return False elif order == highest_buy_order: order_index = orders.index(order) @@ -566,8 +566,8 @@ def is_order_size_correct(self, order, orders): if lower_threshold <= higher_buy_order['amount'] <= upper_threshold: return True else: - self.log.debug('lower_threshold <= higher_buy_order <= upper_threshold: %s <= %s <=%s', - lower_threshold, higher_buy_order['amount'], upper_threshold) + self.log.debug('lower_threshold <= higher_buy_order <= upper_threshold: {} <= {} <= {}'.format( + lower_threshold, higher_buy_order['amount'], upper_threshold)) return False return False @@ -661,7 +661,7 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente :param float | market_center_price: Optional market center price, used to to check order :return dict | order: Returns highest sell order """ - self.log.debug('quote_balance in place_highest_sell_order: %s', quote_balance) + self.log.debug('quote_balance in place_highest_sell_order: {}'.format(quote_balance)) # Todo: Fix edge case where CP is close to upper bound and will go over. if not market_center_price: market_center_price = self.market_center_price @@ -699,7 +699,7 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p :param float | market_center_price: Optional market center price, used to to check order :return dict | order: Returns lowest buy order """ - self.log.debug('base_balance in place_highest_sell_order: %s', base_balance) + self.log.debug('base_balance in place_highest_sell_order: {}'.format(base_balance)) # Todo: Fix edge case where CP is close to lower bound and will go over. if not market_center_price: market_center_price = self.market_center_price From 8532c97211f86cf7c8eb017bb0bb263eabebc71a Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 22 Aug 2018 09:23:25 +0300 Subject: [PATCH 0564/1846] Refactor by removing else-statements --- dexbot/strategies/staggered_orders.py | 58 +++++++++++++-------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 46b1051f9..e6ec8585f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -226,7 +226,6 @@ def remove_outside_orders(self, sell_orders, buy_orders): self.cancel(orders_to_cancel[0]) # To avoid GUI hanging cancel only one order and let switch to another worker return False - else: return True @@ -488,7 +487,6 @@ def is_order_size_correct(self, order, orders): threshold = self.increment / 10 upper_threshold = order_size * (1 + threshold) lower_threshold = order_size / (1 + threshold) - # self.log.debug('lower_threshold: {}, upper_threshold: {}'.format(lower_threshold, upper_threshold)) if self.is_sell_order(order): lowest_sell_order = orders[0] @@ -505,30 +503,29 @@ def is_order_size_correct(self, order, orders): # Check if the old order is same size with accuracy of 0.1% if lower_threshold <= highest_sell_order['amount'] <= upper_threshold: return True - else: - self.log.debug('lower_threshold <= highest_sell_order <= upper_threshold: {} <= {} <= {}'.format( - lower_threshold, highest_sell_order['amount'], upper_threshold)) - return False + + self.log.debug('lower_threshold <= highest_sell_order <= upper_threshold: {} <= {} <= {}'.format( + lower_threshold, highest_sell_order['amount'], upper_threshold)) + return False elif order == highest_sell_order: order_index = orders.index(order) higher_sell_order = self.place_higher_sell_order(orders[order_index - 1], place_order=False) if lower_threshold <= higher_sell_order['amount'] <= upper_threshold: return True - else: - self.log.debug('lower_threshold <= higher_sell_order <= upper_threshold: {} <= {} <= {}'.format( - lower_threshold, higher_sell_order['amount'], upper_threshold)) - return False + + self.log.debug('lower_threshold <= higher_sell_order <= upper_threshold: {} <= {} <= {}'.format( + lower_threshold, higher_sell_order['amount'], upper_threshold)) + return False elif order == lowest_sell_order: order_index = orders.index(order) lower_sell_order = self.place_lower_sell_order(orders[order_index + 1], place_order=False) if lower_threshold <= lower_sell_order['amount'] <= upper_threshold: return True - else: - self.log.debug('lower_threshold <= lower_sell_order <= upper_threshold: {} <= {} <= {}'.format( - lower_threshold, lower_sell_order['amount'], upper_threshold)) - return False + + self.log.debug('lower_threshold <= lower_sell_order <= upper_threshold: {} <= {} <= {}'.format( + lower_threshold, lower_sell_order['amount'], upper_threshold)) return False elif self.is_buy_order(order): lowest_buy_order = orders[-1] @@ -545,30 +542,30 @@ def is_order_size_correct(self, order, orders): # Check if the old order is same size with accuracy of 0.1% if lower_threshold <= lowest_buy_order['amount'] <= upper_threshold: return True - else: - self.log.debug('lower_threshold <= lowest_buy_order <= upper_threshold: {} <= {} <= {}'.format( - lower_threshold, lowest_buy_order['amount'], upper_threshold)) - return False + + self.log.debug('lower_threshold <= lowest_buy_order <= upper_threshold: {} <= {} <= {}'.format( + lower_threshold, lowest_buy_order['amount'], upper_threshold)) + return False elif order == lowest_buy_order: order_index = orders.index(order) lower_buy_order = self.place_lower_buy_order(orders[order_index - 1], place_order=False) if lower_threshold <= lower_buy_order['amount'] <= upper_threshold: return True - else: - self.log.debug('lower_threshold <= lower_buy_order <= upper_threshold: {} <= {} <= {}'.format( - lower_threshold, lower_buy_order['amount'], upper_threshold)) - return False + + self.log.debug('lower_threshold <= lower_buy_order <= upper_threshold: {} <= {} <= {}'.format( + lower_threshold, lower_buy_order['amount'], upper_threshold)) + return False elif order == highest_buy_order: order_index = orders.index(order) higher_buy_order = self.place_higher_buy_order(orders[order_index + 1], place_order=False) if lower_threshold <= higher_buy_order['amount'] <= upper_threshold: return True - else: - self.log.debug('lower_threshold <= higher_buy_order <= upper_threshold: {} <= {} <= {}'.format( - lower_threshold, higher_buy_order['amount'], upper_threshold)) - return False + + self.log.debug('lower_threshold <= higher_buy_order <= upper_threshold: {} <= {} <= {}'.format( + lower_threshold, higher_buy_order['amount'], upper_threshold)) + return False return False @@ -585,7 +582,7 @@ def place_higher_buy_order(self, order, place_order=True): price = order['price'] * (1 + self.increment) if amount / price > self.base_balance['amount']: self.log.debug('Not enough balance to place_higher_buy_order') - place_order = False + return if place_order: self.market_buy(amount, price) @@ -605,7 +602,7 @@ def place_higher_sell_order(self, order, place_order=True): price = (order['price'] ** -1) * (1 + self.increment) if amount > self.quote_balance['amount']: self.log.debug('Not enough balance to place_higher_sell_order') - place_order = False + return if place_order: self.market_sell(amount, price) @@ -625,7 +622,7 @@ def place_lower_buy_order(self, order, place_order=True): price = order['price'] / (1 + self.increment) if amount / price > self.base_balance['amount']: self.log.debug('Not enough balance to place_lower_buy_order') - place_order = False + return if place_order: self.market_buy(amount, price) @@ -642,11 +639,10 @@ def place_lower_sell_order(self, order, place_order=True): :param bool | place_order: True = Places order to the market, False = returns amount and price """ amount = order['base']['amount'] * (1 + self.increment) - initial_amount = amount price = (order['price'] ** -1) / (1 + self.increment) if amount > self.quote_balance['amount']: self.log.debug('Not enough balance to place_lower_sell_order') - place_order = False + return if place_order: self.market_sell(amount, price) From 41974905ecf888f10f69554eac8a87b6f90d0f80 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 22 Aug 2018 09:26:49 +0300 Subject: [PATCH 0565/1846] Change balance given for place_lowest_buy_order --- 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 e6ec8585f..8bdd1fe98 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -282,7 +282,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): # We need to obtain total available base balance total_balance = self.total_balance([], return_asset=True) base_balance = total_balance['base'] - self.base_fee_reserve - self.place_lowest_buy_order(self.base_orders_balance) + self.place_lowest_buy_order(base_balance) else: # Place first buy order as close to the lower bound as possible self.log.debug('Placing first buy order') From 25a32f04cdc9435c783a3a1815a6f862e1e480ff Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 22 Aug 2018 12:17:38 +0300 Subject: [PATCH 0566/1846] Refactor partial revert to earlier stage --- dexbot/strategies/staggered_orders.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 8bdd1fe98..d4833743a 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -582,7 +582,7 @@ def place_higher_buy_order(self, order, place_order=True): price = order['price'] * (1 + self.increment) if amount / price > self.base_balance['amount']: self.log.debug('Not enough balance to place_higher_buy_order') - return + place_order = False if place_order: self.market_buy(amount, price) @@ -602,7 +602,7 @@ def place_higher_sell_order(self, order, place_order=True): price = (order['price'] ** -1) * (1 + self.increment) if amount > self.quote_balance['amount']: self.log.debug('Not enough balance to place_higher_sell_order') - return + place_order = False if place_order: self.market_sell(amount, price) @@ -622,7 +622,7 @@ def place_lower_buy_order(self, order, place_order=True): price = order['price'] / (1 + self.increment) if amount / price > self.base_balance['amount']: self.log.debug('Not enough balance to place_lower_buy_order') - return + place_order = False if place_order: self.market_buy(amount, price) @@ -642,7 +642,7 @@ def place_lower_sell_order(self, order, place_order=True): price = (order['price'] ** -1) / (1 + self.increment) if amount > self.quote_balance['amount']: self.log.debug('Not enough balance to place_lower_sell_order') - return + place_order = False if place_order: self.market_sell(amount, price) From 3739a0132a409ef35c095b93b3732b81cc471062 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 22 Aug 2018 12:18:10 +0300 Subject: [PATCH 0567/1846] Refactor some comments --- dexbot/strategies/staggered_orders.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index d4833743a..200ee959e 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -68,7 +68,7 @@ def __init__(self, *args, **kwargs): self.lower_bound = self.worker['lower_bound'] # Strategy variables - self.bootstrapping = False # Set default True / False? + self.bootstrapping = False # Todo: Set default True / False? self.market_center_price = None self.initial_market_center_price = None self.buy_orders = [] @@ -91,7 +91,6 @@ def __init__(self, *args, **kwargs): def maintain_strategy(self, *args, **kwargs): """ Logic of the strategy - :param args: Order which was added after the bot was started and if there was no market center price :param args: :param kwargs: """ From 54e37caf4594ead7217d85568457d96c03009ca2 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 22 Aug 2018 15:24:00 +0500 Subject: [PATCH 0568/1846] Fix base amount calculation in place_xxx_order --- dexbot/strategies/staggered_orders.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 200ee959e..8ae02487f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -579,7 +579,10 @@ def place_higher_buy_order(self, order, place_order=True): """ amount = order['quote']['amount'] price = order['price'] * (1 + self.increment) - if amount / price > self.base_balance['amount']: + # How many BASE we need to buy QUOTE `amount` + base_amount = amount * price + + if base_amount > self.base_balance['amount']: self.log.debug('Not enough balance to place_higher_buy_order') place_order = False @@ -619,9 +622,12 @@ def place_lower_buy_order(self, order, place_order=True): """ amount = order['quote']['amount'] price = order['price'] / (1 + self.increment) - if amount / price > self.base_balance['amount']: - self.log.debug('Not enough balance to place_lower_buy_order') + # How many BASE we need to buy QUOTE `amount` + base_amount = amount * price + + if base_amount > self.base_balance['amount']: place_order = False + self.log.debug('Not enough balance to place_lower_buy_order') if place_order: self.market_buy(amount, price) @@ -716,8 +722,8 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p precision = self.market['quote']['precision'] amount = previous_amount * (self.base_orders_balance / orders_sum) - # amount / price = amount in QUOTE - amount = amount / price + # We need to turn BASE amount into QUOTE amount (we will buy this QUOTE asset amount) + amount = amount * price amount = int(float(amount) * 10 ** precision) / (10 ** precision) price = previous_price From 48d37d1248d77be7e1aa02c33f9927d6a921beac Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 22 Aug 2018 15:25:43 +0500 Subject: [PATCH 0569/1846] Improve logging when not enough balance to place order --- dexbot/strategies/staggered_orders.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 8ae02487f..55c1ff42a 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -583,7 +583,8 @@ def place_higher_buy_order(self, order, place_order=True): base_amount = amount * price if base_amount > self.base_balance['amount']: - self.log.debug('Not enough balance to place_higher_buy_order') + self.log.debug('Not enough balance to place_higher_buy_order; need/avail: {}/{}'.format( + base_amount, self.base_balance['amount'])) place_order = False if place_order: @@ -603,7 +604,8 @@ def place_higher_sell_order(self, order, place_order=True): amount = order['base']['amount'] / (1 + self.increment) price = (order['price'] ** -1) * (1 + self.increment) if amount > self.quote_balance['amount']: - self.log.debug('Not enough balance to place_higher_sell_order') + self.log.debug('Not enough balance to place_higher_sell_order; need/avail: {}/{}'.format( + amount, self.quote_balance['amount'])) place_order = False if place_order: @@ -626,8 +628,9 @@ def place_lower_buy_order(self, order, place_order=True): base_amount = amount * price if base_amount > self.base_balance['amount']: + self.log.debug('Not enough balance to place_lower_buy_order; need/avail: {}/{}'.format( + base_amount, self.base_balance['amount'])) place_order = False - self.log.debug('Not enough balance to place_lower_buy_order') if place_order: self.market_buy(amount, price) @@ -646,7 +649,8 @@ def place_lower_sell_order(self, order, place_order=True): amount = order['base']['amount'] * (1 + self.increment) price = (order['price'] ** -1) / (1 + self.increment) if amount > self.quote_balance['amount']: - self.log.debug('Not enough balance to place_lower_sell_order') + self.log.debug('Not enough balance to place_lower_sell_order; need/avail: {}/{}'.format( + amount, self.quote_balance['amount'])) place_order = False if place_order: From 199e46a4fa39bde9fa4602331fe8324e3fa3780a Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 22 Aug 2018 14:18:14 +0300 Subject: [PATCH 0570/1846] Change when center price is calculated --- dexbot/strategies/staggered_orders.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 200ee959e..eaf8747df 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -95,19 +95,15 @@ def maintain_strategy(self, *args, **kwargs): :param kwargs: """ - # Calculate market center price - # Todo: Use user's center price if included - self.market_center_price = self.calculate_center_price(suppress_errors=True) - - # Loop until center price appears on the market + # Check if market center price is calculated if not self.market_center_price: + self.market_center_price = self.calculate_center_price(suppress_errors=True) return - - # Save initial market center price, which is used to make sure that first order is still correct - if not self.initial_market_center_price: + elif self.market_center_price and not self.initial_market_center_price: + # Save initial market center price self.initial_market_center_price = self.market_center_price - # Get orders + # Get all user's orders on current market orders = self.orders market_orders = self.market.orderbook(1) From 81a63248f3aa789a27fcffac8d68728882dba11c Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 22 Aug 2018 14:20:11 +0300 Subject: [PATCH 0571/1846] Refactor asset balance variable name --- dexbot/strategies/staggered_orders.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index eaf8747df..c2168185e 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -77,8 +77,8 @@ def __init__(self, *args, **kwargs): self.market_spread = 0 self.base_fee_reserve = None self.quote_fee_reserve = None - self.quote_orders_balance = 0 - self.base_orders_balance = 0 + self.quote_total_balance = 0 + self.base_total_balance = 0 self.quote_balance = 0 self.base_balance = 0 @@ -144,19 +144,20 @@ def maintain_strategy(self, *args, **kwargs): self.quote_fee_reserve = 0.01 * core_exchange_rate['quote']['amount'] * 100 self.base_fee_reserve = 0.01 * core_exchange_rate['base']['amount'] * 100 - self.base_balance['amount'] = self.base_balance['amount'] - self.base_fee_reserve self.quote_balance['amount'] = self.quote_balance['amount'] - self.quote_fee_reserve + self.base_balance['amount'] = self.base_balance['amount'] - self.base_fee_reserve # Balance per asset from orders and account balance order_ids = [order['id'] for order in orders] orders_balance = self.orders_balance(order_ids) - self.quote_orders_balance = orders_balance['quote'] + self.quote_balance['amount'] - self.base_orders_balance = orders_balance['base'] + self.base_balance['amount'] + # Todo: These are more xxx_total_balance + self.quote_total_balance = orders_balance['quote'] + self.quote_balance['amount'] + self.base_total_balance = orders_balance['base'] + self.base_balance['amount'] # Calculate asset thresholds - base_asset_threshold = self.base_orders_balance / 20000 - quote_asset_threshold = self.quote_orders_balance / 20000 + quote_asset_threshold = self.quote_total_balance / 20000 + base_asset_threshold = self.base_total_balance / 20000 # Check boundaries if self.market_center_price > self.upper_bound: @@ -673,7 +674,7 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente amount = amount / (1 + self.increment) precision = self.market['quote']['precision'] - order_size = previous_amount * (self.quote_orders_balance / orders_sum) + order_size = previous_amount * (self.quote_total_balance / orders_sum) amount = int(float(order_size) * 10 ** precision) / (10 ** precision) price = previous_price @@ -711,7 +712,7 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p amount = amount / (1 + self.increment) precision = self.market['quote']['precision'] - amount = previous_amount * (self.base_orders_balance / orders_sum) + amount = previous_amount * (self.base_total_balance / orders_sum) # amount / price = amount in QUOTE amount = amount / price amount = int(float(amount) * 10 ** precision) / (10 ** precision) From 809fe3f963457a38455faff00d576781b610dc17 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 22 Aug 2018 14:20:44 +0300 Subject: [PATCH 0572/1846] Change logic by adding bootstrapping --- dexbot/strategies/staggered_orders.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c2168185e..f7e66a6ca 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -335,9 +335,11 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): else: total_balance = self.total_balance([], return_asset=True) quote_balance = total_balance['quote'] - self.quote_fee_reserve + self.bootstrapping = True self.place_highest_sell_order(quote_balance) else: # Place first order as close to the upper bound as possible + self.bootstrapping = True self.place_highest_sell_order(quote_balance) # Todo: Check completely From 94b7130f18ce0da9146d36efb99db6f48b6561ad Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 22 Aug 2018 14:23:01 +0300 Subject: [PATCH 0573/1846] Fix wrong variable name in place_lowest_buy_order --- 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 173dcf517..9b3501ee7 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -724,7 +724,7 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p amount = amount / (1 + self.increment) precision = self.market['quote']['precision'] - amount = previous_amount * (self.base_orders_balance / orders_sum) + amount = previous_amount * (self.base_total_balance / orders_sum) # We need to turn BASE amount into QUOTE amount (we will buy this QUOTE asset amount) amount = amount * price From 1fd9f85a0a3202a9f1a9741c58259925aff27faa Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 22 Aug 2018 15:04:03 +0300 Subject: [PATCH 0574/1846] Change strategy logic --- dexbot/strategies/staggered_orders.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 9b3501ee7..838b14d41 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -260,10 +260,12 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): self.actual_spread, self.target_spread + self.increment)) self.place_higher_buy_order(highest_buy_order) elif lowest_buy_order['price'] / (1 + self.increment) < self.lower_bound: + self.bootstrapping = False # Lower bound has been reached and now will start allocating rest of the base balance. self.log.debug('Increasing orders sizes for base asset') self.increase_order_sizes('base', base_balance, self.buy_orders) else: + self.bootstrapping = False self.log.debug('Placing lower order than lowest_buy_order') self.place_lower_buy_order(lowest_buy_order) else: @@ -320,9 +322,11 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): self.actual_spread, self.target_spread + self.increment)) self.place_lower_sell_order(lowest_sell_order) elif highest_sell_order_price * (1 + self.increment) > self.upper_bound: + self.bootstrapping = False # Upper bound has been reached and now will start allocating rest of the quote balance. self.increase_order_sizes('quote', quote_balance, self.sell_orders) else: + self.bootstrapping = False self.place_higher_sell_order(highest_sell_order) else: # Cancel lowest sell order From 63adbc28ae2e72bf527b82335356d4d043fe9e49 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 22 Aug 2018 15:04:41 +0300 Subject: [PATCH 0575/1846] Fix lowest buy order amount --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 838b14d41..410c9e740 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -730,8 +730,8 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p precision = self.market['quote']['precision'] amount = previous_amount * (self.base_total_balance / orders_sum) # We need to turn BASE amount into QUOTE amount (we will buy this QUOTE asset amount) - amount = amount * price - + # QUOTE = BASE / price + amount = amount / price amount = int(float(amount) * 10 ** precision) / (10 ** precision) price = previous_price From 518edfcd4cd2bb703901f567a43441e42b6e5c72 Mon Sep 17 00:00:00 2001 From: Juhani Haapala Date: Wed, 22 Aug 2018 19:06:22 +0300 Subject: [PATCH 0576/1846] Fix starting strategy on empty market with cp given --- dexbot/strategies/staggered_orders.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 410c9e740..703fe76a9 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -68,7 +68,7 @@ def __init__(self, *args, **kwargs): self.lower_bound = self.worker['lower_bound'] # Strategy variables - self.bootstrapping = False # Todo: Set default True / False? + self.bootstrapping = True # Todo: Set default True / False? self.market_center_price = None self.initial_market_center_price = None self.buy_orders = [] @@ -96,8 +96,11 @@ def maintain_strategy(self, *args, **kwargs): """ # Check if market center price is calculated - if not self.market_center_price: + if not self.bootstrapping: self.market_center_price = self.calculate_center_price(suppress_errors=True) + elif not self.market_center_price: + # On empty market we have to pass the user specified center price + self.market_center_price = self.calculate_center_price(center_price=self.center_price, suppress_errors=True) return elif self.market_center_price and not self.initial_market_center_price: # Save initial market center price @@ -125,11 +128,13 @@ def maintain_strategy(self, *args, **kwargs): # Calculate market spread # Todo: Market spread is calculated but never used. Is this needed? - highest_market_buy = market_orders['bids'][0]['price'] - lowest_market_sell = market_orders['asks'][0]['price'] + # if there is no orders in both side spread cannot be calculated + if len(market_orders['bids']) and len(market_orders['asks']): + highest_market_buy = market_orders['bids'][0]['price'] + lowest_market_sell = market_orders['asks'][0]['price'] - if highest_market_buy and lowest_market_sell: - self.market_spread = lowest_market_sell / highest_market_buy - 1 + if highest_market_buy and lowest_market_sell: + self.market_spread = lowest_market_sell / highest_market_buy - 1 # Get current account balances account_balances = self.total_balance(order_ids=[], return_asset=True) From 0621aa86ced287d28c36385360dcea36e5e80392 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 22 Aug 2018 17:10:15 +0500 Subject: [PATCH 0577/1846] Fix price in place_lowest_buy_order() --- 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 703fe76a9..f0ba18deb 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -734,11 +734,11 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p precision = self.market['quote']['precision'] amount = previous_amount * (self.base_total_balance / orders_sum) + price = previous_price # We need to turn BASE amount into QUOTE amount (we will buy this QUOTE asset amount) # QUOTE = BASE / price amount = amount / price amount = int(float(amount) * 10 ** precision) / (10 ** precision) - price = previous_price if place_order: self.market_buy(amount, price) From a178483c607d9e77ff2d654bb3322874d9c0ab5b Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 22 Aug 2018 17:12:53 +0500 Subject: [PATCH 0578/1846] Clarify amount variable names in place_xxst_order There was lots of mistakes, let's try to use more clear variable names --- dexbot/strategies/staggered_orders.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index f0ba18deb..618359668 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -695,14 +695,14 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente amount = amount / (1 + self.increment) precision = self.market['quote']['precision'] - order_size = previous_amount * (self.quote_total_balance / orders_sum) - amount = int(float(order_size) * 10 ** precision) / (10 ** precision) + amount_quote = previous_amount * (self.quote_total_balance / orders_sum) + amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) price = previous_price if place_order: - self.market_sell(amount, price) + self.market_sell(amount_quote, price) else: - return {"amount": amount, "price": price} + return {"amount": amount_quote, "price": price} def place_lowest_buy_order(self, base_balance, place_order=True, market_center_price=None): """ Places buy order furthest to the market center price @@ -733,17 +733,17 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p amount = amount / (1 + self.increment) precision = self.market['quote']['precision'] - amount = previous_amount * (self.base_total_balance / orders_sum) + amount_base = previous_amount * (self.base_total_balance / orders_sum) price = previous_price # We need to turn BASE amount into QUOTE amount (we will buy this QUOTE asset amount) # QUOTE = BASE / price - amount = amount / price - amount = int(float(amount) * 10 ** precision) / (10 ** precision) + amount_quote = amount_base / price + amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) if place_order: - self.market_buy(amount, price) + self.market_buy(amount_quote, price) else: - return {"amount": amount, "price": price} + return {"amount": amount_quote, "price": price} def error(self, *args, **kwargs): self.disabled = True From 09f1540726e03f502a1f44fb0443f7c991c5695b Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 22 Aug 2018 17:36:52 +0500 Subject: [PATCH 0579/1846] Update comments --- dexbot/strategies/staggered_orders.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 618359668..c14556b39 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -181,7 +181,8 @@ def maintain_strategy(self, *args, **kwargs): self.allocate_base_asset(self.base_balance) elif self.market_center_price > highest_buy_price * (1 + self.target_spread): if not self.bootstrapping: - # Cancel lowest buy order + # Cancel lowest buy order because center price moved up. + # On the next run there will be placed next buy order closer to the new center self.log.debug('Cancelling lowest buy order in maintain_strategy') self.cancel(self.buy_orders[-1]) @@ -191,7 +192,8 @@ def maintain_strategy(self, *args, **kwargs): self.allocate_quote_asset(self.quote_balance) elif self.market_center_price < lowest_sell_price * (1 - self.target_spread): if not self.bootstrapping: - # Cancel highest sell order + # Cancel highest sell order because center price moved down. + # On the next run there will be placed next sell closer to the new center self.log.debug('Cancelling highest sell order in maintain_strategy') self.cancel(self.sell_orders[-1]) From ae1589a318dee1f1a22990552de5f58a4ff053c0 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 22 Aug 2018 17:44:05 +0500 Subject: [PATCH 0580/1846] Fire "Not enough balance" only when really placing an order This needed to avoid "Not enough balance" flood when place_xxx_order() are called from is_order_size_correct() --- dexbot/strategies/staggered_orders.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c14556b39..112331388 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -593,8 +593,9 @@ def place_higher_buy_order(self, order, place_order=True): base_amount = amount * price if base_amount > self.base_balance['amount']: - self.log.debug('Not enough balance to place_higher_buy_order; need/avail: {}/{}'.format( - base_amount, self.base_balance['amount'])) + if place_order: + self.log.debug('Not enough balance to place_higher_buy_order; need/avail: {}/{}'.format( + base_amount, self.base_balance['amount'])) place_order = False if place_order: @@ -614,8 +615,9 @@ def place_higher_sell_order(self, order, place_order=True): amount = order['base']['amount'] / (1 + self.increment) price = (order['price'] ** -1) * (1 + self.increment) if amount > self.quote_balance['amount']: - self.log.debug('Not enough balance to place_higher_sell_order; need/avail: {}/{}'.format( - amount, self.quote_balance['amount'])) + if place_order: + self.log.debug('Not enough balance to place_higher_sell_order; need/avail: {}/{}'.format( + amount, self.quote_balance['amount'])) place_order = False if place_order: @@ -638,8 +640,9 @@ def place_lower_buy_order(self, order, place_order=True): base_amount = amount * price if base_amount > self.base_balance['amount']: - self.log.debug('Not enough balance to place_lower_buy_order; need/avail: {}/{}'.format( - base_amount, self.base_balance['amount'])) + if place_order: + self.log.debug('Not enough balance to place_lower_buy_order; need/avail: {}/{}'.format( + base_amount, self.base_balance['amount'])) place_order = False if place_order: @@ -659,8 +662,9 @@ def place_lower_sell_order(self, order, place_order=True): amount = order['base']['amount'] * (1 + self.increment) price = (order['price'] ** -1) / (1 + self.increment) if amount > self.quote_balance['amount']: - self.log.debug('Not enough balance to place_lower_sell_order; need/avail: {}/{}'.format( - amount, self.quote_balance['amount'])) + if place_order: + self.log.debug('Not enough balance to place_lower_sell_order; need/avail: {}/{}'.format( + amount, self.quote_balance['amount'])) place_order = False if place_order: From 896fa15c389f280cc5c7b33117ee10d3cbe1c7f2 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 22 Aug 2018 22:28:16 +0500 Subject: [PATCH 0581/1846] Fix working of increase_order_sizes() for BASE asset Also increase order size for only one order at a time, this will prevent flying bugs and will smoothen order increses. New "round" of increasement will not start until ALL orders will not be increased. --- dexbot/strategies/staggered_orders.py | 72 ++++++++++++++++++--------- 1 file changed, 49 insertions(+), 23 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 112331388..b96ca5fbb 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -269,7 +269,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): elif lowest_buy_order['price'] / (1 + self.increment) < self.lower_bound: self.bootstrapping = False # Lower bound has been reached and now will start allocating rest of the base balance. - self.log.debug('Increasing orders sizes for base asset') + self.log.debug('Increasing orders sizes for BASE asset') self.increase_order_sizes('base', base_balance, self.buy_orders) else: self.bootstrapping = False @@ -331,6 +331,7 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): elif highest_sell_order_price * (1 + self.increment) > self.upper_bound: self.bootstrapping = False # Upper bound has been reached and now will start allocating rest of the quote balance. + self.log.debug('Increasing orders sizes for QUOTE asset') self.increase_order_sizes('quote', quote_balance, self.sell_orders) else: self.bootstrapping = False @@ -382,7 +383,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): When making an order, it must not exceed either of these limits, but be made according to the more limiting criteria. """ - # Get orders and amounts to be compared + # Get orders and amounts to be compared. Note: orders are sorted from low price to high for order in orders: order_index = orders.index(order) order_amount = order['base']['amount'] @@ -404,6 +405,9 @@ def increase_order_sizes(self, asset, asset_balance, orders): higher_bound = higher_order['base']['amount'] * (1 + self.increment) + self.log.debug('QUOTE: lower_bound: {}, order_amount: {}, higher_bound: {}'.format( + lower_bound, order_amount * (1 + self.increment / 10), higher_bound)) + if lower_bound > order_amount * (1 + self.increment / 10) < higher_bound: # Calculate new order size and place the order to the market new_order_amount = higher_bound @@ -411,65 +415,87 @@ def increase_order_sizes(self, asset, asset_balance, orders): if higher_bound > lower_bound: new_order_amount = lower_bound + # Limit sell order to available balance if asset_balance < new_order_amount - order_amount: new_order_amount = order_amount + asset_balance['amount'] + self.log.debug('Limiting new sell order to avail asset balance: {}'.format( + new_order_amount)) price = (order['price'] ** -1) - self.log.debug('Cancelling sell order in increase_order_sizes(), mode mountain') + self.log.debug('Cancelling sell order in increase_order_sizes(); ' + 'mode: mountain, base: {}, price: {}'.format( + order_amount, order['price'])) self.cancel(order) self.market_sell(new_order_amount, price) + # Only one increase at a time. This prevents running more than one increaement round + # simultaneously + return elif asset == 'base': # Todo: Work in progress """ Starting from the highest BUY order, for each order, see if it is approximately maximum size. If it is, move on to next. - If not, cancel it and replace with maximum size order. Then return. - If lowest_buy_order is reached, increase it to maximum size + If not, cancel it and replace with maximum size order. Maximum order size will be a + size of higher order. Then return. + If lowest_buy_order is reached, increase it to maximum size. Maximum size is: - as many quote as the order above - and - as many quote * (1 + increment) as the order below - When making an order, it must not exceed either of these limits, but be - made according to the more limiting criteria. + 1. As many BASE as the order above (higher order) + AND + 2. As many BASE * (1 + increment) as the order below (lower order) + + Also when making an order it's size always will be limited by available free balance """ - # Get orders and amounts to be compared + # Get orders and amounts to be compared. Note: orders are sorted from high price to low for order in orders: order_index = orders.index(order) - order_amount = order['quote']['amount'] + order_amount = order['base']['amount'] # This check prevents choosing order with index lower than the list length if order_index == 0: # In case checking the first order, use lowest SELL order in comparison higher_order = self.sell_orders[0] - higher_bound = higher_order['base']['amount'] * (1 + self.increment) + higher_bound = higher_order['quote']['amount'] else: higher_order = orders[order_index - 1] - higher_bound = higher_order['quote']['amount'] * (1 + self.increment) + higher_bound = higher_order['base']['amount'] - # Lower order + # Initially set lower order to the current lower_order = orders[order_index] # This check prevents choosing order with index higher than the list length if order_index + 1 < len(orders): + # If this is not a lowest_buy_order, lower order is a next order down lower_order = orders[order_index + 1] - lower_bound = lower_order['quote']['amount'] + lower_bound = lower_order['base']['amount'] * (1 + self.increment) + + self.log.debug('BASE: lower_bound: {}, order_amount: {}, higher_bound: {}'.format( + lower_bound, order_amount * (1 + self.increment / 10), higher_bound)) if lower_bound > order_amount * (1 + self.increment / 10) < higher_bound: # Calculate new order size and place the order to the market - amount = higher_bound + new_base_amount = lower_bound price = order['price'] - if higher_bound > lower_bound: - amount = lower_bound + if lower_bound > higher_bound: + new_base_amount = higher_bound - if (asset_balance * price) < amount - order_amount: - amount = order_amount + (asset_balance * price) + # Limit buy order to available balance + if (asset_balance / price) < (new_base_amount - order_amount) / price: + new_base_amount = order_amount + asset_balance['amount'] + self.log.debug('Limiting new buy order to avail asset balance: {}'.format( + new_base_amount)) + new_order_amount = new_base_amount / price + self.log.debug('Cancelling buy order in increase_order_sizes(); ' + 'mode: mountain, base: {}, price: {}'.format( + order_amount, order['price'])) self.cancel(order) - self.log.debug('Cancelling buy order in increase_order_sizes(), mode mountain') - self.market_buy(amount, price) + self.market_buy(new_order_amount, price) + # Only one increase at a time. This prevents running more than one increaement round + # simultaneously + return elif self.mode == 'valley': pass elif self.mode == 'neutral': From eaa8b97cec6a60908804e8ee7335b9a59da9c84f Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 22 Aug 2018 23:50:14 +0500 Subject: [PATCH 0582/1846] Fix log message --- 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 b96ca5fbb..c81a5298a 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -423,7 +423,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): price = (order['price'] ** -1) self.log.debug('Cancelling sell order in increase_order_sizes(); ' - 'mode: mountain, base: {}, price: {}'.format( + 'mode: mountain, quote: {}, price: {}'.format( order_amount, order['price'])) self.cancel(order) self.market_sell(new_order_amount, price) From 2d0fc2c869f77e3947b2c1941572c3aaebcbc375 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 23 Aug 2018 10:49:40 +0500 Subject: [PATCH 0583/1846] Implement allow_partial kwarg for place_xxx_order() --- dexbot/strategies/staggered_orders.py | 44 +++++++++++++++++++-------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c81a5298a..8c78ac578 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -604,7 +604,7 @@ def is_order_size_correct(self, order, orders): return False - def place_higher_buy_order(self, order, place_order=True): + def place_higher_buy_order(self, order, place_order=True, allow_partial=False): """ Place higher buy order Mode: MOUNTAIN amount (QUOTE) = lower_buy_order_amount @@ -612,6 +612,7 @@ def place_higher_buy_order(self, order, place_order=True): :param order: Previously highest buy order :param bool | place_order: True = Places order to the market, False = returns amount and price + :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance """ amount = order['quote']['amount'] price = order['price'] * (1 + self.increment) @@ -619,17 +620,21 @@ def place_higher_buy_order(self, order, place_order=True): base_amount = amount * price if base_amount > self.base_balance['amount']: - if place_order: + if place_order and not allow_partial: self.log.debug('Not enough balance to place_higher_buy_order; need/avail: {}/{}'.format( base_amount, self.base_balance['amount'])) - place_order = False + place_order = False + elif allow_partial: + self.log.debug('Limiting order amount to avail balance: {}'.format( + self.base_balance['amount'])) + amount = self.base_balance['amount'] / price if place_order: self.market_buy(amount, price) return {"amount": amount, "price": price} - def place_higher_sell_order(self, order, place_order=True): + def place_higher_sell_order(self, order, place_order=True, allow_partial=False): """ Place higher sell order Mode: MOUNTAIN amount (BASE) = higher_sell_order_amount / (1 + increment) @@ -637,21 +642,26 @@ def place_higher_sell_order(self, order, place_order=True): :param order: highest_sell_order :param bool | place_order: True = Places order to the market, False = returns amount and price + :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance """ amount = order['base']['amount'] / (1 + self.increment) price = (order['price'] ** -1) * (1 + self.increment) if amount > self.quote_balance['amount']: - if place_order: + if place_order and not allow_partial: self.log.debug('Not enough balance to place_higher_sell_order; need/avail: {}/{}'.format( amount, self.quote_balance['amount'])) - place_order = False + place_order = False + elif allow_partial: + self.log.debug('Limiting order amount to avail balance: {}'.format( + self.quote_balance['amount'])) + amount = self.quote_balance['amount'] if place_order: self.market_sell(amount, price) return {"amount": amount, "price": price} - def place_lower_buy_order(self, order, place_order=True): + def place_lower_buy_order(self, order, place_order=True, allow_partial=False): """ Place lower buy order Mode: MOUNTAIN amount (QUOTE) = lowest_buy_order_amount @@ -659,6 +669,7 @@ def place_lower_buy_order(self, order, place_order=True): :param order: Previously lowest buy order :param bool | place_order: True = Places order to the market, False = returns amount and price + :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance """ amount = order['quote']['amount'] price = order['price'] / (1 + self.increment) @@ -666,17 +677,21 @@ def place_lower_buy_order(self, order, place_order=True): base_amount = amount * price if base_amount > self.base_balance['amount']: - if place_order: + if place_order and not allow_partial: self.log.debug('Not enough balance to place_lower_buy_order; need/avail: {}/{}'.format( base_amount, self.base_balance['amount'])) - place_order = False + place_order = False + elif allow_partial: + self.log.debug('Limiting order amount to avail balance: {}'.format( + self.base_balance['amount'])) + amount = self.base_balance['amount'] / price if place_order: self.market_buy(amount, price) else: return {"amount": amount, "price": price} - def place_lower_sell_order(self, order, place_order=True): + def place_lower_sell_order(self, order, place_order=True, allow_partial=False): """ Place lower sell order Mode: MOUNTAIN amount (BASE) = higher_sell_order_amount * (1 + increment) @@ -684,14 +699,19 @@ def place_lower_sell_order(self, order, place_order=True): :param order: Previously higher sell order :param bool | place_order: True = Places order to the market, False = returns amount and price + :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance """ amount = order['base']['amount'] * (1 + self.increment) price = (order['price'] ** -1) / (1 + self.increment) if amount > self.quote_balance['amount']: - if place_order: + if place_order and not allow_partial: self.log.debug('Not enough balance to place_lower_sell_order; need/avail: {}/{}'.format( amount, self.quote_balance['amount'])) - place_order = False + place_order = False + elif allow_partial: + self.log.debug('Limiting order amount to avail balance: {}'.format( + self.quote_balance['amount'])) + amount = self.quote_balance['amount'] if place_order: self.market_sell(amount, price) From c148b04db131ae153c91b884fb535c65fa9c75cb Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 23 Aug 2018 10:52:56 +0500 Subject: [PATCH 0584/1846] Move balance refresh into separate method --- dexbot/strategies/staggered_orders.py | 49 ++++++++++++++------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 8c78ac578..1b615d9e6 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -136,29 +136,7 @@ def maintain_strategy(self, *args, **kwargs): if highest_market_buy and lowest_market_sell: self.market_spread = lowest_market_sell / highest_market_buy - 1 - # Get current account balances - account_balances = self.total_balance(order_ids=[], return_asset=True) - - self.base_balance = account_balances['base'] - self.quote_balance = account_balances['quote'] - - # Reserve transaction fee equivalent in BTS - ticker = self.market.ticker() - core_exchange_rate = ticker['core_exchange_rate'] - # Todo: order_creation_fee(BTS) = 0.01 for now - self.quote_fee_reserve = 0.01 * core_exchange_rate['quote']['amount'] * 100 - self.base_fee_reserve = 0.01 * core_exchange_rate['base']['amount'] * 100 - - self.quote_balance['amount'] = self.quote_balance['amount'] - self.quote_fee_reserve - self.base_balance['amount'] = self.base_balance['amount'] - self.base_fee_reserve - - # Balance per asset from orders and account balance - order_ids = [order['id'] for order in orders] - orders_balance = self.orders_balance(order_ids) - - # Todo: These are more xxx_total_balance - self.quote_total_balance = orders_balance['quote'] + self.quote_balance['amount'] - self.base_total_balance = orders_balance['base'] + self.base_balance['amount'] + self.refresh_balances() # Calculate asset thresholds quote_asset_threshold = self.quote_total_balance / 20000 @@ -197,6 +175,31 @@ def maintain_strategy(self, *args, **kwargs): self.log.debug('Cancelling highest sell order in maintain_strategy') self.cancel(self.sell_orders[-1]) + def refresh_balances(self): + # Get current account balances + account_balances = self.total_balance(order_ids=[], return_asset=True) + + self.base_balance = account_balances['base'] + self.quote_balance = account_balances['quote'] + + # Reserve transaction fee equivalent in BTS + ticker = self.market.ticker() + core_exchange_rate = ticker['core_exchange_rate'] + # Todo: order_creation_fee(BTS) = 0.01 for now + self.quote_fee_reserve = 0.01 * core_exchange_rate['quote']['amount'] * 100 + self.base_fee_reserve = 0.01 * core_exchange_rate['base']['amount'] * 100 + + self.quote_balance['amount'] = self.quote_balance['amount'] - self.quote_fee_reserve + self.base_balance['amount'] = self.base_balance['amount'] - self.base_fee_reserve + + # Balance per asset from orders and account balance + order_ids = [order['id'] for order in self.orders] + orders_balance = self.orders_balance(order_ids) + + # Todo: These are more xxx_total_balance + self.quote_total_balance = orders_balance['quote'] + self.quote_balance['amount'] + self.base_total_balance = orders_balance['base'] + self.base_balance['amount'] + def remove_outside_orders(self, sell_orders, buy_orders): """ Remove orders that exceed boundaries :param list | sell_orders: our sell orders From eb28b5c24f29672d8b56c19f3a8251c68ec74ad1 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 23 Aug 2018 10:54:04 +0500 Subject: [PATCH 0585/1846] Allow to place partial orders in several cases --- dexbot/strategies/staggered_orders.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 1b615d9e6..331dd7308 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -268,7 +268,11 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): # Place order closer to the center price self.log.debug('Placing higher buy order; actual spread: {}, target + increment: {}'.format( self.actual_spread, self.target_spread + self.increment)) - self.place_higher_buy_order(highest_buy_order) + if self.bootstrapping: + self.place_higher_buy_order(highest_buy_order) + else: + # Allow to place partial order whether we are not in boostrapping + self.place_higher_buy_order(highest_buy_order, allow_partial=True) elif lowest_buy_order['price'] / (1 + self.increment) < self.lower_bound: self.bootstrapping = False # Lower bound has been reached and now will start allocating rest of the base balance. @@ -277,14 +281,15 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): else: self.bootstrapping = False self.log.debug('Placing lower order than lowest_buy_order') - self.place_lower_buy_order(lowest_buy_order) + self.place_lower_buy_order(lowest_buy_order, allow_partial=True) else: self.log.debug('Order size is not correct, cancelling highest buy order in allocate_base_asset()') # Cancel highest buy order and immediately replace it with new one. self.cancel(highest_buy_order) + self.refresh_balances() # We have several orders if len(self.buy_orders) > 1: - self.place_higher_buy_order(self.buy_orders[1]) + self.place_higher_buy_order(self.buy_orders[1], allow_partial=True) # Length is 1, we have only one order which is lowest_buy_order else: # We need to obtain total available base balance @@ -330,7 +335,10 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): # Place order closer to the center price self.log.debug('Placing lower sell order; actual spread: {}, target + increment: {}'.format( self.actual_spread, self.target_spread + self.increment)) - self.place_lower_sell_order(lowest_sell_order) + if self.bootstrapping: + self.place_lower_sell_order(lowest_sell_order) + else: + self.place_lower_sell_order(lowest_sell_order, allow_partial=True) elif highest_sell_order_price * (1 + self.increment) > self.upper_bound: self.bootstrapping = False # Upper bound has been reached and now will start allocating rest of the quote balance. @@ -343,9 +351,10 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): # Cancel lowest sell order self.log.debug('Order size is not correct, cancelling lowest sell order in allocate_quote_asset') self.cancel(self.sell_orders[0]) + self.refresh_balances() # We have several orders if len(self.sell_orders) > 1: - self.place_lower_sell_order(self.sell_orders[1]) + self.place_lower_sell_order(self.sell_orders[1], allow_partial=True) # Length is 1, we have only one order which is highest_sell_order else: total_balance = self.total_balance([], return_asset=True) From 96bf99b8063093ee4162de37cc130a82230f7dfd Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 23 Aug 2018 14:28:59 +0500 Subject: [PATCH 0586/1846] Fix price in log message --- 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 331dd7308..c004e8793 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -436,7 +436,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): price = (order['price'] ** -1) self.log.debug('Cancelling sell order in increase_order_sizes(); ' 'mode: mountain, quote: {}, price: {}'.format( - order_amount, order['price'])) + order_amount, price)) self.cancel(order) self.market_sell(new_order_amount, price) # Only one increase at a time. This prevents running more than one increaement round From 24080b39e72e477191ef61cf2242eac7d64de3c1 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 23 Aug 2018 15:08:42 +0500 Subject: [PATCH 0587/1846] Update comment --- dexbot/strategies/staggered_orders.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c004e8793..52a6d305f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -403,6 +403,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): # This check prevents choosing order with index lower than the list length if order_index == 0: # In case checking the first order, use highest BUY order in comparison + # This means our highest-sized order will not exceed our highest BUY order lower_order = self.buy_orders[0] lower_bound = lower_order['quote']['amount'] else: From b31a35149d2841402093e20fbdc421531c917f8d Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 23 Aug 2018 15:09:37 +0500 Subject: [PATCH 0588/1846] Allow partial order in allocate_quote_asset It was just missed, for BASE it was added previously for such condition. --- 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 52a6d305f..2211302b1 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -346,7 +346,7 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): self.increase_order_sizes('quote', quote_balance, self.sell_orders) else: self.bootstrapping = False - self.place_higher_sell_order(highest_sell_order) + self.place_higher_sell_order(highest_sell_order, allow_partial=True) else: # Cancel lowest sell order self.log.debug('Order size is not correct, cancelling lowest sell order in allocate_quote_asset') From 49db79787aab04886cdc6d556c9cef301def5c78 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 23 Aug 2018 15:11:29 +0500 Subject: [PATCH 0589/1846] Cancel furthest order as a fallback only when both assets are allocated --- dexbot/strategies/staggered_orders.py | 33 ++++++++++++++++++++------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 2211302b1..fef642a2c 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -155,21 +155,38 @@ def maintain_strategy(self, *args, **kwargs): # BASE asset check if self.base_balance > base_asset_threshold: + base_allocated = False # Allocate available funds self.allocate_base_asset(self.base_balance) - elif self.market_center_price > highest_buy_price * (1 + self.target_spread): - if not self.bootstrapping: - # Cancel lowest buy order because center price moved up. - # On the next run there will be placed next buy order closer to the new center - self.log.debug('Cancelling lowest buy order in maintain_strategy') - self.cancel(self.buy_orders[-1]) + else: + base_allocated = True # QUOTE asset check if self.quote_balance > quote_asset_threshold: + quote_allocated = False # Allocate available funds self.allocate_quote_asset(self.quote_balance) - elif self.market_center_price < lowest_sell_price * (1 - self.target_spread): - if not self.bootstrapping: + else: + quote_allocated = True + + # Do not continue whether assets is not fully allocated + if not base_allocated and not quote_allocated: + # Futher checks should be performed on next maintenance + return + + # There are no funds and current orders aren't close enough, try to fix the situation by shifting orders + # Measure which price is closer to the center + buy_distance = self.market_center_price - highest_buy_price + sell_distance = self.market_center_price - lowest_sell_price + + if buy_distance > sell_distance: + if self.market_center_price > highest_buy_price * (1 + self.target_spread): + # Cancel lowest buy order because center price moved up. + # On the next run there will be placed next buy order closer to the new center + self.log.debug('Cancelling lowest buy order in maintain_strategy') + self.cancel(self.buy_orders[-1]) + else: + if self.market_center_price < lowest_sell_price * (1 - self.target_spread): # Cancel highest sell order because center price moved down. # On the next run there will be placed next sell closer to the new center self.log.debug('Cancelling highest sell order in maintain_strategy') From e8661f6f9c6772a8f165311afb266ecdc9b9fcf5 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 23 Aug 2018 15:12:20 +0500 Subject: [PATCH 0590/1846] Disable is_order_size_correct() This is only for testing! Rework later. --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index fef642a2c..f07e8fc76 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -275,7 +275,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): highest_buy_order = self.buy_orders[0] # Check if the order size is correct - if self.is_order_size_correct(highest_buy_order, self.buy_orders): + if self.is_order_size_correct(highest_buy_order, self.buy_orders) or True: # Calculate actual spread lowest_sell_price = self.sell_orders[0]['price'] ** -1 highest_buy_price = highest_buy_order['price'] @@ -342,7 +342,7 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): highest_sell_order_price = (highest_sell_order['price'] ** -1) # Check if the order size is correct - if self.is_order_size_correct(lowest_sell_order, self.sell_orders): + if self.is_order_size_correct(lowest_sell_order, self.sell_orders) or True: # Calculate actual spread lowest_sell_price = lowest_sell_order['price'] ** -1 highest_buy_price = self.buy_orders[0]['price'] From 4688904f09d64bee2463d53c750e66148e350984 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 23 Aug 2018 15:42:49 +0500 Subject: [PATCH 0591/1846] Fix sell_distance --- 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 f07e8fc76..8d9b8f5e1 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -177,7 +177,7 @@ def maintain_strategy(self, *args, **kwargs): # There are no funds and current orders aren't close enough, try to fix the situation by shifting orders # Measure which price is closer to the center buy_distance = self.market_center_price - highest_buy_price - sell_distance = self.market_center_price - lowest_sell_price + sell_distance = lowest_sell_price - self.market_center_price if buy_distance > sell_distance: if self.market_center_price > highest_buy_price * (1 + self.target_spread): From f8487ee896fcb7856e0ffb3bb036aa0db732b885 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 23 Aug 2018 15:22:12 +0300 Subject: [PATCH 0592/1846] Merge pull request #288 --- dexbot/strategies/staggered_orders.py | 71 +++++++++++++-------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index f07e8fc76..713d3dae1 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -171,7 +171,7 @@ def maintain_strategy(self, *args, **kwargs): # Do not continue whether assets is not fully allocated if not base_allocated and not quote_allocated: - # Futher checks should be performed on next maintenance + # Further checks should be performed on next maintenance return # There are no funds and current orders aren't close enough, try to fix the situation by shifting orders @@ -193,6 +193,9 @@ def maintain_strategy(self, *args, **kwargs): self.cancel(self.sell_orders[-1]) def refresh_balances(self): + """ This function is used to refresh account balances + :return: + """ # Get current account balances account_balances = self.total_balance(order_ids=[], return_asset=True) @@ -209,11 +212,11 @@ def refresh_balances(self): self.quote_balance['amount'] = self.quote_balance['amount'] - self.quote_fee_reserve self.base_balance['amount'] = self.base_balance['amount'] - self.base_fee_reserve - # Balance per asset from orders and account balance + # Balance per asset from orders order_ids = [order['id'] for order in self.orders] orders_balance = self.orders_balance(order_ids) - # Todo: These are more xxx_total_balance + # Total balance per asset (orders balance and available balance) self.quote_total_balance = orders_balance['quote'] + self.quote_balance['amount'] self.base_total_balance = orders_balance['base'] + self.base_balance['amount'] @@ -249,8 +252,8 @@ def remove_outside_orders(self, sell_orders, buy_orders): self.cancel(orders_to_cancel[0]) # To avoid GUI hanging cancel only one order and let switch to another worker return False - else: - return True + + return True def maintain_mountain_mode(self): """ Mountain mode @@ -275,7 +278,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): highest_buy_order = self.buy_orders[0] # Check if the order size is correct - if self.is_order_size_correct(highest_buy_order, self.buy_orders) or True: + if self.is_order_size_correct(highest_buy_order, self.buy_orders): # Calculate actual spread lowest_sell_price = self.sell_orders[0]['price'] ** -1 highest_buy_price = highest_buy_order['price'] @@ -288,7 +291,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): if self.bootstrapping: self.place_higher_buy_order(highest_buy_order) else: - # Allow to place partial order whether we are not in boostrapping + # Allow to place partial order whether we are not in bootstrapping self.place_higher_buy_order(highest_buy_order, allow_partial=True) elif lowest_buy_order['price'] / (1 + self.increment) < self.lower_bound: self.bootstrapping = False @@ -342,7 +345,7 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): highest_sell_order_price = (highest_sell_order['price'] ** -1) # Check if the order size is correct - if self.is_order_size_correct(lowest_sell_order, self.sell_orders) or True: + if self.is_order_size_correct(lowest_sell_order, self.sell_orders): # Calculate actual spread lowest_sell_price = lowest_sell_order['price'] ** -1 highest_buy_price = self.buy_orders[0]['price'] @@ -452,12 +455,11 @@ def increase_order_sizes(self, asset, asset_balance, orders): new_order_amount)) price = (order['price'] ** -1) - self.log.debug('Cancelling sell order in increase_order_sizes(); ' - 'mode: mountain, quote: {}, price: {}'.format( - order_amount, price)) + self.log.debug('Cancelling sell order in increase_order_sizes(); ' + 'mode: mountain, quote: {}, price: {}'.format(order_amount, price)) self.cancel(order) self.market_sell(new_order_amount, price) - # Only one increase at a time. This prevents running more than one increaement round + # Only one increase at a time. This prevents running more than one increment round # simultaneously return elif asset == 'base': @@ -518,9 +520,8 @@ def increase_order_sizes(self, asset, asset_balance, orders): new_base_amount)) new_order_amount = new_base_amount / price - self.log.debug('Cancelling buy order in increase_order_sizes(); ' - 'mode: mountain, base: {}, price: {}'.format( - order_amount, order['price'])) + self.log.debug('Cancelling buy order in increase_order_sizes(); ' + 'mode: mountain, base: {}, price: {}'.format(order_amount, order['price'])) self.cancel(order) self.market_buy(new_order_amount, price) # Only one increase at a time. This prevents running more than one increaement round @@ -651,12 +652,11 @@ def place_higher_buy_order(self, order, place_order=True, allow_partial=False): if base_amount > self.base_balance['amount']: if place_order and not allow_partial: - self.log.debug('Not enough balance to place_higher_buy_order; need/avail: {}/{}'.format( - base_amount, self.base_balance['amount'])) + self.log.debug('Not enough balance to place_higher_buy_order; need/avail: {}/{}' + .format(base_amount, self.base_balance['amount'])) place_order = False elif allow_partial: - self.log.debug('Limiting order amount to avail balance: {}'.format( - self.base_balance['amount'])) + self.log.debug('Limiting order amount to available balance: {}'.format(self.base_balance['amount'])) amount = self.base_balance['amount'] / price if place_order: @@ -678,12 +678,11 @@ def place_higher_sell_order(self, order, place_order=True, allow_partial=False): price = (order['price'] ** -1) * (1 + self.increment) if amount > self.quote_balance['amount']: if place_order and not allow_partial: - self.log.debug('Not enough balance to place_higher_sell_order; need/avail: {}/{}'.format( - amount, self.quote_balance['amount'])) + self.log.debug('Not enough balance to place_higher_sell_order; need/avail: {}/{}' + .format(amount, self.quote_balance['amount'])) place_order = False elif allow_partial: - self.log.debug('Limiting order amount to avail balance: {}'.format( - self.quote_balance['amount'])) + self.log.debug('Limiting order amount to available balance: {}'.format(self.quote_balance['amount'])) amount = self.quote_balance['amount'] if place_order: @@ -708,12 +707,11 @@ def place_lower_buy_order(self, order, place_order=True, allow_partial=False): if base_amount > self.base_balance['amount']: if place_order and not allow_partial: - self.log.debug('Not enough balance to place_lower_buy_order; need/avail: {}/{}'.format( - base_amount, self.base_balance['amount'])) + self.log.debug('Not enough balance to place_lower_buy_order; need/avail: {}/{}' + .format(base_amount, self.base_balance['amount'])) place_order = False elif allow_partial: - self.log.debug('Limiting order amount to avail balance: {}'.format( - self.base_balance['amount'])) + self.log.debug('Limiting order amount to available balance: {}'.format(self.base_balance['amount'])) amount = self.base_balance['amount'] / price if place_order: @@ -735,12 +733,11 @@ def place_lower_sell_order(self, order, place_order=True, allow_partial=False): price = (order['price'] ** -1) / (1 + self.increment) if amount > self.quote_balance['amount']: if place_order and not allow_partial: - self.log.debug('Not enough balance to place_lower_sell_order; need/avail: {}/{}'.format( - amount, self.quote_balance['amount'])) + self.log.debug('Not enough balance to place_lower_sell_order; need/avail: {}/{}' + .format(amount, self.quote_balance['amount'])) place_order = False elif allow_partial: - self.log.debug('Limiting order amount to avail balance: {}'.format( - self.quote_balance['amount'])) + self.log.debug('Limiting order amount to available balance: {}'.format(self.quote_balance['amount'])) amount = self.quote_balance['amount'] if place_order: @@ -757,7 +754,7 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente :return dict | order: Returns highest sell order """ self.log.debug('quote_balance in place_highest_sell_order: {}'.format(quote_balance)) - # Todo: Fix edge case where CP is close to upper bound and will go over. + if not market_center_price: market_center_price = self.market_center_price @@ -777,9 +774,9 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente amount = amount / (1 + self.increment) precision = self.market['quote']['precision'] + price = previous_price amount_quote = previous_amount * (self.quote_total_balance / orders_sum) amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) - price = previous_price if place_order: self.market_sell(amount_quote, price) @@ -788,6 +785,10 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente def place_lowest_buy_order(self, base_balance, place_order=True, market_center_price=None): """ Places buy order furthest to the market center price + + Turn BASE amount into QUOTE amount (we will buy this QUOTE amount). + QUOTE = BASE / price + Mode: MOUNTAIN :param Amount | base_balance: Available BASE asset balance :param bool | place_order: True = Places order to the market, False = returns amount and price @@ -795,7 +796,7 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p :return dict | order: Returns lowest buy order """ self.log.debug('base_balance in place_highest_sell_order: {}'.format(base_balance)) - # Todo: Fix edge case where CP is close to lower bound and will go over. + if not market_center_price: market_center_price = self.market_center_price @@ -817,8 +818,6 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p precision = self.market['quote']['precision'] amount_base = previous_amount * (self.base_total_balance / orders_sum) price = previous_price - # We need to turn BASE amount into QUOTE amount (we will buy this QUOTE asset amount) - # QUOTE = BASE / price amount_quote = amount_base / price amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) From c3614c63d16efb7bcc30f7750b87857777ec972d Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 24 Aug 2018 11:44:16 +0300 Subject: [PATCH 0593/1846] Refactor by disabling market spread --- dexbot/strategies/staggered_orders.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 4af33c59e..d7c11fbdb 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -74,7 +74,7 @@ def __init__(self, *args, **kwargs): self.buy_orders = [] self.sell_orders = [] self.actual_spread = self.target_spread + 1 - self.market_spread = 0 + # self.market_spread = 0 self.base_fee_reserve = None self.quote_fee_reserve = None self.quote_total_balance = 0 @@ -108,11 +108,11 @@ def maintain_strategy(self, *args, **kwargs): # Get all user's orders on current market orders = self.orders - market_orders = self.market.orderbook(1) # Sort orders so that order with index 0 is closest to the center price and -1 is furthers self.buy_orders = self.get_buy_orders('DESC', orders) self.sell_orders = self.get_sell_orders('DESC', orders) + # market_orders = self.market.orderbook(1) # Get highest buy and lowest sell prices from orders highest_buy_price = 0 @@ -126,16 +126,15 @@ def maintain_strategy(self, *args, **kwargs): # Invert the sell price to BASE lowest_sell_price = lowest_sell_price ** -1 + # Todo: Market spread is calculated but never used, can this be removed? # Calculate market spread - # Todo: Market spread is calculated but never used. Is this needed? - # if there is no orders in both side spread cannot be calculated - if len(market_orders['bids']) and len(market_orders['asks']): - highest_market_buy = market_orders['bids'][0]['price'] - lowest_market_sell = market_orders['asks'][0]['price'] - - if highest_market_buy and lowest_market_sell: - self.market_spread = lowest_market_sell / highest_market_buy - 1 - + # if there are no orders in both side spread cannot be calculated + # if len(market_orders['bids']) and len(market_orders['asks']): + # highest_market_buy = market_orders['bids'][0]['price'] + # lowest_market_sell = market_orders['asks'][0]['price'] + # + # if highest_market_buy and lowest_market_sell: + # self.market_spread = lowest_market_sell / highest_market_buy - 1 self.refresh_balances() # Calculate asset thresholds From 2bc09e4aa79d1d6de9669fb3a611983bd4179e5d Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 24 Aug 2018 11:45:00 +0300 Subject: [PATCH 0594/1846] Add refresh_orders() --- dexbot/strategies/staggered_orders.py | 47 ++++++++++++++++----------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index d7c11fbdb..8e0c03a87 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -107,11 +107,7 @@ def maintain_strategy(self, *args, **kwargs): self.initial_market_center_price = self.market_center_price # Get all user's orders on current market - orders = self.orders - - # Sort orders so that order with index 0 is closest to the center price and -1 is furthers - self.buy_orders = self.get_buy_orders('DESC', orders) - self.sell_orders = self.get_sell_orders('DESC', orders) + self.refresh_orders() # market_orders = self.market.orderbook(1) # Get highest buy and lowest sell prices from orders @@ -123,7 +119,7 @@ def maintain_strategy(self, *args, **kwargs): if self.sell_orders: lowest_sell_price = self.sell_orders[0].get('price') - # Invert the sell price to BASE + # Invert the sell price to BASE so it can be used in comparison lowest_sell_price = lowest_sell_price ** -1 # Todo: Market spread is calculated but never used, can this be removed? @@ -135,13 +131,15 @@ def maintain_strategy(self, *args, **kwargs): # # if highest_market_buy and lowest_market_sell: # self.market_spread = lowest_market_sell / highest_market_buy - 1 + + # Calculate balances self.refresh_balances() # Calculate asset thresholds quote_asset_threshold = self.quote_total_balance / 20000 base_asset_threshold = self.base_total_balance / 20000 - # Check boundaries + # Check market's price boundaries if self.market_center_price > self.upper_bound: self.upper_bound = self.market_center_price elif self.market_center_price < self.lower_bound: @@ -149,13 +147,17 @@ def maintain_strategy(self, *args, **kwargs): # Remove orders that exceed boundaries success = self.remove_outside_orders(self.sell_orders, self.buy_orders) - if not success: + if success: + # Refresh orders to prevent orders outside boundaries being in the future comparisons + self.refresh_orders() + else: + # Return back to beginning return # BASE asset check if self.base_balance > base_asset_threshold: base_allocated = False - # Allocate available funds + # Allocate available BASE funds self.allocate_base_asset(self.base_balance) else: base_allocated = True @@ -163,7 +165,7 @@ def maintain_strategy(self, *args, **kwargs): # QUOTE asset check if self.quote_balance > quote_asset_threshold: quote_allocated = False - # Allocate available funds + # Allocate available QUOTE funds self.allocate_quote_asset(self.quote_balance) else: quote_allocated = True @@ -193,7 +195,6 @@ def maintain_strategy(self, *args, **kwargs): def refresh_balances(self): """ This function is used to refresh account balances - :return: """ # Get current account balances account_balances = self.total_balance(order_ids=[], return_asset=True) @@ -219,10 +220,19 @@ def refresh_balances(self): self.quote_total_balance = orders_balance['quote'] + self.quote_balance['amount'] self.base_total_balance = orders_balance['base'] + self.base_balance['amount'] + def refresh_orders(self): + """ Updates buy and sell orders + """ + orders = self.orders + + # Sort orders so that order with index 0 is closest to the center price and -1 is furthers + self.buy_orders = self.get_buy_orders('DESC', orders) + self.sell_orders = self.get_sell_orders('DESC', orders) + def remove_outside_orders(self, sell_orders, buy_orders): """ Remove orders that exceed boundaries - :param list | sell_orders: our sell orders - :param list | buy_orders: our buy orders + :param list | sell_orders: User's sell orders + :param list | buy_orders: User's buy orders """ orders_to_cancel = [] @@ -320,12 +330,8 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): self.log.debug('Placing first buy order') self.place_lowest_buy_order(base_balance) - # Finally get all the orders again, in case there has been changes - # Todo: Is this necessary? - orders = self.orders - - self.buy_orders = self.get_buy_orders('DESC', orders) - self.sell_orders = self.get_sell_orders('DESC', orders) + # Get latest orders + self.refresh_orders() def allocate_quote_asset(self, quote_balance, *args, **kwargs): """ Allocates available quote asset as sell orders. @@ -385,6 +391,9 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): self.bootstrapping = True self.place_highest_sell_order(quote_balance) + # Get latest orders + self.refresh_orders() + # Todo: Check completely def increase_order_sizes(self, asset, asset_balance, orders): # Todo: Change asset or separate buy / sell in different functions? From 7b211cc09dda66634c5da285bd68ec9a3ff258b5 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 24 Aug 2018 12:55:28 +0300 Subject: [PATCH 0595/1846] Refactor allocate_xxx_asset bootstrapping --- dexbot/strategies/staggered_orders.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 8e0c03a87..02a59717e 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -285,6 +285,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): # Get currently the lowest and highest buy orders lowest_buy_order = self.buy_orders[-1] highest_buy_order = self.buy_orders[0] + lowest_buy_order_price = lowest_buy_order['price'] # Check if the order size is correct if self.is_order_size_correct(highest_buy_order, self.buy_orders): @@ -302,8 +303,9 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): else: # Allow to place partial order whether we are not in bootstrapping self.place_higher_buy_order(highest_buy_order, allow_partial=True) - elif lowest_buy_order['price'] / (1 + self.increment) < self.lower_bound: - self.bootstrapping = False + elif lowest_buy_order_price / (1 + self.increment) < self.lower_bound: + if self.buy_orders and self.sell_orders: + self.bootstrapping = False # Lower bound has been reached and now will start allocating rest of the base balance. self.log.debug('Increasing orders sizes for BASE asset') self.increase_order_sizes('base', base_balance, self.buy_orders) @@ -365,7 +367,8 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): else: self.place_lower_sell_order(lowest_sell_order, allow_partial=True) elif highest_sell_order_price * (1 + self.increment) > self.upper_bound: - self.bootstrapping = False + if self.buy_orders and self.sell_orders: + self.bootstrapping = False # Upper bound has been reached and now will start allocating rest of the quote balance. self.log.debug('Increasing orders sizes for QUOTE asset') self.increase_order_sizes('quote', quote_balance, self.sell_orders) From 1a3bc6b41cfa35f82f6234fee52bac5ec0632cba Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 24 Aug 2018 13:05:15 +0500 Subject: [PATCH 0596/1846] Bootstrapping is enabled only when there is no buy or sell orders --- dexbot/strategies/staggered_orders.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 02a59717e..8beba712d 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -68,7 +68,8 @@ def __init__(self, *args, **kwargs): self.lower_bound = self.worker['lower_bound'] # Strategy variables - self.bootstrapping = True # Todo: Set default True / False? + # Bootstrap is turned on whether there will no buy or sell orders at all + self.bootstrapping = False self.market_center_price = None self.initial_market_center_price = None self.buy_orders = [] @@ -95,6 +96,13 @@ def maintain_strategy(self, *args, **kwargs): :param kwargs: """ + # Get all user's orders on current market + self.refresh_orders() + # market_orders = self.market.orderbook(1) + + if not self.buy_orders or not self.sell_orders: + self.bootstrapping = True + # Check if market center price is calculated if not self.bootstrapping: self.market_center_price = self.calculate_center_price(suppress_errors=True) @@ -106,10 +114,6 @@ def maintain_strategy(self, *args, **kwargs): # Save initial market center price self.initial_market_center_price = self.market_center_price - # Get all user's orders on current market - self.refresh_orders() - # market_orders = self.market.orderbook(1) - # Get highest buy and lowest sell prices from orders highest_buy_price = 0 lowest_sell_price = 0 @@ -329,6 +333,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): self.place_lowest_buy_order(base_balance) else: # Place first buy order as close to the lower bound as possible + self.bootstrapping = True self.log.debug('Placing first buy order') self.place_lowest_buy_order(base_balance) From 3244eef511e356ccdadf8c3955825c31c71c961e Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 24 Aug 2018 13:59:26 +0500 Subject: [PATCH 0597/1846] Revert "Handle situation when there are sell/buy orders without opposite" This reverts commit 4317e805cd76760d8cdb5f06af472da4a7bdb7e6. Check is removeed because we need to be able to start one-sided. --- dexbot/strategies/staggered_orders.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 8beba712d..bb9fed01f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -282,10 +282,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): :param kwargs: """ self.log.debug('Need to allocate base: {}'.format(base_balance)) - if self.buy_orders and not self.sell_orders: - self.log.debug('Buy orders without sell orders') - return - elif self.buy_orders: + if self.buy_orders: # Get currently the lowest and highest buy orders lowest_buy_order = self.buy_orders[-1] highest_buy_order = self.buy_orders[0] @@ -347,10 +344,7 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): :param kwargs: """ self.log.debug('Need to allocate quote: {}'.format(quote_balance)) - if self.sell_orders and not self.buy_orders: - self.log.debug('Sell orders without buy orders') - return - elif self.sell_orders: + if self.sell_orders: lowest_sell_order = self.sell_orders[0] highest_sell_order = self.sell_orders[-1] # Sell price is inverted so it can be compared to the upper bound From 3cf4c27c38cfc2c274918bd1552b4793ab7f406e Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 24 Aug 2018 15:36:50 +0500 Subject: [PATCH 0598/1846] Empirically calculate needed prices when start is one-sided --- dexbot/strategies/staggered_orders.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index bb9fed01f..cfe55169e 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -291,7 +291,12 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): # Check if the order size is correct if self.is_order_size_correct(highest_buy_order, self.buy_orders): # Calculate actual spread - lowest_sell_price = self.sell_orders[0]['price'] ** -1 + if self.sell_orders: + lowest_sell_price = self.sell_orders[0]['price'] ** -1 + else: + # For one-sided start, calculate lowest_sell_price empirically + lowest_sell_price = self.market_center_price * (1 + self.target_spread / 2) + highest_buy_price = highest_buy_order['price'] self.actual_spread = (lowest_sell_price / highest_buy_price) - 1 @@ -353,8 +358,12 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): # Check if the order size is correct if self.is_order_size_correct(lowest_sell_order, self.sell_orders): # Calculate actual spread + if self.buy_orders: + highest_buy_price = self.buy_orders[0]['price'] + else: + # For one-sided start, calculate highest_buy_price empirically + highest_buy_price = self.market_center_price / (1 + self.target_spread / 2) lowest_sell_price = lowest_sell_order['price'] ** -1 - highest_buy_price = self.buy_orders[0]['price'] self.actual_spread = (lowest_sell_price / highest_buy_price) - 1 if self.actual_spread >= self.target_spread + self.increment: From 2b813c4728b76136b286bf64437a53e5d976b003 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 24 Aug 2018 15:37:42 +0500 Subject: [PATCH 0599/1846] Disable size increase during bootstrapping --- dexbot/strategies/staggered_orders.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index cfe55169e..660c4c9bf 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -417,6 +417,10 @@ def increase_order_sizes(self, asset, asset_balance, orders): :param list | orders: List of buy or sell orders :return None """ + # Disable size increase during boostrapping, may happen with one-sided start + if self.bootstrapping: + return + # Mountain mode: if self.mode == 'mountain': # Todo: Work in progress. From b314087e9c661204861b2d9dd068a4ad01f22ba7 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 24 Aug 2018 15:40:56 +0500 Subject: [PATCH 0600/1846] Add option for limit order sizes --- dexbot/strategies/staggered_orders.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 660c4c9bf..cab341bde 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -658,7 +658,7 @@ def is_order_size_correct(self, order, orders): return False - def place_higher_buy_order(self, order, place_order=True, allow_partial=False): + def place_higher_buy_order(self, order, place_order=True, allow_partial=False, base_limit=None): """ Place higher buy order Mode: MOUNTAIN amount (QUOTE) = lower_buy_order_amount @@ -667,12 +667,17 @@ def place_higher_buy_order(self, order, place_order=True, allow_partial=False): :param order: Previously highest buy order :param bool | place_order: True = Places order to the market, False = returns amount and price :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance + :param float | base_limit: order should be limited in size by this BASE amount """ amount = order['quote']['amount'] price = order['price'] * (1 + self.increment) # How many BASE we need to buy QUOTE `amount` base_amount = amount * price + if base_limit and base_limit < base_amount: + base_amount = base_limit + amount = base_limit / price + if base_amount > self.base_balance['amount']: if place_order and not allow_partial: self.log.debug('Not enough balance to place_higher_buy_order; need/avail: {}/{}' @@ -742,7 +747,7 @@ def place_lower_buy_order(self, order, place_order=True, allow_partial=False): else: return {"amount": amount, "price": price} - def place_lower_sell_order(self, order, place_order=True, allow_partial=False): + def place_lower_sell_order(self, order, place_order=True, allow_partial=False, limit=None): """ Place lower sell order Mode: MOUNTAIN amount (BASE) = higher_sell_order_amount * (1 + increment) @@ -751,8 +756,11 @@ def place_lower_sell_order(self, order, place_order=True, allow_partial=False): :param order: Previously higher sell order :param bool | place_order: True = Places order to the market, False = returns amount and price :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance + :param float | limit: order should be limited in size by this QUOTE amount """ amount = order['base']['amount'] * (1 + self.increment) + if limit and limit < amount: + amount = limit price = (order['price'] ** -1) / (1 + self.increment) if amount > self.quote_balance['amount']: if place_order and not allow_partial: From 9d9d56ff5349437780aff00667977d85a8404c1a Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 24 Aug 2018 15:42:18 +0500 Subject: [PATCH 0601/1846] When in not bootstrap, limit new orders by opposite order amount --- dexbot/strategies/staggered_orders.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index cab341bde..2f642dbb4 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -307,8 +307,11 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): if self.bootstrapping: self.place_higher_buy_order(highest_buy_order) else: - # Allow to place partial order whether we are not in bootstrapping - self.place_higher_buy_order(highest_buy_order, allow_partial=True) + # Place order limited by size of the opposite-side order + lowest_sell_order = self.sell_orders[0]['quote']['amount'] + limit = lowest_sell_order / (1 + self.increment) + self.log.debug('Limiting buy order base by opposite order quote: {}'.format(limit)) + self.place_higher_buy_order(highest_buy_order, base_limit=limit) elif lowest_buy_order_price / (1 + self.increment) < self.lower_bound: if self.buy_orders and self.sell_orders: self.bootstrapping = False @@ -373,7 +376,11 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): if self.bootstrapping: self.place_lower_sell_order(lowest_sell_order) else: - self.place_lower_sell_order(lowest_sell_order, allow_partial=True) + # Place order limited by opposite-side order + highest_buy_order = self.buy_orders[0]['quote']['amount'] + limit = highest_buy_order / (1 + self.increment) + self.log.debug('Limiting sell order by opposite order quote: {}'.format(limit)) + self.place_lower_sell_order(lowest_sell_order, limit=limit) elif highest_sell_order_price * (1 + self.increment) > self.upper_bound: if self.buy_orders and self.sell_orders: self.bootstrapping = False From 798b53692fd24d39e3be155d7bcf54c21fcbe26f Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 24 Aug 2018 17:11:34 +0500 Subject: [PATCH 0602/1846] Turn bootstrap off when not enough balance This may occur in one-sided start when the first order gets filled and there are funds available. Intial distribution of new funds will not cover full range up to target spread because there will no enough balance. This change will allow "big" side to place an additional order. This new behavior is like just restarting the bot in such situation. --- dexbot/strategies/staggered_orders.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 2f642dbb4..a59d8ca9c 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -690,6 +690,8 @@ def place_higher_buy_order(self, order, place_order=True, allow_partial=False, b self.log.debug('Not enough balance to place_higher_buy_order; need/avail: {}/{}' .format(base_amount, self.base_balance['amount'])) place_order = False + # Turn bootstrap off, maybe we will be able to place limited order + self.bootstrapping = False elif allow_partial: self.log.debug('Limiting order amount to available balance: {}'.format(self.base_balance['amount'])) amount = self.base_balance['amount'] / price @@ -774,6 +776,8 @@ def place_lower_sell_order(self, order, place_order=True, allow_partial=False, l self.log.debug('Not enough balance to place_lower_sell_order; need/avail: {}/{}' .format(amount, self.quote_balance['amount'])) place_order = False + # Turn bootstrap off, maybe we will be able to place limited order + self.bootstrapping = False elif allow_partial: self.log.debug('Limiting order amount to available balance: {}'.format(self.quote_balance['amount'])) amount = self.quote_balance['amount'] From f5b4b7530e507ebe9e90506655ec6a0d9c2566e4 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 24 Aug 2018 17:57:00 +0500 Subject: [PATCH 0603/1846] More properly avoid orders increase when no opposite orders --- dexbot/strategies/staggered_orders.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index a59d8ca9c..55cdb785d 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -312,9 +312,11 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): limit = lowest_sell_order / (1 + self.increment) self.log.debug('Limiting buy order base by opposite order quote: {}'.format(limit)) self.place_higher_buy_order(highest_buy_order, base_limit=limit) + elif not self.sell_orders: + # Do not try to do anything than placing higher buy whether there is no sell orders + return elif lowest_buy_order_price / (1 + self.increment) < self.lower_bound: - if self.buy_orders and self.sell_orders: - self.bootstrapping = False + self.bootstrapping = False # Lower bound has been reached and now will start allocating rest of the base balance. self.log.debug('Increasing orders sizes for BASE asset') self.increase_order_sizes('base', base_balance, self.buy_orders) @@ -381,9 +383,11 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): limit = highest_buy_order / (1 + self.increment) self.log.debug('Limiting sell order by opposite order quote: {}'.format(limit)) self.place_lower_sell_order(lowest_sell_order, limit=limit) + elif not self.buy_orders: + # Do not try to do anything than placing lower sell whether there is no buy orders + return elif highest_sell_order_price * (1 + self.increment) > self.upper_bound: - if self.buy_orders and self.sell_orders: - self.bootstrapping = False + self.bootstrapping = False # Upper bound has been reached and now will start allocating rest of the quote balance. self.log.debug('Increasing orders sizes for QUOTE asset') self.increase_order_sizes('quote', quote_balance, self.sell_orders) @@ -424,10 +428,6 @@ def increase_order_sizes(self, asset, asset_balance, orders): :param list | orders: List of buy or sell orders :return None """ - # Disable size increase during boostrapping, may happen with one-sided start - if self.bootstrapping: - return - # Mountain mode: if self.mode == 'mountain': # Todo: Work in progress. From f1990a5964b01c694bc2e693d5e4ce96a0cedfe3 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 24 Aug 2018 19:11:47 +0500 Subject: [PATCH 0604/1846] Refactor checking of partially filled orders --- dexbot/strategies/staggered_orders.py | 64 ++++++++++++++++----------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 55cdb785d..d453cac9f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -66,6 +66,7 @@ def __init__(self, *args, **kwargs): self.increment = self.worker['increment'] / 100 self.upper_bound = self.worker['upper_bound'] self.lower_bound = self.worker['lower_bound'] + self.partial_fill_threshold = self.increment / 10 # Strategy variables # Bootstrap is turned on whether there will no buy or sell orders at all @@ -289,7 +290,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): lowest_buy_order_price = lowest_buy_order['price'] # Check if the order size is correct - if self.is_order_size_correct(highest_buy_order, self.buy_orders): + if self.check_partial_fill(highest_buy_order): # Calculate actual spread if self.sell_orders: lowest_sell_price = self.sell_orders[0]['price'] ** -1 @@ -325,19 +326,15 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): self.log.debug('Placing lower order than lowest_buy_order') self.place_lower_buy_order(lowest_buy_order, allow_partial=True) else: - self.log.debug('Order size is not correct, cancelling highest buy order in allocate_base_asset()') - # Cancel highest buy order and immediately replace it with new one. - self.cancel(highest_buy_order) - self.refresh_balances() - # We have several orders - if len(self.buy_orders) > 1: - self.place_higher_buy_order(self.buy_orders[1], allow_partial=True) - # Length is 1, we have only one order which is lowest_buy_order + # Make sure we have enough balance to replace partially filled order + if base_balance + highest_buy_order['for_sale']['amount'] >= highest_buy_order['base']['amount']: + # Cancel highest buy order and immediately replace it with new one. + self.log.info('Replacing partially filled buy order') + self.cancel(highest_buy_order) + self.market_buy(highest_buy_order['quote']['amount'], highest_buy_order['price']) + self.refresh_balances() else: - # We need to obtain total available base balance - total_balance = self.total_balance([], return_asset=True) - base_balance = total_balance['base'] - self.base_fee_reserve - self.place_lowest_buy_order(base_balance) + self.log.debug('Not replacing partially filled order because there is not enough funds') else: # Place first buy order as close to the lower bound as possible self.bootstrapping = True @@ -361,7 +358,7 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): highest_sell_order_price = (highest_sell_order['price'] ** -1) # Check if the order size is correct - if self.is_order_size_correct(lowest_sell_order, self.sell_orders): + if self.check_partial_fill(lowest_sell_order): # Calculate actual spread if self.buy_orders: highest_buy_price = self.buy_orders[0]['price'] @@ -395,19 +392,16 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): self.bootstrapping = False self.place_higher_sell_order(highest_sell_order, allow_partial=True) else: - # Cancel lowest sell order - self.log.debug('Order size is not correct, cancelling lowest sell order in allocate_quote_asset') - self.cancel(self.sell_orders[0]) - self.refresh_balances() - # We have several orders - if len(self.sell_orders) > 1: - self.place_lower_sell_order(self.sell_orders[1], allow_partial=True) - # Length is 1, we have only one order which is highest_sell_order + # Make sure we have enough balance to replace partially filled order + if quote_balance + lowest_sell_order['for_sale']['amount'] >= lowest_sell_order['base']['amount']: + # Cancel lowest sell order and immediately replace it with new one. + self.log.info('Replacing partially filled sell order') + self.cancel(lowest_sell_order) + price = lowest_sell_order['price'] ** -1 + self.market_sell(lowest_sell_order['base']['amount'], price) + self.refresh_balances() else: - total_balance = self.total_balance([], return_asset=True) - quote_balance = total_balance['quote'] - self.quote_fee_reserve - self.bootstrapping = True - self.place_highest_sell_order(quote_balance) + self.log.debug('Not replacing partially filled order because there is not enough funds') else: # Place first order as close to the upper bound as possible self.bootstrapping = True @@ -567,9 +561,27 @@ def increase_order_sizes(self, asset, asset_balance, orders): pass return None + def check_partial_fill(self, order): + """ Checks whether order was partially filled it needs to be replaced + + :param order: Order closest to the center price from buy or sell side + :return: bool | True = Order is correct size or within the threshold + False = Order is not right size + """ + if order['for_sale']['amount'] != order['base']['amount']: + diff_abs = order['base']['amount'] - order['for_sale']['amount'] + diff_rel = diff_abs / order['base']['amount'] + if diff_rel >= self.partial_fill_threshold: + self.log.debug('Partially filled order: {} @ {:.8f}, filled: {:.2%}'.format( + order['base']['amount'], order['price'], diff_rel)) + return False + return True + def is_order_size_correct(self, order, orders): """ Checks if the order is big enough. Oversized orders are allowed to enable manual manipulation + This is old version of check_partial_fill() + :param order: Order closest to the center price from buy or sell side :param orders: List of buy or sell orders :return: bool | True = Order is correct size or within the threshold From 45dce8c44a59f5d256d758d20191ccc30c0ff27a Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 24 Aug 2018 19:13:01 +0500 Subject: [PATCH 0605/1846] Adjust price precision in debug messages --- dexbot/strategies/staggered_orders.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index d453cac9f..25fb60964 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -303,7 +303,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): if self.actual_spread >= self.target_spread + self.increment: # Place order closer to the center price - self.log.debug('Placing higher buy order; actual spread: {}, target + increment: {}'.format( + self.log.debug('Placing higher buy order; actual spread: {:.8f}, target + increment: {}'.format( self.actual_spread, self.target_spread + self.increment)) if self.bootstrapping: self.place_higher_buy_order(highest_buy_order) @@ -370,7 +370,7 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): if self.actual_spread >= self.target_spread + self.increment: # Place order closer to the center price - self.log.debug('Placing lower sell order; actual spread: {}, target + increment: {}'.format( + self.log.debug('Placing lower sell order; actual spread: {:.8f}, target + increment: {}'.format( self.actual_spread, self.target_spread + self.increment)) if self.bootstrapping: self.place_lower_sell_order(lowest_sell_order) @@ -480,7 +480,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): price = (order['price'] ** -1) self.log.debug('Cancelling sell order in increase_order_sizes(); ' - 'mode: mountain, quote: {}, price: {}'.format(order_amount, price)) + 'mode: mountain, quote: {}, price: {:.8f}'.format(order_amount, price)) self.cancel(order) self.market_sell(new_order_amount, price) # Only one increase at a time. This prevents running more than one increment round @@ -545,7 +545,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): new_order_amount = new_base_amount / price self.log.debug('Cancelling buy order in increase_order_sizes(); ' - 'mode: mountain, base: {}, price: {}'.format(order_amount, order['price'])) + 'mode: mountain, base: {}, price: {:.8f}'.format(order_amount, order['price'])) self.cancel(order) self.market_buy(new_order_amount, price) # Only one increase at a time. This prevents running more than one increaement round From 2a40a876ff52a834cb78c1d02515692d1bfcfa56 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 24 Aug 2018 19:13:31 +0500 Subject: [PATCH 0606/1846] Add debug message --- dexbot/strategies/staggered_orders.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 25fb60964..89e1023cd 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -405,6 +405,7 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): else: # Place first order as close to the upper bound as possible self.bootstrapping = True + self.log.debug('Placing first sell order') self.place_highest_sell_order(quote_balance) # Get latest orders From 906bfd987aebcd7db5e26c8e75a995f4f77680b5 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 24 Aug 2018 20:02:32 +0500 Subject: [PATCH 0607/1846] Limit frequency of maintenance execution On active markets there are hundreds of events and we must avoid event queue build up. This is also prevents useless CPU load. --- dexbot/strategies/staggered_orders.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 89e1023cd..b93ba0495 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1,5 +1,6 @@ import math from datetime import datetime +from datetime import timedelta from dexbot.basestrategy import BaseStrategy, ConfigElement from dexbot.qt_queue.idle_queue import idle_add @@ -87,6 +88,8 @@ def __init__(self, *args, **kwargs): # Order expiration time self.expiration = 60 * 60 * 24 * 365 * 5 self.last_check = datetime.now() + # Minimal check interval is needed to prevent event queue accumulation + self.min_check_interval = 0.05 if self.view: self.update_gui_slider() @@ -96,6 +99,12 @@ def maintain_strategy(self, *args, **kwargs): :param args: :param kwargs: """ + delta = datetime.now() - self.last_check + + # Only allow to maintain whether minimal time passed. + if delta < timedelta(seconds=self.min_check_interval): + self.log.debug('Ignoring event as min_check_interval has not passed') + return # Get all user's orders on current market self.refresh_orders() @@ -178,6 +187,7 @@ def maintain_strategy(self, *args, **kwargs): # Do not continue whether assets is not fully allocated if not base_allocated and not quote_allocated: # Further checks should be performed on next maintenance + self.last_check = datetime.now() return # There are no funds and current orders aren't close enough, try to fix the situation by shifting orders @@ -198,6 +208,8 @@ def maintain_strategy(self, *args, **kwargs): self.log.debug('Cancelling highest sell order in maintain_strategy') self.cancel(self.sell_orders[-1]) + self.last_check = datetime.now() + def refresh_balances(self): """ This function is used to refresh account balances """ From f8bcdc0173ccdf96af16104abb33f6fdf55d8958 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 24 Aug 2018 20:04:06 +0500 Subject: [PATCH 0608/1846] Fix fallback behavior Bootstrap check was lost somewhere, so restore it. --- dexbot/strategies/staggered_orders.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index b93ba0495..09c0f847c 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -185,12 +185,13 @@ def maintain_strategy(self, *args, **kwargs): quote_allocated = True # Do not continue whether assets is not fully allocated - if not base_allocated and not quote_allocated: + if (not base_allocated and not quote_allocated) or self.bootstrapping: # Further checks should be performed on next maintenance self.last_check = datetime.now() return - # There are no funds and current orders aren't close enough, try to fix the situation by shifting orders + # There are no funds and current orders aren't close enough, try to fix the situation by shifting orders. + # This is a fallback logic. # Measure which price is closer to the center buy_distance = self.market_center_price - highest_buy_price sell_distance = lowest_sell_price - self.market_center_price From 0c0b748a587f236670994513437953ac04b4aeb9 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 24 Aug 2018 20:04:52 +0500 Subject: [PATCH 0609/1846] Execute strategy more frequently by tick count This improves initial bootstrap on inactive markets. --- 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 09c0f847c..b08330698 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -903,7 +903,7 @@ def pause(self): def tick(self, d): """ Ticks come in on every block """ - if not (self.counter or 0) % 5: + if not (self.counter or 0) % 3: self.maintain_strategy() self.counter += 1 From cddc5a808165f096f17bf0228b445fd2e41409b6 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 24 Aug 2018 23:16:09 +0500 Subject: [PATCH 0610/1846] Fix condition deading to fallback check --- 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 b08330698..fd63482a1 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -185,7 +185,7 @@ def maintain_strategy(self, *args, **kwargs): quote_allocated = True # Do not continue whether assets is not fully allocated - if (not base_allocated and not quote_allocated) or self.bootstrapping: + if (not base_allocated or not quote_allocated) or self.bootstrapping: # Further checks should be performed on next maintenance self.last_check = datetime.now() return From 82d33e5038c2e5caa511d38c914dcad73b5e2797 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 24 Aug 2018 23:17:52 +0500 Subject: [PATCH 0611/1846] Do not limit closest-to-center order size during size increase round When there is excess funds, we do not want to limit their allocation by enforcing opposite-side order size check. --- dexbot/strategies/staggered_orders.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index fd63482a1..685ba4dd7 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -460,10 +460,10 @@ def increase_order_sizes(self, asset, asset_balance, orders): # This check prevents choosing order with index lower than the list length if order_index == 0: - # In case checking the first order, use highest BUY order in comparison - # This means our highest-sized order will not exceed our highest BUY order - lower_order = self.buy_orders[0] - lower_bound = lower_order['quote']['amount'] + # In case checking the first order, use the same order, but increased by 1 increment + # This allows our lowest sell order amount exceed highest buy order + lower_order = order + lower_bound = lower_order['base']['amount'] * (1 + self.increment) else: lower_order = orders[order_index - 1] lower_bound = lower_order['base']['amount'] @@ -523,9 +523,10 @@ def increase_order_sizes(self, asset, asset_balance, orders): # This check prevents choosing order with index lower than the list length if order_index == 0: - # In case checking the first order, use lowest SELL order in comparison - higher_order = self.sell_orders[0] - higher_bound = higher_order['quote']['amount'] + # In case checking the first order, use the same order, but increased by 1 increment + # This allows our highest buy order amount exceed lowest sell order + higher_order = order + higher_bound = higher_order['base']['amount'] * (1 + self.increment) else: higher_order = orders[order_index - 1] higher_bound = higher_order['base']['amount'] From 8961ddd3c9760ad939779d13f36717511c2a3dc1 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 25 Aug 2018 00:07:36 +0500 Subject: [PATCH 0612/1846] Refactor how we're doing opposite-side limiting Previous version used base/quote incorrectly, now we're actually building a mountain. --- dexbot/strategies/staggered_orders.py | 37 ++++++++++++++++++++------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 685ba4dd7..f28209393 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -322,10 +322,10 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): self.place_higher_buy_order(highest_buy_order) else: # Place order limited by size of the opposite-side order - lowest_sell_order = self.sell_orders[0]['quote']['amount'] + lowest_sell_order = self.sell_orders[0]['base']['amount'] limit = lowest_sell_order / (1 + self.increment) - self.log.debug('Limiting buy order base by opposite order quote: {}'.format(limit)) - self.place_higher_buy_order(highest_buy_order, base_limit=limit) + self.log.debug('Limiting buy order base by opposite order: {}'.format(limit)) + self.place_higher_buy_order(highest_buy_order, limit=limit, allow_partial=True) elif not self.sell_orders: # Do not try to do anything than placing higher buy whether there is no sell orders return @@ -389,10 +389,10 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): self.place_lower_sell_order(lowest_sell_order) else: # Place order limited by opposite-side order - highest_buy_order = self.buy_orders[0]['quote']['amount'] + highest_buy_order = self.buy_orders[0]['base']['amount'] limit = highest_buy_order / (1 + self.increment) self.log.debug('Limiting sell order by opposite order quote: {}'.format(limit)) - self.place_lower_sell_order(lowest_sell_order, limit=limit) + self.place_lower_sell_order(lowest_sell_order, base_limit=limit, allow_partial=True) elif not self.buy_orders: # Do not try to do anything than placing lower sell whether there is no buy orders return @@ -692,7 +692,7 @@ def is_order_size_correct(self, order, orders): return False - def place_higher_buy_order(self, order, place_order=True, allow_partial=False, base_limit=None): + def place_higher_buy_order(self, order, place_order=True, allow_partial=False, base_limit=None, limit=None): """ Place higher buy order Mode: MOUNTAIN amount (QUOTE) = lower_buy_order_amount @@ -702,7 +702,13 @@ def place_higher_buy_order(self, order, place_order=True, allow_partial=False, b :param bool | place_order: True = Places order to the market, False = returns amount and price :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance :param float | base_limit: order should be limited in size by this BASE amount + :param float | limit: order should be limited in size by this QUOTE amount """ + if base_limit and limit: + self.log.error('Only base_limit or limit should be specified') + self.disabled = True + return None + amount = order['quote']['amount'] price = order['price'] * (1 + self.increment) # How many BASE we need to buy QUOTE `amount` @@ -711,6 +717,9 @@ def place_higher_buy_order(self, order, place_order=True, allow_partial=False, b if base_limit and base_limit < base_amount: base_amount = base_limit amount = base_limit / price + elif limit: + base_amount = limit * price + amount = limit if base_amount > self.base_balance['amount']: if place_order and not allow_partial: @@ -783,7 +792,7 @@ def place_lower_buy_order(self, order, place_order=True, allow_partial=False): else: return {"amount": amount, "price": price} - def place_lower_sell_order(self, order, place_order=True, allow_partial=False, limit=None): + def place_lower_sell_order(self, order, place_order=True, allow_partial=False, base_limit=None, limit=None): """ Place lower sell order Mode: MOUNTAIN amount (BASE) = higher_sell_order_amount * (1 + increment) @@ -792,12 +801,22 @@ def place_lower_sell_order(self, order, place_order=True, allow_partial=False, l :param order: Previously higher sell order :param bool | place_order: True = Places order to the market, False = returns amount and price :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance + :param float | base_limit: order should be limited in size by this BASE amount :param float | limit: order should be limited in size by this QUOTE amount """ + if base_limit and limit: + self.log.error('Only base_limit or limit should be specified') + self.disabled = True + return None + amount = order['base']['amount'] * (1 + self.increment) - if limit and limit < amount: - amount = limit price = (order['price'] ** -1) / (1 + self.increment) + + if base_limit: + amount = base_limit / price + elif limit and limit < amount: + amount = limit + if amount > self.quote_balance['amount']: if place_order and not allow_partial: self.log.debug('Not enough balance to place_lower_sell_order; need/avail: {}/{}' From b0f4f2aa81ef935842266275410eff8f42171ca2 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 25 Aug 2018 09:56:13 +0500 Subject: [PATCH 0613/1846] Remove a couple of unneeded debug messages --- dexbot/strategies/staggered_orders.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index f28209393..499d73273 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -841,8 +841,6 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente :param float | market_center_price: Optional market center price, used to to check order :return dict | order: Returns highest sell order """ - self.log.debug('quote_balance in place_highest_sell_order: {}'.format(quote_balance)) - if not market_center_price: market_center_price = self.market_center_price @@ -883,8 +881,6 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p :param float | market_center_price: Optional market center price, used to to check order :return dict | order: Returns lowest buy order """ - self.log.debug('base_balance in place_highest_sell_order: {}'.format(base_balance)) - if not market_center_price: market_center_price = self.market_center_price From e46d85118cfd9495bb142a89b9260c559a2ef5b4 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 25 Aug 2018 09:59:18 +0500 Subject: [PATCH 0614/1846] Optimize initial order size This significantly reduces order increasements. --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 499d73273..c2ddacd84 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -861,7 +861,7 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente precision = self.market['quote']['precision'] price = previous_price - amount_quote = previous_amount * (self.quote_total_balance / orders_sum) + amount_quote = previous_amount * (self.quote_total_balance / orders_sum) * (1 + self.increment * 0.75) amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) if place_order: @@ -900,7 +900,7 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p amount = amount / (1 + self.increment) precision = self.market['quote']['precision'] - amount_base = previous_amount * (self.base_total_balance / orders_sum) + amount_base = previous_amount * (self.base_total_balance / orders_sum) * (1 + self.increment * 0.75) price = previous_price amount_quote = amount_base / price amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) From 985020cc32d9cc3f391cd0ea512e78ff2a919b8b Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 25 Aug 2018 10:21:20 +0500 Subject: [PATCH 0615/1846] Optimize furthest order sizes when increasing orders This greatly reduces number of allocation rounds needed when there are new huge funds available, which may occur when starting one-sided or when one of the side is significantly bigger than another, or when there is huge funds arrived via transfer from another account. --- dexbot/strategies/staggered_orders.py | 58 +++++++++++++++++++-------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c2ddacd84..2c9971954 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -447,11 +447,11 @@ def increase_order_sizes(self, asset, asset_balance, orders): If highest_sell_order is reached, increase it to maximum size Maximum size is: - as many quote as the order below - and - as many quote * (1 + increment) as the order above - When making an order, it must not exceed either of these limits, but be - made according to the more limiting criteria. + 1. As many "quote * (1 + increment)" as the order below (higher_bound) + AND + 2. As many "quote as the order above (lower_bound) + + Also when making an order it's size always will be limited by available free balance """ # Get orders and amounts to be compared. Note: orders are sorted from low price to high for order in orders: @@ -468,11 +468,13 @@ def increase_order_sizes(self, asset, asset_balance, orders): lower_order = orders[order_index - 1] lower_bound = lower_order['base']['amount'] - higher_order = orders[order_index] - # This check prevents choosing order with index higher than the list length if order_index + 1 < len(orders): higher_order = orders[order_index + 1] + is_least_order = False + else: + higher_order = orders[order_index] + is_least_order = True higher_bound = higher_order['base']['amount'] * (1 + self.increment) @@ -483,8 +485,21 @@ def increase_order_sizes(self, asset, asset_balance, orders): # Calculate new order size and place the order to the market new_order_amount = higher_bound - if higher_bound > lower_bound: - new_order_amount = lower_bound + if is_least_order: + new_orders_sum = 0 + amount = order_amount + for o in orders: + amount = amount * (1 + self.increment) + new_orders_sum += amount + # To reduce allocation rounds, increase furthest order more + new_order_amount = order_amount * (self.quote_total_balance / new_orders_sum) \ + * (1 + self.increment * 0.75) + if new_order_amount < lower_bound: + """ Use partial-increment increase, so we'll got at least one full increase round. + Whether we will just use `new_order_amount = lower_bound`, we will get less than + one full allocation round, thus leaving lowest sell order not increased. + """ + new_order_amount = lower_bound * (1 - self.increment * 0.2) # Limit sell order to available balance if asset_balance < new_order_amount - order_amount: @@ -510,9 +525,9 @@ def increase_order_sizes(self, asset, asset_balance, orders): If lowest_buy_order is reached, increase it to maximum size. Maximum size is: - 1. As many BASE as the order above (higher order) + 1. As many "base * (1 + increment)" as the order below (lower_bound) AND - 2. As many BASE * (1 + increment) as the order below (lower order) + 2. As many "base" as the order above (higher_bound) Also when making an order it's size always will be limited by available free balance """ @@ -531,13 +546,15 @@ def increase_order_sizes(self, asset, asset_balance, orders): higher_order = orders[order_index - 1] higher_bound = higher_order['base']['amount'] - # Initially set lower order to the current - lower_order = orders[order_index] - # This check prevents choosing order with index higher than the list length if order_index + 1 < len(orders): # If this is not a lowest_buy_order, lower order is a next order down lower_order = orders[order_index + 1] + is_least_order = False + else: + # Current order + lower_order = orders[order_index] + is_least_order = True lower_bound = lower_order['base']['amount'] * (1 + self.increment) @@ -549,8 +566,17 @@ def increase_order_sizes(self, asset, asset_balance, orders): new_base_amount = lower_bound price = order['price'] - if lower_bound > higher_bound: - new_base_amount = higher_bound + if is_least_order: + # To reduce allocation rounds, increase furthest order more + new_orders_sum = 0 + amount = order_amount + for o in orders: + amount = amount * (1 + self.increment) + new_orders_sum += amount + new_base_amount = order_amount * (self.base_total_balance / new_orders_sum) \ + * (1 + self.increment * 0.75) + if new_base_amount < higher_bound: + new_base_amount = higher_bound * (1 - self.increment * 0.2) # Limit buy order to available balance if (asset_balance / price) < (new_base_amount - order_amount) / price: From 2d1006d1bdac8750b8baaaebefe9e06111fea4d8 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 25 Aug 2018 10:53:49 +0500 Subject: [PATCH 0616/1846] Switch to ui autogeneration for staggered orders --- dexbot/controllers/worker_controller.py | 2 +- .../views/ui/forms/staggered_orders_widget.ui | 899 ------------------ 2 files changed, 1 insertion(+), 900 deletions(-) delete mode 100644 dexbot/views/ui/forms/staggered_orders_widget.ui diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index cadceaf47..d6850083d 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -32,7 +32,7 @@ def strategies(self): } strategies['dexbot.strategies.staggered_orders'] = { 'name': 'Staggered Orders', - 'form_module': 'dexbot.views.ui.forms.staggered_orders_widget_ui' + 'form_module': '' } for desc, module in find_external_strategies(): strategies[module] = {'name': desc, 'form_module': module} diff --git a/dexbot/views/ui/forms/staggered_orders_widget.ui b/dexbot/views/ui/forms/staggered_orders_widget.ui deleted file mode 100644 index 54b5b26dd..000000000 --- a/dexbot/views/ui/forms/staggered_orders_widget.ui +++ /dev/null @@ -1,899 +0,0 @@ - - - Form - - - - 0 - 0 - 449 - 333 - - - - Form - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Worker Parameters - - - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Mode - - - true - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - How to allocate funds and profits. Doesn't effect existing orders, only future ones - - - - - - ? - - - 5 - - - - - - - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Spread - - - true - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - The percentage difference between buy and sell - - - ? - - - 5 - - - - - - - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - % - - - 100000.000000000000000 - - - 6.000000000000000 - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Increment - - - true - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - The percentage difference between each staggered order - - - ? - - - 5 - - - - - - - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - % - - - 100000.000000000000000 - - - 4.000000000000000 - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Lower bound - - - true - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - The bottom price in the range - - - ? - - - 5 - - - - - - - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 8 - - - 1000000000.000000000000000 - - - 0.000001000000000 - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Center Price - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Fixed center price expressed in base asset: base/quote - - - ? - - - 5 - - - - - - - - - - false - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - false - - - false - - - 8 - - - 0.000000000000000 - - - 999999999.998999953269958 - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Always calculate the middle from the closest market orders - - - ? - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - 5 - - - - - - - - - - - 0 - 0 - - - - Update center price from closest market orders - - - true - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Upper bound - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - The top price in the range - - - ? - - - 5 - - - - - - - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 8 - - - 1000000000.000000000000000 - - - 1000000.000000000000000 - - - - - - - - - - - - center_price_dynamic_input - toggled(bool) - center_price_input - setDisabled(bool) - - - 281 - 272 - - - 217 - 236 - - - - - From deb03a39f5b75fe7718e5e1614c8a8637171e2f3 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 25 Aug 2018 13:11:53 +0500 Subject: [PATCH 0617/1846] Fix limiting in place_higher_buy_order() --- 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 2c9971954..9781a5586 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -743,7 +743,8 @@ def place_higher_buy_order(self, order, place_order=True, allow_partial=False, b if base_limit and base_limit < base_amount: base_amount = base_limit amount = base_limit / price - elif limit: + elif limit and limit < amount: + # Limit order amount only when is lower than amount base_amount = limit * price amount = limit From 6cbfa20e09282fdf41774a16ab693bfbff7f8c3e Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 25 Aug 2018 13:12:31 +0500 Subject: [PATCH 0618/1846] Measure maintenance execution time --- dexbot/strategies/staggered_orders.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 9781a5586..b5e3766b1 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -99,7 +99,8 @@ def maintain_strategy(self, *args, **kwargs): :param args: :param kwargs: """ - delta = datetime.now() - self.last_check + start = datetime.now() + delta = start - self.last_check # Only allow to maintain whether minimal time passed. if delta < timedelta(seconds=self.min_check_interval): @@ -187,7 +188,10 @@ def maintain_strategy(self, *args, **kwargs): # Do not continue whether assets is not fully allocated if (not base_allocated or not quote_allocated) or self.bootstrapping: # Further checks should be performed on next maintenance - self.last_check = datetime.now() + now = datetime.now() + self.last_check = now + delta = now - start + self.log.debug('Maintenance execution took: {:.2f} seconds'.format(delta.total_seconds())) return # There are no funds and current orders aren't close enough, try to fix the situation by shifting orders. From 6dac27b7d9d0cbeeac6115643d361a5209cbc65e Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 25 Aug 2018 13:49:16 +0500 Subject: [PATCH 0619/1846] Turn bootstrapping on by default again --- dexbot/strategies/staggered_orders.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index b5e3766b1..d2d076cdd 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -70,8 +70,8 @@ def __init__(self, *args, **kwargs): self.partial_fill_threshold = self.increment / 10 # Strategy variables - # Bootstrap is turned on whether there will no buy or sell orders at all - self.bootstrapping = False + # Assume we are in bootstrap mode by default. This prevents weird things when bootstrap was interrupted + self.bootstrapping = True self.market_center_price = None self.initial_market_center_price = None self.buy_orders = [] @@ -111,9 +111,6 @@ def maintain_strategy(self, *args, **kwargs): self.refresh_orders() # market_orders = self.market.orderbook(1) - if not self.buy_orders or not self.sell_orders: - self.bootstrapping = True - # Check if market center price is calculated if not self.bootstrapping: self.market_center_price = self.calculate_center_price(suppress_errors=True) From 5bc006d772cbf9b2b8a83124a1bb6ae808bd9112 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 25 Aug 2018 13:49:47 +0500 Subject: [PATCH 0620/1846] Revert "Turn bootstrap off when not enough balance" This reverts commit fe37f05cdc98de5a4aba932a194b4c1075306aae. This change is reverted because it may bring some mess in another bootstrap situations. --- dexbot/strategies/staggered_orders.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index d2d076cdd..083350287 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -754,8 +754,6 @@ def place_higher_buy_order(self, order, place_order=True, allow_partial=False, b self.log.debug('Not enough balance to place_higher_buy_order; need/avail: {}/{}' .format(base_amount, self.base_balance['amount'])) place_order = False - # Turn bootstrap off, maybe we will be able to place limited order - self.bootstrapping = False elif allow_partial: self.log.debug('Limiting order amount to available balance: {}'.format(self.base_balance['amount'])) amount = self.base_balance['amount'] / price @@ -850,8 +848,6 @@ def place_lower_sell_order(self, order, place_order=True, allow_partial=False, b self.log.debug('Not enough balance to place_lower_sell_order; need/avail: {}/{}' .format(amount, self.quote_balance['amount'])) place_order = False - # Turn bootstrap off, maybe we will be able to place limited order - self.bootstrapping = False elif allow_partial: self.log.debug('Limiting order amount to available balance: {}'.format(self.quote_balance['amount'])) amount = self.quote_balance['amount'] From 067b803cf386b7fce9d26f9160c8acf5b707b503 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 25 Aug 2018 14:12:21 +0500 Subject: [PATCH 0621/1846] Disallow partial lower_buy / higher_sell orders This was a workaround for several cases, probably we should not have a problems now. --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 083350287..c136febf4 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -326,7 +326,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): lowest_sell_order = self.sell_orders[0]['base']['amount'] limit = lowest_sell_order / (1 + self.increment) self.log.debug('Limiting buy order base by opposite order: {}'.format(limit)) - self.place_higher_buy_order(highest_buy_order, limit=limit, allow_partial=True) + self.place_higher_buy_order(highest_buy_order, limit=limit, allow_partial=False) elif not self.sell_orders: # Do not try to do anything than placing higher buy whether there is no sell orders return @@ -393,7 +393,7 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): highest_buy_order = self.buy_orders[0]['base']['amount'] limit = highest_buy_order / (1 + self.increment) self.log.debug('Limiting sell order by opposite order quote: {}'.format(limit)) - self.place_lower_sell_order(lowest_sell_order, base_limit=limit, allow_partial=True) + self.place_lower_sell_order(lowest_sell_order, base_limit=limit, allow_partial=False) elif not self.buy_orders: # Do not try to do anything than placing lower sell whether there is no buy orders return From 5bdce840b6a74662c4655788ebca8a309020cfbf Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 25 Aug 2018 14:14:36 +0500 Subject: [PATCH 0622/1846] Move maintenance measurement to separate function --- dexbot/strategies/staggered_orders.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c136febf4..ce5b27b9c 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -99,8 +99,8 @@ def maintain_strategy(self, *args, **kwargs): :param args: :param kwargs: """ - start = datetime.now() - delta = start - self.last_check + self.start = datetime.now() + delta = self.start - self.last_check # Only allow to maintain whether minimal time passed. if delta < timedelta(seconds=self.min_check_interval): @@ -117,6 +117,7 @@ def maintain_strategy(self, *args, **kwargs): elif not self.market_center_price: # On empty market we have to pass the user specified center price self.market_center_price = self.calculate_center_price(center_price=self.center_price, suppress_errors=True) + self.log_maintenance_time() return elif self.market_center_price and not self.initial_market_center_price: # Save initial market center price @@ -164,6 +165,7 @@ def maintain_strategy(self, *args, **kwargs): self.refresh_orders() else: # Return back to beginning + self.log_maintenance_time() return # BASE asset check @@ -185,10 +187,8 @@ def maintain_strategy(self, *args, **kwargs): # Do not continue whether assets is not fully allocated if (not base_allocated or not quote_allocated) or self.bootstrapping: # Further checks should be performed on next maintenance - now = datetime.now() - self.last_check = now - delta = now - start - self.log.debug('Maintenance execution took: {:.2f} seconds'.format(delta.total_seconds())) + self.last_check = datetime.now() + self.log_maintenance_time() return # There are no funds and current orders aren't close enough, try to fix the situation by shifting orders. @@ -211,6 +211,13 @@ def maintain_strategy(self, *args, **kwargs): self.cancel(self.sell_orders[-1]) self.last_check = datetime.now() + self.log_maintenance_time() + + def log_maintenance_time(self): + """ Measure time from self.start and print a log message + """ + delta = datetime.now() - self.start + self.log.debug('Maintenance execution took: {:.2f} seconds'.format(delta.total_seconds())) def refresh_balances(self): """ This function is used to refresh account balances From d44edf9efcabe36d1d8bc488351089ee8fe47bca Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 25 Aug 2018 14:54:00 +0500 Subject: [PATCH 0623/1846] Implement 'instant_fill' option --- dexbot/strategies/staggered_orders.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index ce5b27b9c..b1416e762 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -41,7 +41,10 @@ def configure(cls, return_base_config=True): 'The bottom price in the range', (0, None, 8, '')), ConfigElement( 'upper_bound', 'float', 1000000, 'Upper bound', - 'The top price in the range', (0, None, 8, '')) + 'The top price in the range', (0, None, 8, '')), + ConfigElement( + 'instant_fill', 'bool', True, 'Allow instant fill', + 'Allow to execute orders by market', None) ] def __init__(self, *args, **kwargs): @@ -68,6 +71,7 @@ def __init__(self, *args, **kwargs): self.upper_bound = self.worker['upper_bound'] self.lower_bound = self.worker['lower_bound'] self.partial_fill_threshold = self.increment / 10 + self.is_instant_fill_enabled = self.worker.get('instant_fill', True) # Strategy variables # Assume we are in bootstrap mode by default. This prevents weird things when bootstrap was interrupted @@ -84,6 +88,7 @@ def __init__(self, *args, **kwargs): self.base_total_balance = 0 self.quote_balance = 0 self.base_balance = 0 + self.ticker = None # Order expiration time self.expiration = 60 * 60 * 24 * 365 * 5 @@ -748,6 +753,10 @@ def place_higher_buy_order(self, order, place_order=True, allow_partial=False, b # How many BASE we need to buy QUOTE `amount` base_amount = amount * price + if not self.is_instant_fill_enabled and price > float(self.ticker['lowestAsk']): + self.log.info('Refusing to place an order which crosses lowestAsk') + return None + if base_limit and base_limit < base_amount: base_amount = base_limit amount = base_limit / price @@ -845,6 +854,10 @@ def place_lower_sell_order(self, order, place_order=True, allow_partial=False, b amount = order['base']['amount'] * (1 + self.increment) price = (order['price'] ** -1) / (1 + self.increment) + if not self.is_instant_fill_enabled and price < float(self.ticker['highestBid']): + self.log.info('Refusing to place an order which crosses highestBid') + return None + if base_limit: amount = base_limit / price elif limit and limit < amount: From 2aa5b726f131b75e392e2049aefe8b54c85d4491 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 25 Aug 2018 14:55:03 +0500 Subject: [PATCH 0624/1846] Switch to self.ticker() usage --- dexbot/strategies/staggered_orders.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index b1416e762..d4d85a0d5 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -234,8 +234,8 @@ def refresh_balances(self): self.quote_balance = account_balances['quote'] # Reserve transaction fee equivalent in BTS - ticker = self.market.ticker() - core_exchange_rate = ticker['core_exchange_rate'] + self.ticker = self.market.ticker() + core_exchange_rate = self.ticker['core_exchange_rate'] # Todo: order_creation_fee(BTS) = 0.01 for now self.quote_fee_reserve = 0.01 * core_exchange_rate['quote']['amount'] * 100 self.base_fee_reserve = 0.01 * core_exchange_rate['base']['amount'] * 100 @@ -968,8 +968,7 @@ def tick(self, d): self.counter += 1 def update_gui_slider(self): - ticker = self.market.ticker() - latest_price = ticker.get('latest', {}).get('price', None) + latest_price = self.ticker.get('latest', {}).get('price', None) if not latest_price: return From a9465ce767628bbe5398c21c396f54a4813e61df Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 25 Aug 2018 19:03:31 +0500 Subject: [PATCH 0625/1846] Respect center price enable/disable setting --- dexbot/strategies/staggered_orders.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index d4d85a0d5..7ca560401 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -66,12 +66,16 @@ def __init__(self, *args, **kwargs): self.view = kwargs.get('view') self.mode = self.worker['mode'] self.target_spread = self.worker['spread'] / 100 - self.center_price = self.worker['center_price'] self.increment = self.worker['increment'] / 100 self.upper_bound = self.worker['upper_bound'] self.lower_bound = self.worker['lower_bound'] self.partial_fill_threshold = self.increment / 10 self.is_instant_fill_enabled = self.worker.get('instant_fill', True) + 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.worker['center_price'] # Strategy variables # Assume we are in bootstrap mode by default. This prevents weird things when bootstrap was interrupted From e7db8daab8fef3e548edf7344b7efbd8138a2582 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 25 Aug 2018 23:46:17 +0500 Subject: [PATCH 0626/1846] Flip amount calculation for readability --- dexbot/strategies/staggered_orders.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 7ca560401..afbd62306 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -511,12 +511,15 @@ def increase_order_sizes(self, asset, asset_balance, orders): # To reduce allocation rounds, increase furthest order more new_order_amount = order_amount * (self.quote_total_balance / new_orders_sum) \ * (1 + self.increment * 0.75) + if new_order_amount < lower_bound: - """ Use partial-increment increase, so we'll got at least one full increase round. - Whether we will just use `new_order_amount = lower_bound`, we will get less than - one full allocation round, thus leaving lowest sell order not increased. + """ This is for situations when calculated new_order_amount is not big enough to + allocate all funds. Use partial-increment increase, so we'll got at least one full + increase round. Whether we will just use `new_order_amount = lower_bound`, we will + get less than one full allocation round, thus leaving lowest sell order not + increased. """ - new_order_amount = lower_bound * (1 - self.increment * 0.2) + new_order_amount = lower_bound / (1 + self.increment * 0.2) # Limit sell order to available balance if asset_balance < new_order_amount - order_amount: @@ -593,7 +596,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): new_base_amount = order_amount * (self.base_total_balance / new_orders_sum) \ * (1 + self.increment * 0.75) if new_base_amount < higher_bound: - new_base_amount = higher_bound * (1 - self.increment * 0.2) + new_base_amount = higher_bound / (1 + self.increment * 0.2) # Limit buy order to available balance if (asset_balance / price) < (new_base_amount - order_amount) / price: From bb43b4f4d9511c866e4c51d9955a6fd5d5832505 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 26 Aug 2018 23:06:16 +0500 Subject: [PATCH 0627/1846] Fix typo --- 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 afbd62306..a1735e09f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -768,7 +768,7 @@ def place_higher_buy_order(self, order, place_order=True, allow_partial=False, b base_amount = base_limit amount = base_limit / price elif limit and limit < amount: - # Limit order amount only when is lower than amount + # Limit order amount only when it is lower than amount base_amount = limit * price amount = limit From 4bb41de6b8b8f10063e9eaf8e1f7587ae38ca240 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 26 Aug 2018 23:06:32 +0500 Subject: [PATCH 0628/1846] Restore own ticker usage inside update_gui_slider() As reported by Marko: Traceback (most recent call last): File "/home/marko/localsoftware/dexbot/dexbot/worker.py", line 76, in init_workers view=self.view File "/home/marko/localsoftware/dexbot/dexbot/strategies/staggered_orders.py", line 104, in __init__ self.update_gui_slider() File "/home/marko/localsoftware/dexbot/dexbot/strategies/staggered_orders.py", line 976, in update_gui_slider latest_price = self.ticker.get('latest', {}).get('price', None) AttributeError: 'NoneType' object has no attribute 'get' --- 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 a1735e09f..d00c39495 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -975,7 +975,8 @@ def tick(self, d): self.counter += 1 def update_gui_slider(self): - latest_price = self.ticker.get('latest', {}).get('price', None) + ticker = self.market.ticker() + latest_price = ticker.get('latest', {}).get('price', None) if not latest_price: return From fc85b57721c19799d6aba54b827d52371a316ca6 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 27 Aug 2018 00:05:31 +0500 Subject: [PATCH 0629/1846] Add a couple of comments --- dexbot/strategies/staggered_orders.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index d00c39495..c22abab91 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -352,6 +352,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): self.log.debug('Increasing orders sizes for BASE asset') self.increase_order_sizes('base', base_balance, self.buy_orders) else: + # Lower bound is not reached, we need to add additional orders at the extremes self.bootstrapping = False self.log.debug('Placing lower order than lowest_buy_order') self.place_lower_buy_order(lowest_buy_order, allow_partial=True) @@ -419,6 +420,7 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): self.log.debug('Increasing orders sizes for QUOTE asset') self.increase_order_sizes('quote', quote_balance, self.sell_orders) else: + # Higher bound is not reached, we need to add additional orders at the extremes self.bootstrapping = False self.place_higher_sell_order(highest_sell_order, allow_partial=True) else: From 360a582360ed60eec1339322dcef30ab3bd566f9 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 27 Aug 2018 00:06:37 +0500 Subject: [PATCH 0630/1846] Implement additional check to determine bootstrap status --- dexbot/strategies/staggered_orders.py | 41 ++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c22abab91..b5bc6f9ae 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -93,6 +93,8 @@ def __init__(self, *args, **kwargs): self.quote_balance = 0 self.base_balance = 0 self.ticker = None + self.quote_asset_threshold = 0 + self.base_asset_threshold = 0 # Order expiration time self.expiration = 60 * 60 * 24 * 365 * 5 @@ -158,8 +160,8 @@ def maintain_strategy(self, *args, **kwargs): self.refresh_balances() # Calculate asset thresholds - quote_asset_threshold = self.quote_total_balance / 20000 - base_asset_threshold = self.base_total_balance / 20000 + self.quote_asset_threshold = self.quote_total_balance / 20000 + self.base_asset_threshold = self.base_total_balance / 20000 # Check market's price boundaries if self.market_center_price > self.upper_bound: @@ -178,7 +180,7 @@ def maintain_strategy(self, *args, **kwargs): return # BASE asset check - if self.base_balance > base_asset_threshold: + if self.base_balance > self.base_asset_threshold: base_allocated = False # Allocate available BASE funds self.allocate_base_asset(self.base_balance) @@ -186,7 +188,7 @@ def maintain_strategy(self, *args, **kwargs): base_allocated = True # QUOTE asset check - if self.quote_balance > quote_asset_threshold: + if self.quote_balance > self.quote_asset_threshold: quote_allocated = False # Allocate available QUOTE funds self.allocate_quote_asset(self.quote_balance) @@ -332,6 +334,33 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): self.actual_spread = (lowest_sell_price / highest_buy_price) - 1 if self.actual_spread >= self.target_spread + self.increment: + if self.quote_balance <= self.quote_asset_threshold and self.bootstrapping: + """ During the bootstrap we're fist placing orders of some amounts, than we are reaching target + spread and then turning bootstrap flag off and starting to allocate remaining balance by + gradually increasing order sizes. After bootstrap is complete and following order size + increase is complete too, we will not have available balance. + + When we have a different amount of assets (for example, 100 USD for base and 1 BTC for + quote), the orders on the one size will be bigger than at the opposite. + + During the bootstrap we are not allowing to place orders with limited amount by opposite + order. Bootstrap is designed to place orders of the same size. But, when the bootstrap is + done, we are beginning to limit new orders by opposite side orders. We need this to stay in + game when orders on the lower side gets filled. Because they are less than big-side orders, + we cannot just place another big order on the big side. So we are limiting the big-side + order to amount of a low-side one! + + Normally we are turning bootstrap off after initial allocation is done and we're biginning + to distribute remaining funds. But, whether we will restart the bot after size increase was + done, we have no chance to know if bootsrap was done or not. This is where this check comes + in! The situation when the target spread is not reached, but we have some available balance + on the one side and not have any free balance of the other side, clearly says to us that an + order from lower-side was filled! Thus, we can safely turn bootstrap off and thus place an + order limited in size by opposite-side order. + """ + self.log.debug('Turning bootstrapping off: actual_spread > target_spread, and not having ' + 'opposite-side balance') + self.bootstrapping = False # Place order closer to the center price self.log.debug('Placing higher buy order; actual spread: {:.8f}, target + increment: {}'.format( self.actual_spread, self.target_spread + self.increment)) @@ -400,6 +429,10 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): self.actual_spread = (lowest_sell_price / highest_buy_price) - 1 if self.actual_spread >= self.target_spread + self.increment: + if self.base_balance <= self.base_asset_threshold and self.bootstrapping: + self.log.debug('Turning bootstrapping off: actual_spread > target_spread, and not having ' + 'opposite-side balance') + self.bootstrapping = False # Place order closer to the center price self.log.debug('Placing lower sell order; actual spread: {:.8f}, target + increment: {}'.format( self.actual_spread, self.target_spread + self.increment)) From 2d7a3e07814f8e1e7a3194ffb4d7da37431b1278 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 27 Aug 2018 00:34:49 +0500 Subject: [PATCH 0631/1846] Simplify how we're calculating amount of limited orders --- dexbot/strategies/staggered_orders.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index b5bc6f9ae..b230e0ff1 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -368,10 +368,10 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): self.place_higher_buy_order(highest_buy_order) else: # Place order limited by size of the opposite-side order - lowest_sell_order = self.sell_orders[0]['base']['amount'] - limit = lowest_sell_order / (1 + self.increment) + lowest_sell_order = self.sell_orders[0] + limit = lowest_sell_order['quote']['amount'] self.log.debug('Limiting buy order base by opposite order: {}'.format(limit)) - self.place_higher_buy_order(highest_buy_order, limit=limit, allow_partial=False) + self.place_higher_buy_order(highest_buy_order, base_limit=limit, allow_partial=False) elif not self.sell_orders: # Do not try to do anything than placing higher buy whether there is no sell orders return @@ -440,10 +440,10 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): self.place_lower_sell_order(lowest_sell_order) else: # Place order limited by opposite-side order - highest_buy_order = self.buy_orders[0]['base']['amount'] - limit = highest_buy_order / (1 + self.increment) + highest_buy_order = self.buy_orders[0] + limit = self.buy_orders[0]['quote']['amount'] self.log.debug('Limiting sell order by opposite order quote: {}'.format(limit)) - self.place_lower_sell_order(lowest_sell_order, base_limit=limit, allow_partial=False) + self.place_lower_sell_order(lowest_sell_order, limit=limit, allow_partial=False) elif not self.buy_orders: # Do not try to do anything than placing lower sell whether there is no buy orders return From 4809d2f4712d8d32d75401416e47ada97ce3f4da Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 27 Aug 2018 12:01:47 +0500 Subject: [PATCH 0632/1846] Improve debug message --- 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 b230e0ff1..162902a4a 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -370,7 +370,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): # Place order limited by size of the opposite-side order lowest_sell_order = self.sell_orders[0] limit = lowest_sell_order['quote']['amount'] - self.log.debug('Limiting buy order base by opposite order: {}'.format(limit)) + self.log.debug('Limiting buy order base by opposite order base asset amount: {}'.format(limit)) self.place_higher_buy_order(highest_buy_order, base_limit=limit, allow_partial=False) elif not self.sell_orders: # Do not try to do anything than placing higher buy whether there is no sell orders From 5dbed783aa43b434dcfa8efdf366d3deda71e84c Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 27 Aug 2018 14:17:23 +0500 Subject: [PATCH 0633/1846] Keep orders when removing a worker --- dexbot/strategies/staggered_orders.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 162902a4a..1e1a1fa2a 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1003,6 +1003,12 @@ def pause(self): """ Override pause() in BaseStrategy """ pass + def purge(self): + """ We are not cancelling orders on save/remove worker from the GUI + TODO: don't work yet because worker removal is happening via Basestrategy staticmethod + """ + pass + def tick(self, d): """ Ticks come in on every block """ if not (self.counter or 0) % 3: From ca1e751552bb4b83bfe88bbac2df424f1b16c1fe Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 27 Aug 2018 22:30:25 +0500 Subject: [PATCH 0634/1846] Increase self.min_check_interval --- 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 1e1a1fa2a..8b7c6ce0f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -100,7 +100,7 @@ def __init__(self, *args, **kwargs): self.expiration = 60 * 60 * 24 * 365 * 5 self.last_check = datetime.now() # Minimal check interval is needed to prevent event queue accumulation - self.min_check_interval = 0.05 + self.min_check_interval = 1 if self.view: self.update_gui_slider() From bbcaf81f78980072f289a51c4d661c0893131149 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 27 Aug 2018 22:38:43 +0500 Subject: [PATCH 0635/1846] Update log messages --- dexbot/strategies/staggered_orders.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 8b7c6ce0f..67509b156 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -212,13 +212,15 @@ def maintain_strategy(self, *args, **kwargs): if self.market_center_price > highest_buy_price * (1 + self.target_spread): # Cancel lowest buy order because center price moved up. # On the next run there will be placed next buy order closer to the new center - self.log.debug('Cancelling lowest buy order in maintain_strategy') + self.log.info('No avail balances and we not in bootstrap mode and target spread is not reached. ' + 'Cancelling lowest buy order as a fallback.') self.cancel(self.buy_orders[-1]) else: if self.market_center_price < lowest_sell_price * (1 - self.target_spread): # Cancel highest sell order because center price moved down. # On the next run there will be placed next sell closer to the new center - self.log.debug('Cancelling highest sell order in maintain_strategy') + self.log.info('No avail balances and we not in bootstrap mode and target spread is not reached. ' + 'Cancelling highest sell order as a fallback.') self.cancel(self.sell_orders[-1]) self.last_check = datetime.now() @@ -277,14 +279,14 @@ def remove_outside_orders(self, sell_orders, buy_orders): for order in sell_orders: order_price = order['price'] ** -1 if order_price > self.upper_bound: - self.log.debug('Cancelling sell order outside range: {}'.format(order_price)) + self.log.info('Cancelling sell order outside range: {}'.format(order_price)) orders_to_cancel.append(order) # Remove buy orders that exceed boundaries for order in buy_orders: order_price = order['price'] if order_price < self.lower_bound: - self.log.debug('Cancelling buy order outside range: {}'.format(order_price)) + self.log.info('Cancelling buy order outside range: {}'.format(order_price)) orders_to_cancel.append(order) if orders_to_cancel: @@ -559,7 +561,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): # Limit sell order to available balance if asset_balance < new_order_amount - order_amount: new_order_amount = order_amount + asset_balance['amount'] - self.log.debug('Limiting new sell order to avail asset balance: {}'.format( + self.log.info('Limiting new sell order to avail asset balance: {}'.format( new_order_amount)) price = (order['price'] ** -1) @@ -636,7 +638,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): # Limit buy order to available balance if (asset_balance / price) < (new_base_amount - order_amount) / price: new_base_amount = order_amount + asset_balance['amount'] - self.log.debug('Limiting new buy order to avail asset balance: {}'.format( + self.log.info('Limiting new buy order to avail asset balance: {}'.format( new_base_amount)) new_order_amount = new_base_amount / price From 99fc84e68e5fe7be70bc1e47de301df61d3b6127 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 27 Aug 2018 22:47:13 +0500 Subject: [PATCH 0636/1846] Refuse to work whether spread < increment --- dexbot/strategies/staggered_orders.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 67509b156..f5cd89659 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -77,6 +77,10 @@ def __init__(self, *args, **kwargs): else: self.center_price = self.worker['center_price'] + if self.target_spread < self.increment: + self.log.error('Spread is more than increment, refusing to work because worker will make losses') + self.disabled = True + # Strategy variables # Assume we are in bootstrap mode by default. This prevents weird things when bootstrap was interrupted self.bootstrapping = True From 2d68c2626855be4f86e01cf36f5f48791c4afacf Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 27 Aug 2018 23:40:12 +0500 Subject: [PATCH 0637/1846] Improve logging of actual spread --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index f5cd89659..f04a15049 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -368,7 +368,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): 'opposite-side balance') self.bootstrapping = False # Place order closer to the center price - self.log.debug('Placing higher buy order; actual spread: {:.8f}, target + increment: {}'.format( + self.log.debug('Placing higher buy order; actual spread: {:.4%}, target + increment: {:.4%}'.format( self.actual_spread, self.target_spread + self.increment)) if self.bootstrapping: self.place_higher_buy_order(highest_buy_order) @@ -440,7 +440,7 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): 'opposite-side balance') self.bootstrapping = False # Place order closer to the center price - self.log.debug('Placing lower sell order; actual spread: {:.8f}, target + increment: {}'.format( + self.log.debug('Placing lower sell order; actual spread: {:.4%}, target + increment: {:.4%}'.format( self.actual_spread, self.target_spread + self.increment)) if self.bootstrapping: self.place_lower_sell_order(lowest_sell_order) From 2fe25880a038fe709939d102b750d3dcabfa30c1 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 28 Aug 2018 11:57:35 +0500 Subject: [PATCH 0638/1846] More proper fix for spamming log with "Oreders correct ..." As described by @mikakoi: The point of logging "Orders correct on market" is that it's used as a status for the worker in the GUI. In a case where the strategy is initialized but no action is required to be done after it, a status message "Initializing Relative Orders" will get stuck on the GUI and that will just confuse the user. Code part is from 347eedf4d3443d1a86c84cc9ffac89f10375e3e2 Closes: #270 --- dexbot/strategies/relative_orders.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 567d19cb1..41d4430d3 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -89,6 +89,7 @@ def __init__(self, *args, **kwargs): self.buy_price = None self.sell_price = None + self.initializing = True self.initial_balance = self['initial_balance'] or 0 self.worker_name = kwargs.get('name') @@ -257,9 +258,10 @@ def check_orders(self, event, *args, **kwargs): if need_update: self.update_orders() - else: - pass - #self.log.debug("Orders correct on market") + elif self.initializing: + self.log.info("Orders correct on market") + + self.initializing = False if self.view: self.update_gui_profit() From ba80c10efe0abb793ad374e7d2634d01f2cc765e Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 28 Aug 2018 12:04:42 +0500 Subject: [PATCH 0639/1846] Uncomment and enable debug message --- 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 41d4430d3..4c2959b27 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -158,7 +158,7 @@ def calculate_order_prices(self): self.sell_price = self.center_price * math.sqrt(1 + self.spread) def update_orders(self): - #self.log.debug('Change detected, updating orders') + self.log.debug('Starting to update orders') # Recalculate buy and sell order prices self.calculate_order_prices() From 275ce1bcc687030c4e812a6850b127d31c08e72d Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 28 Aug 2018 12:12:31 +0500 Subject: [PATCH 0640/1846] Fix option name in the log message --- 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 4c2959b27..6187a1041 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -97,7 +97,7 @@ def __init__(self, *args, **kwargs): # Check for conflicting settings if self.is_reset_on_price_change and not self.is_center_price_dynamic: - self.log.error('reset_on_price_change requires Dynamic Center Price') + self.log.error('"Reset orders on center price change" requires "Dynamic Center Price"') self.disabled = True self.update_orders() From 9f1da45f704c6f2737bc7bd98728b421f1251d63 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 28 Aug 2018 12:14:29 +0500 Subject: [PATCH 0641/1846] Do not let update_orders() when conflicting settings detected --- dexbot/strategies/relative_orders.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 6187a1041..3fa2a0e54 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -99,6 +99,7 @@ def __init__(self, *args, **kwargs): if self.is_reset_on_price_change and not self.is_center_price_dynamic: self.log.error('"Reset orders on center price change" requires "Dynamic Center Price"') self.disabled = True + return self.update_orders() def error(self, *args, **kwargs): From d1abe0d346b65593a961eb81ee024340cf87b6cd Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 28 Aug 2018 12:18:57 +0500 Subject: [PATCH 0642/1846] Call check_orders() on initialization instead of update_orders() There are maybe old orders from previous run, so give a chance to them. --- 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 3fa2a0e54..dce1dd2ef 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -100,7 +100,9 @@ def __init__(self, *args, **kwargs): self.log.error('"Reset orders on center price change" requires "Dynamic Center Price"') self.disabled = True return - self.update_orders() + + # Old orders from previous run may still be on market, so let's check them + self.check_orders('init') def error(self, *args, **kwargs): self.cancel_all() From c43c0c2e43a16c35852b646573dd0e6ad0bba65c Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 28 Aug 2018 12:25:04 +0500 Subject: [PATCH 0643/1846] Write a trade log entry only when we are not using custom expiration --- dexbot/strategies/relative_orders.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index dce1dd2ef..6eeee07d8 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -224,9 +224,10 @@ def check_orders(self, event, *args, **kwargs): if not current_order: need_update = True self.log.debug('Could not found order on the market, it was filled, expired or cancelled') - # FIXME: writing a log entry is disabled because we cannot distinguish an expired order - # from filled - #self.write_order_log(self.worker_name, order) + # Write a trade log entry only when we are not using custom expiration because we cannot + # distinguish an expired order from filled + if not self.is_custom_expiration: + self.write_order_log(self.worker_name, order) elif self.is_reset_on_partial_fill: # Detect partially filled orders; # on fresh order 'for_sale' is always equal to ['base']['amount'] From 4e527dc44cac5ee85630c3f121e8018c557acfe6 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 28 Aug 2018 10:32:04 +0300 Subject: [PATCH 0644/1846] Add relative orders UI file --- .../views/ui/forms/relative_orders_widget.ui | 866 ++++++++++++++++++ 1 file changed, 866 insertions(+) create mode 100644 dexbot/views/ui/forms/relative_orders_widget.ui 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..a8345a88f --- /dev/null +++ b/dexbot/views/ui/forms/relative_orders_widget.ui @@ -0,0 +1,866 @@ + + + Form + + + + 0 + 0 + 439 + 291 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Worker Parameters + + + + 6 + + + 9 + + + 9 + + + 9 + + + 9 + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Order size + + + amount_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Fixed order size, expressed in quote asset, unless "relative order size" selected + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 8 + + + 1000000000.000000000000000 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Amount is expressed as a percentage of the account balance of quote/base asset + + + ? + + + 5 + + + + + + + + + + Relative order size + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Center Price + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Fixed center price expressed in base asset: base/quote + + + ? + + + 5 + + + + + + + + + + false + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + false + + + false + + + 8 + + + 0.000000000000000 + + + 999999999.998999953269958 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Always calculate the middle from the closest market orders + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + Update center price from closest market orders + + + true + + + false + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Automatically adjust orders up or down based on the imbalance of your assets + + + ? + + + 5 + + + + + + + + + + Center price offset based on asset balances + + + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + QAbstractSpinBox::UpDownArrows + + + false + + + % + + + -50.000000000000000 + + + 100.000000000000000 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + + + + Manual center price offset + + + true + + + manual_offset_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Manually adjust orders up or down. Works independently of other offsets and doesn't override them + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Spread + + + spread_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + The percentage difference between buy and sell + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + % + + + 100000.000000000000000 + + + 5.000000000000000 + + + + + + + + + + + + center_price_dynamic_input + clicked(bool) + center_price_input + setDisabled(bool) + + + 284 + 129 + + + 208 + 99 + + + + + From 7caf08b3e7aac514cb51c8ca549af16af2ebfdee Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 28 Aug 2018 12:43:47 +0500 Subject: [PATCH 0645/1846] Update the log message --- 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 6eeee07d8..16a6fd767 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -114,7 +114,7 @@ def tick(self, d): """ if (self.is_reset_on_price_change and not self.counter % 8): - self.log.debug('checking orders by tick threshold') + self.log.debug('Checking orders by tick threshold') self.check_orders('tick') self.counter += 1 From 28e8dc831b41c4c543f153f26634b0104c5b3a59 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 28 Aug 2018 12:46:37 +0500 Subject: [PATCH 0646/1846] Reset orders on startup if is_reset_on_price_change active We still need to cancel old orders on startup when "Reset orders on center price change" is active. We don't know what the center price was before. --- dexbot/strategies/relative_orders.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 16a6fd767..340bd74f4 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -101,8 +101,13 @@ def __init__(self, *args, **kwargs): self.disabled = True return - # Old orders from previous run may still be on market, so let's check them - self.check_orders('init') + # Check old orders from previous run (from force-interruption) only whether we are not using "Reset orders on + # center price change" option + if self.is_reset_on_price_change: + self.log.info('"Reset orders on center price change" is active, placing fresh orders') + self.update_orders() + else: + self.check_orders('init') def error(self, *args, **kwargs): self.cancel_all() From 2282da77b8615bb61ea26dfd8453e59c352e57b4 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 28 Aug 2018 14:01:45 +0500 Subject: [PATCH 0647/1846] Add modes description by Marko --- dexbot/strategies/staggered_orders.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index f04a15049..29c9e6d8e 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -11,6 +11,25 @@ class Strategy(BaseStrategy): @classmethod def configure(cls, return_base_config=True): + """ Modes description: + + Mountain: + - Buy orders same QUOTE + - Sell orders same BASE + + Neutral: + - All orders lower_order_quote / sqrt(1 + increment) + + Valley: + - Buy orders same BASE + - Sell orders same QUOTE + + Buy slope: + - All orders same BASE (profit comes in QUOTE) + + Sell slope: + - All orders same QUOTE (profit made in BASE) + """ # Todo: - Add other modes modes = [ ('mountain', 'Mountain'), From e93946093459cf5e7a8c5c003dffa1b9ae01bfce Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 28 Aug 2018 12:12:07 +0300 Subject: [PATCH 0648/1846] Modify relative orders widget --- .../views/ui/forms/relative_orders_widget.ui | 306 ++++++++++-------- 1 file changed, 176 insertions(+), 130 deletions(-) diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index a8345a88f..f5902d17c 100644 --- a/dexbot/views/ui/forms/relative_orders_widget.ui +++ b/dexbot/views/ui/forms/relative_orders_widget.ui @@ -6,7 +6,7 @@ 0 0 - 439 + 449 291 @@ -257,8 +257,8 @@ - - + + 0 @@ -277,7 +277,7 @@ 16777215 - + 0 @@ -294,7 +294,7 @@ 0 - + 0 @@ -314,15 +314,15 @@ - Center Price + Spread - center_price_input + spread_input - + 0 @@ -339,7 +339,7 @@ WhatsThisCursor - Fixed center price expressed in base asset: base/quote + The percentage difference between buy and sell ? @@ -352,11 +352,8 @@ - - - - false - + + 0 @@ -375,21 +372,110 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - false + + % - - false + + 100000.000000000000000 - - 8 + + 5.000000000000000 - - 0.000000000000000 + + + + + + + 0 + 0 + - - 999999999.998999953269958 + + + 120 + 0 + + + + + 120 + 16777215 + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Center Price + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Fixed center price expressed in base asset: base/quote + + + ? + + + 5 + + + + @@ -578,43 +664,6 @@ - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - QAbstractSpinBox::UpDownArrows - - - false - - - % - - - -50.000000000000000 - - - 100.000000000000000 - - - @@ -713,29 +762,60 @@ - - + + - + 0 0 - 120 + 170 0 - + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + QAbstractSpinBox::UpDownArrows + + + false + + + % + + + -50.000000000000000 + + + 100.000000000000000 + + + + + + + + 0 + 0 + + + 120 - 16777215 + 0 - + - 0 + 9 0 @@ -750,95 +830,61 @@ 0 - + + + false + - + 0 0 - 0 + 170 0 - - - 16777215 - 16777215 - + + - - Spread + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - spread_input + + false + + + false + + + 8 + + + 0.000000000000000 + + + 999999999.998999953269958 - + - + 0 0 - - - 75 - true - - - - WhatsThisCursor - - - The percentage difference between buy and sell - - ? - - - 5 + - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - % - - - 100000.000000000000000 - - - 5.000000000000000 - - - From 0f6ec8f335da05fc892af768f681b024364073ba Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 28 Aug 2018 13:52:10 +0300 Subject: [PATCH 0649/1846] Remove unused variable self.market_spread --- dexbot/strategies/staggered_orders.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 29c9e6d8e..dbd417fe8 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -108,7 +108,6 @@ def __init__(self, *args, **kwargs): self.buy_orders = [] self.sell_orders = [] self.actual_spread = self.target_spread + 1 - # self.market_spread = 0 self.base_fee_reserve = None self.quote_fee_reserve = None self.quote_total_balance = 0 @@ -143,7 +142,6 @@ def maintain_strategy(self, *args, **kwargs): # Get all user's orders on current market self.refresh_orders() - # market_orders = self.market.orderbook(1) # Check if market center price is calculated if not self.bootstrapping: @@ -169,16 +167,6 @@ def maintain_strategy(self, *args, **kwargs): # Invert the sell price to BASE so it can be used in comparison lowest_sell_price = lowest_sell_price ** -1 - # Todo: Market spread is calculated but never used, can this be removed? - # Calculate market spread - # if there are no orders in both side spread cannot be calculated - # if len(market_orders['bids']) and len(market_orders['asks']): - # highest_market_buy = market_orders['bids'][0]['price'] - # lowest_market_sell = market_orders['asks'][0]['price'] - # - # if highest_market_buy and lowest_market_sell: - # self.market_spread = lowest_market_sell / highest_market_buy - 1 - # Calculate balances self.refresh_balances() From b69c0e5df591bf9bfead2e69bc00c442ca5510ca Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 28 Aug 2018 13:53:20 +0300 Subject: [PATCH 0650/1846] Refactor some typos in comments --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index dbd417fe8..6d579975b 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -363,9 +363,9 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): we cannot just place another big order on the big side. So we are limiting the big-side order to amount of a low-side one! - Normally we are turning bootstrap off after initial allocation is done and we're biginning + Normally we are turning bootstrap off after initial allocation is done and we're beginning to distribute remaining funds. But, whether we will restart the bot after size increase was - done, we have no chance to know if bootsrap was done or not. This is where this check comes + done, we have no chance to know if bootstrap was done or not. This is where this check comes in! The situation when the target spread is not reached, but we have some available balance on the one side and not have any free balance of the other side, clearly says to us that an order from lower-side was filled! Thus, we can safely turn bootstrap off and thus place an From 0736972dad59a401c410290b452d9a62071f4ab0 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 28 Aug 2018 13:54:09 +0300 Subject: [PATCH 0651/1846] Remove is_order_size_correct() --- dexbot/strategies/staggered_orders.py | 100 -------------------------- 1 file changed, 100 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 6d579975b..cfa57b6e8 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -686,106 +686,6 @@ def check_partial_fill(self, order): return False return True - def is_order_size_correct(self, order, orders): - """ Checks if the order is big enough. Oversized orders are allowed to enable manual manipulation - - This is old version of check_partial_fill() - - :param order: Order closest to the center price from buy or sell side - :param orders: List of buy or sell orders - :return: bool | True = Order is correct size or within the threshold - False = Order is not right size - """ - # Calculate threshold - order_size = order['quote']['amount'] - if self.is_sell_order(order): - order_size = order['base']['amount'] - - threshold = self.increment / 10 - upper_threshold = order_size * (1 + threshold) - lower_threshold = order_size / (1 + threshold) - - if self.is_sell_order(order): - lowest_sell_order = orders[0] - highest_sell_order = orders[-1] - - # Order is the only sell order, and size must be calculated like initializing - if lowest_sell_order == highest_sell_order: - total_balance = self.total_balance(orders, return_asset=True) - quote_balance = total_balance['quote'] - self.quote_fee_reserve - highest_sell_order = self.place_highest_sell_order(quote_balance, - place_order=False, - market_center_price=self.initial_market_center_price) - - # Check if the old order is same size with accuracy of 0.1% - if lower_threshold <= highest_sell_order['amount'] <= upper_threshold: - return True - - self.log.debug('lower_threshold <= highest_sell_order <= upper_threshold: {} <= {} <= {}'.format( - lower_threshold, highest_sell_order['amount'], upper_threshold)) - return False - elif order == highest_sell_order: - order_index = orders.index(order) - higher_sell_order = self.place_higher_sell_order(orders[order_index - 1], place_order=False) - - if lower_threshold <= higher_sell_order['amount'] <= upper_threshold: - return True - - self.log.debug('lower_threshold <= higher_sell_order <= upper_threshold: {} <= {} <= {}'.format( - lower_threshold, higher_sell_order['amount'], upper_threshold)) - return False - elif order == lowest_sell_order: - order_index = orders.index(order) - lower_sell_order = self.place_lower_sell_order(orders[order_index + 1], place_order=False) - - if lower_threshold <= lower_sell_order['amount'] <= upper_threshold: - return True - - self.log.debug('lower_threshold <= lower_sell_order <= upper_threshold: {} <= {} <= {}'.format( - lower_threshold, lower_sell_order['amount'], upper_threshold)) - return False - elif self.is_buy_order(order): - lowest_buy_order = orders[-1] - highest_buy_order = orders[0] - - # Order is the only buy order, and size must be calculated like initializing - if highest_buy_order == lowest_buy_order: - total_balance = self.total_balance(orders, return_asset=True) - base_balance = total_balance['base'] - self.base_fee_reserve - lowest_buy_order = self.place_lowest_buy_order(base_balance, - place_order=False, - market_center_price=self.initial_market_center_price) - - # Check if the old order is same size with accuracy of 0.1% - if lower_threshold <= lowest_buy_order['amount'] <= upper_threshold: - return True - - self.log.debug('lower_threshold <= lowest_buy_order <= upper_threshold: {} <= {} <= {}'.format( - lower_threshold, lowest_buy_order['amount'], upper_threshold)) - return False - elif order == lowest_buy_order: - order_index = orders.index(order) - lower_buy_order = self.place_lower_buy_order(orders[order_index - 1], place_order=False) - - if lower_threshold <= lower_buy_order['amount'] <= upper_threshold: - return True - - self.log.debug('lower_threshold <= lower_buy_order <= upper_threshold: {} <= {} <= {}'.format( - lower_threshold, lower_buy_order['amount'], upper_threshold)) - return False - elif order == highest_buy_order: - order_index = orders.index(order) - higher_buy_order = self.place_higher_buy_order(orders[order_index + 1], place_order=False) - - if lower_threshold <= higher_buy_order['amount'] <= upper_threshold: - return True - - self.log.debug('lower_threshold <= higher_buy_order <= upper_threshold: {} <= {} <= {}'.format( - lower_threshold, higher_buy_order['amount'], upper_threshold)) - return False - - return False - def place_higher_buy_order(self, order, place_order=True, allow_partial=False, base_limit=None, limit=None): """ Place higher buy order Mode: MOUNTAIN From cd7892d74672cb5218bdcf49769d679b0143f469 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 28 Aug 2018 13:54:35 +0300 Subject: [PATCH 0652/1846] Remove maintain_mountain_mode() --- dexbot/strategies/staggered_orders.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index cfa57b6e8..9e25d7e9f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -314,13 +314,6 @@ def remove_outside_orders(self, sell_orders, buy_orders): return True - def maintain_mountain_mode(self): - """ Mountain mode - This structure is not final, but an idea was that each mode has separate function which runs the loop. - """ - # Todo: Work in progress - pass - def allocate_base_asset(self, base_balance, *args, **kwargs): """ Allocates available base asset as buy orders. :param base_balance: Amount of the base asset available to use From d4dd95c6ce0a253b4f95d9caa00961fed67a5525 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 28 Aug 2018 13:55:13 +0300 Subject: [PATCH 0653/1846] Refactor comments and remove WIP comments --- dexbot/strategies/staggered_orders.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 9e25d7e9f..3ee9fc985 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -91,6 +91,7 @@ def __init__(self, *args, **kwargs): self.partial_fill_threshold = self.increment / 10 self.is_instant_fill_enabled = self.worker.get('instant_fill', True) self.is_center_price_dynamic = self.worker['center_price_dynamic'] + if self.is_center_price_dynamic: self.center_price = None else: @@ -120,7 +121,9 @@ def __init__(self, *args, **kwargs): # Order expiration time self.expiration = 60 * 60 * 24 * 365 * 5 + self.start = datetime.now() self.last_check = datetime.now() + # Minimal check interval is needed to prevent event queue accumulation self.min_check_interval = 1 @@ -224,14 +227,14 @@ def maintain_strategy(self, *args, **kwargs): # Cancel lowest buy order because center price moved up. # On the next run there will be placed next buy order closer to the new center self.log.info('No avail balances and we not in bootstrap mode and target spread is not reached. ' - 'Cancelling lowest buy order as a fallback.') + 'Cancelling lowest buy order as a fallback.') self.cancel(self.buy_orders[-1]) else: if self.market_center_price < lowest_sell_price * (1 - self.target_spread): # Cancel highest sell order because center price moved down. # On the next run there will be placed next sell closer to the new center self.log.info('No avail balances and we not in bootstrap mode and target spread is not reached. ' - 'Cancelling highest sell order as a fallback.') + 'Cancelling highest sell order as a fallback.') self.cancel(self.sell_orders[-1]) self.last_check = datetime.now() @@ -482,9 +485,7 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): # Get latest orders self.refresh_orders() - # Todo: Check completely def increase_order_sizes(self, asset, asset_balance, orders): - # Todo: Change asset or separate buy / sell in different functions? """ Checks which order should be increased in size and replaces it with a maximum size order, according to global limits. Logic depends on mode in question @@ -496,7 +497,6 @@ def increase_order_sizes(self, asset, asset_balance, orders): """ # Mountain mode: if self.mode == 'mountain': - # Todo: Work in progress. if asset == 'quote': """ Starting from the lowest SELL order. For each order, see if it is approximately maximum size. @@ -577,7 +577,6 @@ def increase_order_sizes(self, asset, asset_balance, orders): # simultaneously return elif asset == 'base': - # Todo: Work in progress """ Starting from the highest BUY order, for each order, see if it is approximately maximum size. If it is, move on to next. @@ -650,8 +649,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): 'mode: mountain, base: {}, price: {:.8f}'.format(order_amount, order['price'])) self.cancel(order) self.market_buy(new_order_amount, price) - # Only one increase at a time. This prevents running more than one increaement round - # simultaneously + # One increase at a time. This prevents running more than one increment round simultaneously. return elif self.mode == 'valley': pass @@ -911,7 +909,7 @@ def pause(self): def purge(self): """ We are not cancelling orders on save/remove worker from the GUI - TODO: don't work yet because worker removal is happening via Basestrategy staticmethod + TODO: don't work yet because worker removal is happening via BaseStrategy staticmethod """ pass From 767dd9d57723698209aeffd360bf3d6f63e43918 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 28 Aug 2018 20:32:59 +0500 Subject: [PATCH 0654/1846] Fix imports styling --- dexbot/strategies/relative_orders.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 340bd74f4..0e8e7c22e 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -1,6 +1,5 @@ import math -from datetime import datetime -from datetime import timedelta +from datetime import datetime, timedelta from dexbot.basestrategy import BaseStrategy, ConfigElement from dexbot.qt_queue.idle_queue import idle_add From b44716169fcc04a5c83bae8cbcfacda7b07d6b80 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 28 Aug 2018 21:04:55 +0500 Subject: [PATCH 0655/1846] Update "Fill threshold" option description --- 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 0e8e7c22e..b0fba9eeb 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -32,7 +32,7 @@ def configure(cls, return_base_config=True): ConfigElement('reset_on_partial_fill', 'bool', True, 'Reset orders on partial fill', 'Reset orders when buy or sell order is partially filled', None), ConfigElement('partial_fill_threshold', 'float', 30, 'Fill threshold', - 'Fill threshold to reset orders', (0, 100, 2, '%')), + 'Order fill threshold to reset orders', (0, 100, 2, '%')), ConfigElement('reset_on_market_trade', 'bool', False, 'Reset orders on market trade', 'Reset orders when detected a market trade', None), ConfigElement('reset_on_price_change', 'bool', False, 'Reset orders on center price change', From db74ed7c1951801add1ed1278526a04035d740ff Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 28 Aug 2018 22:26:42 +0500 Subject: [PATCH 0656/1846] Remove reset_on_market_trade option This is option is actually not needed because it's duplicating behavior of "Reset on center price change" with "threshold = 0" --- dexbot/strategies/relative_orders.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index b0fba9eeb..0030e36e5 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -33,8 +33,6 @@ def configure(cls, return_base_config=True): 'Reset orders when buy or sell order is partially filled', None), ConfigElement('partial_fill_threshold', 'float', 30, 'Fill threshold', 'Order fill threshold to reset orders', (0, 100, 2, '%')), - ConfigElement('reset_on_market_trade', 'bool', False, 'Reset orders on market trade', - 'Reset orders when detected a market trade', None), ConfigElement('reset_on_price_change', 'bool', False, 'Reset orders on center price change', 'Reset orders when center price is changed more than threshold', None), ConfigElement('price_change_threshold', 'float', 2, 'Price change threshold', @@ -75,7 +73,6 @@ def __init__(self, *args, **kwargs): self.spread = self.worker.get('spread') / 100 self.is_reset_on_partial_fill = self.worker.get('reset_on_partial_fill', True) self.partial_fill_threshold = self.worker.get('partial_fill_threshold', 30) / 100 - self.is_reset_on_market_trade = self.worker.get('reset_on_market_trade', False) self.is_reset_on_price_change = self.worker.get('reset_on_price_change', False) self.price_change_threshold = self.worker.get('price_change_threshold', 2) / 100 self.is_custom_expiration = self.worker.get('custom_expiration', False) @@ -106,7 +103,7 @@ def __init__(self, *args, **kwargs): self.log.info('"Reset orders on center price change" is active, placing fresh orders') self.update_orders() else: - self.check_orders('init') + self.check_orders() def error(self, *args, **kwargs): self.cancel_all() @@ -119,7 +116,7 @@ def tick(self, d): if (self.is_reset_on_price_change and not self.counter % 8): self.log.debug('Checking orders by tick threshold') - self.check_orders('tick') + self.check_orders() self.counter += 1 @property @@ -204,7 +201,7 @@ def update_orders(self): if len(order_ids) < expected_num_orders and not self.disabled: self.update_orders() - def check_orders(self, event, *args, **kwargs): + def check_orders(self, *args, **kwargs): """ Tests if the orders need updating """ delta = datetime.now() - self.last_check @@ -245,11 +242,6 @@ def check_orders(self, event, *args, **kwargs): # we're updating order it may be filled futher so trade log entry will not # be correct - if (self.is_reset_on_market_trade and - isinstance(event, FilledOrder)): - self.log.debug('Market trade detected, updating orders') - need_update = True - if self.is_reset_on_price_change: center_price = self.calculate_center_price( None, From 901e8809e4a90b9e7686ed083d439570c9588de2 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 28 Aug 2018 22:59:30 +0500 Subject: [PATCH 0657/1846] Do not ignore market update events while initializing check_orders() are called from __init__(), so it will not work because initial "self.last_check = now" --- 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 0030e36e5..e9e475702 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -207,7 +207,7 @@ def check_orders(self, *args, **kwargs): delta = datetime.now() - self.last_check # Only allow to check orders whether minimal time passed - if delta < timedelta(seconds=self.min_check_interval): + if delta < timedelta(seconds=self.min_check_interval) and not self.initializing: self.log.debug('Ignoring market_update event as min_check_interval is not passed') return From 7c1bbcc0e7fdf03d3528485bf9686b9dfd071715 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 29 Aug 2018 08:31:00 +0300 Subject: [PATCH 0658/1846] Change base asset field to blank field --- dexbot/controllers/strategy_controller.py | 4 +- dexbot/controllers/worker_controller.py | 6 +-- dexbot/views/create_worker.py | 1 - dexbot/views/edit_worker.py | 3 +- dexbot/views/ui/create_worker_window.ui | 44 ++++++++++--------- dexbot/views/ui/edit_worker_window.ui | 43 +++++++++--------- .../views/ui/forms/staggered_orders_widget.ui | 2 +- 7 files changed, 53 insertions(+), 50 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index a02d024cc..099ce349d 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -139,7 +139,7 @@ def __init__(self, view, configure, worker_controller, worker_data): self.configure = configure self.worker_controller = worker_controller - worker_controller.view.base_asset_input.editTextChanged.connect(lambda: self.on_value_change()) + worker_controller.view.base_asset_input.textChanged.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()) @@ -159,7 +159,7 @@ def __init__(self, view, configure, worker_controller, worker_data): @gui_error def on_value_change(self): - base_asset = self.worker_controller.view.base_asset_input.currentText() + base_asset = self.worker_controller.view.base_asset_input.text() quote_asset = self.worker_controller.view.quote_asset_input.text() try: market = Market('{}:{}'.format(quote_asset, base_asset)) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index 0ed89315c..be9af5b1c 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -28,7 +28,7 @@ def strategies(self): strategies = collections.OrderedDict() strategies['dexbot.strategies.relative_orders'] = { 'name': 'Relative Orders', - 'form_module': '' + 'form_module': 'dexbot.views.ui.forms.relative_orders_widget_ui' } strategies['dexbot.strategies.staggered_orders'] = { 'name': 'Staggered Orders', @@ -184,7 +184,7 @@ def validate_account_not_in_use(cls, account): @gui_error def validate_form(self): error_texts = [] - base_asset = self.view.base_asset_input.currentText() + base_asset = self.view.base_asset_input.text() quote_asset = self.view.quote_asset_input.text() fee_asset = self.view.fee_asset_input.text() worker_name = self.view.worker_name_input.text() @@ -238,7 +238,7 @@ def handle_save(self): else: # Edit account = self.view.account_name.text() - base_asset = self.view.base_asset_input.currentText() + base_asset = self.view.base_asset_input.text() quote_asset = self.view.quote_asset_input.text() fee_asset = self.view.fee_asset_input.text() strategy_module = self.view.strategy_input.currentData() diff --git a/dexbot/views/create_worker.py b/dexbot/views/create_worker.py index a42a99dcf..174b13b84 100644 --- a/dexbot/views/create_worker.py +++ b/dexbot/views/create_worker.py @@ -19,7 +19,6 @@ def __init__(self, bitshares_instance): 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() diff --git a/dexbot/views/edit_worker.py b/dexbot/views/edit_worker.py index 47d75b9ef..e7f3ebe15 100644 --- a/dexbot/views/edit_worker.py +++ b/dexbot/views/edit_worker.py @@ -27,8 +27,7 @@ def __init__(self, parent_widget, bitshares_instance, worker_name, 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.base_asset_input.setText(self.controller.get_base_asset(worker_data)) self.quote_asset_input.setText(self.controller.get_quote_asset(worker_data)) self.fee_asset_input.setText(worker_data.get('fee_asset', 'BTS')) self.account_name.setText(self.controller.get_account(worker_data)) diff --git a/dexbot/views/ui/create_worker_window.ui b/dexbot/views/ui/create_worker_window.ui index 30b92a565..0dfbfdf73 100644 --- a/dexbot/views/ui/create_worker_window.ui +++ b/dexbot/views/ui/create_worker_window.ui @@ -7,7 +7,7 @@ 0 0 428 - 378 + 388 @@ -323,25 +323,6 @@ - - - - - 0 - 0 - - - - - 170 - 0 - - - - true - - - @@ -631,6 +612,28 @@ + + + + + 0 + 0 + + + + + 145 + 0 + + + + + 80 + 16777215 + + + + @@ -657,7 +660,6 @@ strategy_input worker_name_input - base_asset_input quote_asset_input fee_asset_input account_input diff --git a/dexbot/views/ui/edit_worker_window.ui b/dexbot/views/ui/edit_worker_window.ui index 3767b532c..af790dd41 100644 --- a/dexbot/views/ui/edit_worker_window.ui +++ b/dexbot/views/ui/edit_worker_window.ui @@ -7,7 +7,7 @@ 0 0 428 - 347 + 351 @@ -74,25 +74,6 @@ - - - - - 0 - 0 - - - - - 170 - 0 - - - - true - - - @@ -398,6 +379,28 @@ + + + + + 0 + 0 + + + + + 145 + 0 + + + + + 145 + 16777215 + + + + diff --git a/dexbot/views/ui/forms/staggered_orders_widget.ui b/dexbot/views/ui/forms/staggered_orders_widget.ui index 102e15826..87c308902 100644 --- a/dexbot/views/ui/forms/staggered_orders_widget.ui +++ b/dexbot/views/ui/forms/staggered_orders_widget.ui @@ -6,7 +6,7 @@ 0 0 - 439 + 449 415 From 39666b3aa39e1de29875dd8601f58ba470d81a8b Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 29 Aug 2018 08:32:06 +0300 Subject: [PATCH 0659/1846] Change relative orders to have asset label --- dexbot/controllers/strategy_controller.py | 29 ++++ .../views/ui/forms/relative_orders_widget.ui | 142 ++++++++++++------ 2 files changed, 129 insertions(+), 42 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 099ce349d..df8f3199d 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -87,6 +87,14 @@ def __init__(self, view, configure, worker_controller, worker_data): self.view = view self.configure = configure self.worker_controller = worker_controller + + # Refresh center price market label + self.onchange_asset_labels() + + # Refresh center price market label every time the text changes is base or quote asset input fields + worker_controller.view.base_asset_input.textChanged.connect(self.onchange_asset_labels) + worker_controller.view.quote_asset_input.textChanged.connect(self.onchange_asset_labels) + self.view.strategy_widget.relative_order_size_input.toggled.connect( self.onchange_relative_order_size_input ) @@ -112,6 +120,21 @@ def onchange_center_price_dynamic_input(self, checked): else: self.view.strategy_widget.center_price_input.setDisabled(False) + def onchange_asset_labels(self): + base_symbol = self.worker_controller.view.base_asset_input.text() + quote_symbol = self.worker_controller.view.quote_asset_input.text() + + if quote_symbol: + self.set_amount_asset_label(quote_symbol) + else: + self.set_amount_asset_label('') + + if base_symbol and quote_symbol: + text = '{} / {}'.format(base_symbol, quote_symbol) + self.set_center_price_market_label(text) + else: + self.set_center_price_market_label('') + def order_size_input_to_relative(self): self.view.strategy_widget.amount_input.setSuffix('%') self.view.strategy_widget.amount_input.setDecimals(2) @@ -123,6 +146,12 @@ def order_size_input_to_static(self): self.view.strategy_widget.amount_input.setDecimals(8) self.view.strategy_widget.amount_input.setMaximum(1000000000.000000) + def set_center_price_market_label(self, text): + self.view.strategy_widget.center_price_market_label.setText(text) + + def set_amount_asset_label(self, text): + self.view.strategy_widget.amount_input_asset_label.setText(text) + def validation_errors(self): error_texts = [] if not self.view.strategy_widget.amount_input.value(): diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index f5902d17c..c0d10ff6e 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 449 - 291 + 297 @@ -142,35 +142,7 @@ - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 8 - - - 1000000000.000000000000000 - - - - + @@ -250,14 +222,14 @@ - + Relative order size - + @@ -352,7 +324,7 @@ - + @@ -383,7 +355,7 @@ - + @@ -478,7 +450,7 @@ - + @@ -558,7 +530,7 @@ - + @@ -577,7 +549,7 @@ - + @@ -657,14 +629,14 @@ - + Center price offset based on asset balances - + @@ -762,7 +734,7 @@ - + @@ -799,7 +771,7 @@ - + @@ -872,11 +844,97 @@ - + + 0 + 0 + + + + + 120 + 0 + + + + + + + + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 9 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 8 + + + 1000000000.000000000000000 + + + + + + + 0 0 + + + 120 + 0 + + From 7ffc1cebf53367cd308460c6e094fd8adb2c3bd8 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 29 Aug 2018 13:54:29 +0300 Subject: [PATCH 0660/1846] Add QSlider to automated UI generation --- dexbot/controllers/strategy_controller.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index df8f3199d..01e82b556 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -52,7 +52,7 @@ def values(self): data = {} for key, element in self.elements.items(): class_name = element.__class__.__name__ - if class_name in ('QDoubleSpinBox', 'QSpinBox', 'QLineEdit'): + if class_name in ('QDoubleSpinBox', 'QSpinBox', 'QLineEdit', 'QSlider'): data[key] = element.value() elif class_name == 'QCheckBox': data[key] = element.isChecked() @@ -70,7 +70,8 @@ def elements(self): QtWidgets.QSpinBox, QtWidgets.QLineEdit, QtWidgets.QCheckBox, - QtWidgets.QComboBox + QtWidgets.QComboBox, + QtWidgets.QSlider ) for option in self.configure: From ce6dc472420d03dcc6f639f30818cca3e20ca650 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 29 Aug 2018 13:55:01 +0300 Subject: [PATCH 0661/1846] Small changes to relative orders logic --- dexbot/controllers/strategy_controller.py | 35 +++++++++++------------ dexbot/strategies/relative_orders.py | 13 ++++----- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index b8f2a3068..db1d22ab2 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -84,39 +84,36 @@ def elements(self): class RelativeOrdersController(StrategyController): def __init__(self, view, configure, worker_controller, worker_data): + super().__init__(view, configure, worker_controller, worker_data) + self.view = view self.configure = configure self.worker_controller = worker_controller - self.view.strategy_widget.relative_order_size_input.toggled.connect( + widget = self.view.strategy_widget + + # Event connecting + widget.relative_order_size_input.clicked.connect( self.onchange_relative_order_size_input ) - self.view.strategy_widget.center_price_dynamic_input.toggled.connect( + widget.center_price_dynamic_input.clicked.connect( self.onchange_center_price_dynamic_input ) - self.view.strategy_widget.reset_on_partial_fill_input.toggled.connect( + widget.reset_on_partial_fill_input.clicked.connect( self.onchange_reset_on_partial_fill_input ) - self.view.strategy_widget.reset_on_price_change_input.toggled.connect( + widget.reset_on_price_change_input.clicked.connect( self.onchange_reset_on_price_change_input ) - self.view.strategy_widget.custom_expiration_input.toggled.connect( + widget.custom_expiration_input.clicked.connect( self.onchange_custom_expiration_input ) - # Do this after the event connecting - super().__init__(view, configure, worker_controller, worker_data) - - if not self.view.strategy_widget.center_price_dynamic_input.isChecked(): - self.view.strategy_widget.center_price_input.setDisabled(False) - - if not self.view.strategy_widget.reset_on_partial_fill_input.isChecked(): - self.view.strategy_widget.partial_fill_threshold_input.setDisabled(True) - - if not self.view.strategy_widget.reset_on_price_change_input.isChecked(): - self.view.strategy_widget.price_change_threshold_input.setDisabled(True) - - if not self.view.strategy_widget.custom_expiration_input.isChecked(): - self.view.strategy_widget.expiration_time_input.setDisabled(True) + # Trigger the onchange events once + self.onchange_relative_order_size_input(widget.relative_order_size_input.isChecked()) + self.onchange_center_price_dynamic_input(widget.center_price_dynamic_input.isChecked()) + self.onchange_reset_on_partial_fill_input(widget.reset_on_partial_fill_input.isChecked()) + self.onchange_reset_on_price_change_input(widget.reset_on_price_change_input.isChecked()) + self.onchange_custom_expiration_input(widget.custom_expiration_input.isChecked()) def onchange_relative_order_size_input(self, checked): if checked: diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index e9e475702..400b6d962 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -4,7 +4,6 @@ from dexbot.basestrategy import BaseStrategy, ConfigElement from dexbot.qt_queue.idle_queue import idle_add -from bitshares.price import FilledOrder class Strategy(BaseStrategy): """ Relative Orders strategy @@ -114,7 +113,7 @@ def tick(self, d): do not triggers a market_update event """ if (self.is_reset_on_price_change and not - self.counter % 8): + self.counter % 8): self.log.debug('Checking orders by tick threshold') self.check_orders() self.counter += 1 @@ -238,11 +237,11 @@ def check_orders(self, *args, **kwargs): if diff_rel >= self.partial_fill_threshold: need_update = True self.log.info('Partially filled order detected, filled {:.2%}'.format(diff_rel)) - # FIXME: need to write trade operation; possible race condition may occur: while - # we're updating order it may be filled futher so trade log entry will not + # FIXME: Need to write trade operation; possible race condition may occur: while + # we're updating order it may be filled further so trade log entry will not # be correct - if self.is_reset_on_price_change: + if self.is_reset_on_price_change and not self.is_center_price_dynamic: center_price = self.calculate_center_price( None, self.is_asset_offset, @@ -250,8 +249,7 @@ def check_orders(self, *args, **kwargs): self['order_ids'], self.manual_offset ) - diff = (self.center_price - center_price) / self.center_price - diff = abs(diff) + diff = abs((self.center_price - center_price) / self.center_price) if diff >= self.price_change_threshold: self.log.debug('Center price changed, updating orders. Diff: {:.2%}'.format(diff)) need_update = True @@ -264,7 +262,6 @@ def check_orders(self, *args, **kwargs): self.initializing = False if self.view: - self.update_gui_profit() self.update_gui_slider() self.last_check = datetime.now() From 1edce5cf2ce49fd64c939509507762dd1dc3c0e9 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 29 Aug 2018 13:55:02 +0300 Subject: [PATCH 0662/1846] Remove base_assets property --- dexbot/controllers/worker_controller.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index be9af5b1c..3ea429628 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -45,13 +45,6 @@ def get_strategies(cls): """ return cls(None, None, None).strategies - @property - def base_assets(self): - assets = [ - 'USD', 'OPEN.BTC', 'CNY', 'BTS', 'BTC' - ] - return assets - def add_private_key(self, private_key): wallet = self.bitshares.wallet try: From 3d73fd462f21200d20cc7d42b3197e57c3805ab8 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 29 Aug 2018 13:56:53 +0300 Subject: [PATCH 0663/1846] Change manual offset input to slider --- dexbot/controllers/strategy_controller.py | 20 + .../views/ui/forms/relative_orders_widget.ui | 449 +++++++++++------- 2 files changed, 292 insertions(+), 177 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 01e82b556..5ca3195a0 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -102,6 +102,14 @@ def __init__(self, view, configure, worker_controller, worker_data): self.view.strategy_widget.center_price_dynamic_input.toggled.connect( self.onchange_center_price_dynamic_input ) + self.view.strategy_widget.manual_offset_input.valueChanged.connect( + self.onchange_manual_offset_input + ) + + # QSlider uses (int) values and manual_offset is stored as (float) with 0.1 precision. + # This reverts it so QSlider can handle the number, when fetching from config. + if worker_data: + worker_data['manual_offset'] = worker_data['manual_offset'] * 10 # Do this after the event connecting super().__init__(view, configure, worker_controller, worker_data) @@ -109,6 +117,18 @@ def __init__(self, view, configure, worker_controller, worker_data): if not self.view.strategy_widget.center_price_dynamic_input.isChecked(): self.view.strategy_widget.center_price_input.setDisabled(False) + @property + def values(self): + # This turns the int value of manual_offset from QSlider to float with desired precision. + values = super().values + values['manual_offset'] = values['manual_offset'] / 10 + return values + + def onchange_manual_offset_input(self): + value = self.view.strategy_widget.manual_offset_input.value() / 10 + text = "{}%".format(value) + self.view.strategy_widget.manual_offset_amount_label.setText(text) + def onchange_relative_order_size_input(self, checked): if checked: self.order_size_input_to_relative() diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index c0d10ff6e..c532af3a3 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 449 - 297 + 309 @@ -142,6 +142,86 @@ + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 9 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 8 + + + 1000000000.000000000000000 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + + + + + + @@ -450,8 +530,8 @@ - - + + 0 @@ -464,15 +544,9 @@ 0 - - - 120 - 16777215 - - - + - 0 + 9 0 @@ -487,70 +561,75 @@ 0 - - - Qt::Horizontal + + + false - + + + 0 + 0 + + + - 40 - 20 + 170 + 0 - + + ArrowCursor + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + false + + + false + + + 8 + + + 0.000000000000000 + + + 999999999.998999953269958 + + - + + + true + 0 0 - - - 75 - true - - - - WhatsThisCursor - - - Always calculate the middle from the closest market orders + + + 120 + 0 + - ? - - - 5 + - - - - - 0 - 0 - - - - Update center price from closest market orders - - - true - - - false - - - - - + + 0 @@ -569,7 +648,7 @@ 16777215 - + 0 @@ -586,7 +665,7 @@ 0 - + Qt::Horizontal @@ -599,7 +678,7 @@ - + 0 @@ -616,7 +695,7 @@ WhatsThisCursor - Automatically adjust orders up or down based on the imbalance of your assets + Always calculate the middle from the closest market orders ? @@ -629,14 +708,26 @@ - - + + + + + 0 + 0 + + - Center price offset based on asset balances + Update center price from closest market orders + + + true + + + false - + @@ -698,9 +789,6 @@ true - - manual_offset_input - @@ -734,61 +822,27 @@ - - + + - + 0 0 - 170 + 290 0 - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - QAbstractSpinBox::UpDownArrows - - - false - - - % - - - -50.000000000000000 - - - 100.000000000000000 - - - - - - - - 0 - 0 - - - + - 120 - 0 + 16777215 + 16777215 - - - 9 - + 0 @@ -801,70 +855,104 @@ 0 - - - - false - + + 0 + + + 6 + + + - + 0 0 - + - 170 - 0 + 40 + 16777215 - - + + 0 % Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - false - - - false - - - 8 - - - 0.000000000000000 - - - 999999999.998999953269958 - - - + + + + true + - + 0 0 - 120 + 250 0 - - + + + 145 + 16777215 + + + + QSlider::groove:horizontal { + border: 1px solid #999999; + height: 8px; /* The groove expands to the size of the slider by default. by giving it a height, it has a fixed size */ + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #ffffff, stop:1 #ffffff); + margin: 2px 0; +} + +QSlider::handle:horizontal { + background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #bfc3c9, stop:1 #bfc3c9); + border: 1px solid #5c5c5c; + width: 18px; + margin: -2px 0; /* Handle is placed by default on the contents rect of the groove. Expand outside the groove */ + border-radius: 3px; +} + + + -100 + + + 100 + + + 1 + + + Qt::Horizontal + + + false + + + false + + + QSlider::TicksBelow + + + 20 - - + + 0 @@ -877,9 +965,15 @@ 0 - + + + 120 + 16777215 + + + - 9 + 0 0 @@ -894,55 +988,56 @@ 0 - - - - 0 - 0 - + + + Qt::Horizontal - + - 170 - 0 + 40 + 20 - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 8 - - - 1000000000.000000000000000 - - + - + 0 0 - - - 120 - 0 - + + + 75 + true + + + + WhatsThisCursor + + + Automatically adjust orders up or down based on the imbalance of your assets - + ? + + + 5 + + + + Center price offset based on asset balances + + + @@ -957,12 +1052,12 @@ setDisabled(bool) - 284 - 129 + 427 + 207 - 208 - 99 + 312 + 168 From b8a1a96b8fef3e796ec8a7913b20b48f9cd8bb2b Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 29 Aug 2018 13:55:01 +0300 Subject: [PATCH 0664/1846] Small changes to relative orders logic --- dexbot/basestrategy.py | 2 +- dexbot/controllers/strategy_controller.py | 35 +++++++++++------------ dexbot/strategies/relative_orders.py | 14 ++++----- 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index ac7227b2a..d4e8762f2 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -421,7 +421,7 @@ def get_updated_limit_order(limit_order): :return: dict """ o = copy.deepcopy(limit_order) - price = o['sell_price']['base']['amount'] / o['sell_price']['quote']['amount'] + price = float(o['sell_price']['base']['amount']) / float(o['sell_price']['quote']['amount']) base_amount = o['for_sale'] quote_amount = base_amount / price o['sell_price']['base']['amount'] = base_amount diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index b8f2a3068..db1d22ab2 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -84,39 +84,36 @@ def elements(self): class RelativeOrdersController(StrategyController): def __init__(self, view, configure, worker_controller, worker_data): + super().__init__(view, configure, worker_controller, worker_data) + self.view = view self.configure = configure self.worker_controller = worker_controller - self.view.strategy_widget.relative_order_size_input.toggled.connect( + widget = self.view.strategy_widget + + # Event connecting + widget.relative_order_size_input.clicked.connect( self.onchange_relative_order_size_input ) - self.view.strategy_widget.center_price_dynamic_input.toggled.connect( + widget.center_price_dynamic_input.clicked.connect( self.onchange_center_price_dynamic_input ) - self.view.strategy_widget.reset_on_partial_fill_input.toggled.connect( + widget.reset_on_partial_fill_input.clicked.connect( self.onchange_reset_on_partial_fill_input ) - self.view.strategy_widget.reset_on_price_change_input.toggled.connect( + widget.reset_on_price_change_input.clicked.connect( self.onchange_reset_on_price_change_input ) - self.view.strategy_widget.custom_expiration_input.toggled.connect( + widget.custom_expiration_input.clicked.connect( self.onchange_custom_expiration_input ) - # Do this after the event connecting - super().__init__(view, configure, worker_controller, worker_data) - - if not self.view.strategy_widget.center_price_dynamic_input.isChecked(): - self.view.strategy_widget.center_price_input.setDisabled(False) - - if not self.view.strategy_widget.reset_on_partial_fill_input.isChecked(): - self.view.strategy_widget.partial_fill_threshold_input.setDisabled(True) - - if not self.view.strategy_widget.reset_on_price_change_input.isChecked(): - self.view.strategy_widget.price_change_threshold_input.setDisabled(True) - - if not self.view.strategy_widget.custom_expiration_input.isChecked(): - self.view.strategy_widget.expiration_time_input.setDisabled(True) + # Trigger the onchange events once + self.onchange_relative_order_size_input(widget.relative_order_size_input.isChecked()) + self.onchange_center_price_dynamic_input(widget.center_price_dynamic_input.isChecked()) + self.onchange_reset_on_partial_fill_input(widget.reset_on_partial_fill_input.isChecked()) + self.onchange_reset_on_price_change_input(widget.reset_on_price_change_input.isChecked()) + self.onchange_custom_expiration_input(widget.custom_expiration_input.isChecked()) def onchange_relative_order_size_input(self, checked): if checked: diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index e9e475702..a9f638a90 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -4,7 +4,6 @@ from dexbot.basestrategy import BaseStrategy, ConfigElement from dexbot.qt_queue.idle_queue import idle_add -from bitshares.price import FilledOrder class Strategy(BaseStrategy): """ Relative Orders strategy @@ -106,7 +105,6 @@ def __init__(self, *args, **kwargs): self.check_orders() def error(self, *args, **kwargs): - self.cancel_all() self.disabled = True def tick(self, d): @@ -114,7 +112,7 @@ def tick(self, d): do not triggers a market_update event """ if (self.is_reset_on_price_change and not - self.counter % 8): + self.counter % 8): self.log.debug('Checking orders by tick threshold') self.check_orders() self.counter += 1 @@ -238,11 +236,11 @@ def check_orders(self, *args, **kwargs): if diff_rel >= self.partial_fill_threshold: need_update = True self.log.info('Partially filled order detected, filled {:.2%}'.format(diff_rel)) - # FIXME: need to write trade operation; possible race condition may occur: while - # we're updating order it may be filled futher so trade log entry will not + # FIXME: Need to write trade operation; possible race condition may occur: while + # we're updating order it may be filled further so trade log entry will not # be correct - if self.is_reset_on_price_change: + if self.is_reset_on_price_change and not self.is_center_price_dynamic: center_price = self.calculate_center_price( None, self.is_asset_offset, @@ -250,8 +248,7 @@ def check_orders(self, *args, **kwargs): self['order_ids'], self.manual_offset ) - diff = (self.center_price - center_price) / self.center_price - diff = abs(diff) + diff = abs((self.center_price - center_price) / self.center_price) if diff >= self.price_change_threshold: self.log.debug('Center price changed, updating orders. Diff: {:.2%}'.format(diff)) need_update = True @@ -264,7 +261,6 @@ def check_orders(self, *args, **kwargs): self.initializing = False if self.view: - self.update_gui_profit() self.update_gui_slider() self.last_check = datetime.now() From e8b996e8e7d5024b4297065c936955e2b985d832 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 29 Aug 2018 14:06:29 +0300 Subject: [PATCH 0665/1846] Fix logic error in relative orders controller --- dexbot/controllers/strategy_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index db1d22ab2..e87468747 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -139,7 +139,7 @@ def onchange_reset_on_partial_fill_input(self, checked): self.view.strategy_widget.partial_fill_threshold_input.setDisabled(True) def onchange_reset_on_price_change_input(self, checked): - if checked: + if checked and self.view.strategy_widget.center_price_dynamic_input.isChecked(): self.view.strategy_widget.price_change_threshold_input.setDisabled(False) else: self.view.strategy_widget.price_change_threshold_input.setDisabled(True) From a4f83d8939848b7cde729a584522d8d991f54adc Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 29 Aug 2018 14:08:17 +0300 Subject: [PATCH 0666/1846] Change dexbot version number to 0.5.14 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index c8b3af190..bcf8256d6 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.5.13' +VERSION = '0.5.14' AUTHOR = 'Codaone Oy' __version__ = VERSION From d009dc86117c0e96665ee4db6860889e488ada97 Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 29 Aug 2018 14:55:46 +0300 Subject: [PATCH 0667/1846] Change dexbot version number to 0.5.15 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index c8b3af190..d198c48bb 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.5.13' +VERSION = '0.5.15' AUTHOR = 'Codaone Oy' __version__ = VERSION From 0d8d47c5209ce8c37c9d4bb1a70c30aad9ec04cb Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 29 Aug 2018 15:07:25 +0300 Subject: [PATCH 0668/1846] Fix typo in basestrategy --- dexbot/basestrategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index d4e8762f2..1f2c4dd3c 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -280,7 +280,7 @@ def orders(self): @property def all_orders(self): - """ Return the worker's open accounts in all markets + """ Return the accounts's open orders in all markets """ self.account.refresh() return [o for o in self.account.openorders] From 8e1328bf8fe5541dd323026b698398a2abab3810 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 29 Aug 2018 15:22:22 +0300 Subject: [PATCH 0669/1846] Add new options to UI file --- .../views/ui/forms/relative_orders_widget.ui | 770 ++++++++++++++++-- 1 file changed, 687 insertions(+), 83 deletions(-) diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index c532af3a3..162dc1597 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 449 - 309 + 465 @@ -32,21 +32,6 @@ Worker Parameters - - 6 - - - 9 - - - 9 - - - 9 - - - 9 - @@ -222,7 +207,7 @@ - + @@ -302,14 +287,14 @@ - + Relative order size - + @@ -404,7 +389,7 @@ - + @@ -435,7 +420,7 @@ - + @@ -530,7 +515,7 @@ - + @@ -628,7 +613,7 @@ - + @@ -708,7 +693,7 @@ - + @@ -727,7 +712,94 @@ - + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Automatically adjust orders up or down based on the imbalance of your assets + + + ? + + + 5 + + + + + + + + + + Center price offset based on asset balances + + + + @@ -822,7 +894,7 @@ - + @@ -842,7 +914,7 @@ 16777215 - + 0 @@ -855,35 +927,7 @@ 0 - - 0 - - - 6 - - - - - - 0 - 0 - - - - - 40 - 16777215 - - - - 0 % - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - + true @@ -948,11 +992,33 @@ QSlider::handle:horizontal { + + + + + 0 + 0 + + + + + 40 + 16777215 + + + + 0 % + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + - - + + 0 @@ -971,7 +1037,7 @@ QSlider::handle:horizontal { 16777215 - + 0 @@ -988,7 +1054,7 @@ QSlider::handle:horizontal { 0 - + Qt::Horizontal @@ -1001,7 +1067,7 @@ QSlider::handle:horizontal { - + 0 @@ -1018,7 +1084,7 @@ QSlider::handle:horizontal { WhatsThisCursor - Automatically adjust orders up or down based on the imbalance of your assets + Reset orders when buy or sell order is partially filled ? @@ -1031,10 +1097,565 @@ QSlider::handle:horizontal { - - + + - Center price offset based on asset balances + Reset orders on partial fill + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Fill threshold + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Order fill threshold to reset orders + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + % + + + 100000.000000000000000 + + + 0.000000000000000 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Reset orders when center price is changed more than threshold + + + ? + + + 5 + + + + + + + + + + Resert orders on center price change + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Price change + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Define center price threshold to react on + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + % + + + 100000.000000000000000 + + + 0.000000000000000 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Override order expiration time to trigger a reset + + + ? + + + 5 + + + + + + + + + + Custom expiration + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Order expiration + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Define custom order expiration time to force orders reset more often, seconds + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 0 + + + 100000000000.000000000000000 + + + 5.000000000000000 @@ -1044,22 +1665,5 @@ QSlider::handle:horizontal { - - - center_price_dynamic_input - clicked(bool) - center_price_input - setDisabled(bool) - - - 427 - 207 - - - 312 - 168 - - - - + From c5ae34ca5b3fe337cb2a1a735f9d881809ef898e Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 29 Aug 2018 15:34:50 +0300 Subject: [PATCH 0670/1846] Change dexbot version number to 0.5.16 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index bcf8256d6..1cfa78bbc 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.5.14' +VERSION = '0.5.16' AUTHOR = 'Codaone Oy' __version__ = VERSION From 1143b8885858e223f81994de9f7a0e5de79e73fb Mon Sep 17 00:00:00 2001 From: Mika Koivistoinen Date: Wed, 29 Aug 2018 15:54:20 +0300 Subject: [PATCH 0671/1846] Slider hotfix --- dexbot/controllers/strategy_controller.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 0fffe03e1..28608c336 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -85,6 +85,10 @@ def elements(self): class RelativeOrdersController(StrategyController): def __init__(self, view, configure, worker_controller, worker_data): + # QSlider uses (int) values and manual_offset is stored as (float) with 0.1 precision. + # This reverts it so QSlider can handle the number, when fetching from config. + worker_data['manual_offset'] = worker_data['manual_offset'] * 10 + super().__init__(view, configure, worker_controller, worker_data) self.view = view @@ -104,12 +108,6 @@ def __init__(self, view, configure, worker_controller, worker_data): widget.relative_order_size_input.clicked.connect(self.onchange_relative_order_size_input) widget.center_price_dynamic_input.clicked.connect(self.onchange_center_price_dynamic_input) widget.manual_offset_input.valueChanged.connect(self.onchange_manual_offset_input) - - # QSlider uses (int) values and manual_offset is stored as (float) with 0.1 precision. - # This reverts it so QSlider can handle the number, when fetching from config. - if worker_data: - worker_data['manual_offset'] = worker_data['manual_offset'] * 10 - widget.reset_on_partial_fill_input.clicked.connect(self.onchange_reset_on_partial_fill_input) widget.reset_on_price_change_input.clicked.connect(self.onchange_reset_on_price_change_input) widget.custom_expiration_input.clicked.connect(self.onchange_custom_expiration_input) @@ -120,6 +118,7 @@ def __init__(self, view, configure, worker_controller, worker_data): self.onchange_reset_on_partial_fill_input(widget.reset_on_partial_fill_input.isChecked()) self.onchange_reset_on_price_change_input(widget.reset_on_price_change_input.isChecked()) self.onchange_custom_expiration_input(widget.custom_expiration_input.isChecked()) + self.onchange_manual_offset_input() @property def values(self): From 66debd6cd19fb304752c62b9c1e85b8f4c53d900 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 28 Aug 2018 23:06:00 +0500 Subject: [PATCH 0672/1846] Add more descriptions by Marko for various modes --- dexbot/strategies/staggered_orders.py | 45 ++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 3ee9fc985..31594ba41 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -488,7 +488,23 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): def increase_order_sizes(self, asset, asset_balance, orders): """ Checks which order should be increased in size and replaces it with a maximum size order, according to global limits. Logic - depends on mode in question + depends on mode in question. + + Mountain: + Maximize order size as close to center as possible + + Neutral: + Try to flatten everything by increasing order sizes to neutral. When everything is correct, maximize closest + orders and then increase other orders to match that. + + Valley: + Maximize order sizes as far as possible from center + + Buy slope: + Maximize order size as low as possible. Buy orders as far, and sell orders as close as possible to cp. + + Sell slope: + Maximize order size as high as possible. Buy orders as close, and sell orders as far as possible from cp :param str | asset: 'base' or 'quote', depending if checking sell or buy :param Amount | asset_balance: Balance of the account @@ -865,6 +881,33 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p Turn BASE amount into QUOTE amount (we will buy this QUOTE amount). QUOTE = BASE / price + Furthest order amount calculations: + ----------------------------------- + + Mountain: + For asset to be allocated (base for buy and quote for sell orders) + First order = balance * increment + Next order = previous order / (1 + increment) + Repeat until last order. + + Neutral: + For asset to be allocated (base for buy and quote for sell orders) + First order = balance * (sqrt(1 + increment) - 1) + Next order = previous order / sqrt(1 + increment) + Repeat until last order + + Valley: + For asset to be allocated (base for buy and quote for sell orders) + All orders = balance / number of orders (per side) + + Buy slope: + Buy orders same as valley + Sell orders same asmountain + + Sell slope: + Buy orders same as mountain + Sell orders same as valley + Mode: MOUNTAIN :param Amount | base_balance: Available BASE asset balance :param bool | place_order: True = Places order to the market, False = returns amount and price From a380b0c12185299027778926b8cfe11455dc33fc Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 29 Aug 2018 11:17:40 +0500 Subject: [PATCH 0673/1846] Prevent zero division error when orders_sum == 0 This may happen whether furthest order price is outside lower or upper bound. --- dexbot/strategies/staggered_orders.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 31594ba41..3d31e6036 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -857,6 +857,12 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente amount = quote_balance['amount'] * self.increment previous_amount = amount + if price > self.upper_bound: + self.log.info('Not placing highest sell order because price will exceed higher bound. Market center ' + 'price: {:.8f}, closest order price: {:.8f}, higher_bound: {}'.format(market_center_price, + price, self.higher_bound)) + return + while price <= self.upper_bound: orders_sum += previous_amount previous_price = price @@ -924,6 +930,12 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p amount = base_balance['amount'] * self.increment previous_amount = amount + if price < self.lower_bound: + self.log.info('Not placing lowest buy order because price will exceed lower bound. Market center price: ' + '{:.8f}, closest order price: {:.8f}, lower bound: {}'.format(market_center_price, price, + self.lower_bound)) + return + while price >= self.lower_bound: orders_sum += previous_amount previous_price = price From 1b055562be2e9ec8f05b6ca1f58ad2df15a6d8e4 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 30 Aug 2018 08:22:58 +0300 Subject: [PATCH 0674/1846] Fix error when creating relative orders worker --- dexbot/controllers/strategy_controller.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 28608c336..040282d03 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -85,9 +85,11 @@ def elements(self): class RelativeOrdersController(StrategyController): def __init__(self, view, configure, worker_controller, worker_data): - # QSlider uses (int) values and manual_offset is stored as (float) with 0.1 precision. - # This reverts it so QSlider can handle the number, when fetching from config. - worker_data['manual_offset'] = worker_data['manual_offset'] * 10 + # Check if there is worker data. This prevents error when multiplying None type when creating worker. + if worker_data: + # QSlider uses (int) values and manual_offset is stored as (float) with 0.1 precision. + # This reverts it so QSlider can handle the number, when fetching from config. + worker_data['manual_offset'] = worker_data['manual_offset'] * 10 super().__init__(view, configure, worker_controller, worker_data) From 2fb0ece4ca6a5c63c6a2ae8ec4d34e484a4193ad Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 30 Aug 2018 08:24:20 +0300 Subject: [PATCH 0675/1846] Change forms tab orders --- dexbot/views/ui/create_worker_window.ui | 3 ++- dexbot/views/ui/forms/relative_orders_widget.ui | 15 +++++++++++++++ dexbot/views/ui/forms/staggered_orders_widget.ui | 9 +++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/dexbot/views/ui/create_worker_window.ui b/dexbot/views/ui/create_worker_window.ui index 0dfbfdf73..669eb9eed 100644 --- a/dexbot/views/ui/create_worker_window.ui +++ b/dexbot/views/ui/create_worker_window.ui @@ -660,12 +660,13 @@ strategy_input worker_name_input + base_asset_input quote_asset_input fee_asset_input account_input private_key_input - cancel_button save_button + cancel_button diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index 162dc1597..2c042d89f 100644 --- a/dexbot/views/ui/forms/relative_orders_widget.ui +++ b/dexbot/views/ui/forms/relative_orders_widget.ui @@ -1664,6 +1664,21 @@ QSlider::handle:horizontal { + + amount_input + relative_order_size_input + spread_input + center_price_input + center_price_dynamic_input + center_price_offset_input + manual_offset_input + reset_on_partial_fill_input + partial_fill_threshold_input + reset_on_price_change_input + price_change_threshold_input + custom_expiration_input + expiration_time_input + diff --git a/dexbot/views/ui/forms/staggered_orders_widget.ui b/dexbot/views/ui/forms/staggered_orders_widget.ui index 87c308902..9ba44817c 100644 --- a/dexbot/views/ui/forms/staggered_orders_widget.ui +++ b/dexbot/views/ui/forms/staggered_orders_widget.ui @@ -959,6 +959,15 @@ + + amount_input + spread_input + increment_input + lower_bound_input + center_price_input + center_price_dynamic_input + upper_bound_input + From 9cb931a221a1787f17262148cdd4facc0e939adc Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 30 Aug 2018 08:25:08 +0300 Subject: [PATCH 0676/1846] Change fill threshold max to 100 --- dexbot/views/ui/forms/relative_orders_widget.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index 2c042d89f..ce3ec4e66 100644 --- a/dexbot/views/ui/forms/relative_orders_widget.ui +++ b/dexbot/views/ui/forms/relative_orders_widget.ui @@ -1223,7 +1223,7 @@ QSlider::handle:horizontal { % - 100000.000000000000000 + 100.000000000000000 0.000000000000000 From 3e727ba9e27058a2bd61bb78eb37b6d5bdce07de Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 30 Aug 2018 08:29:20 +0300 Subject: [PATCH 0677/1846] Change dexbot version number to 0.5.17 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 1cfa78bbc..83eb9651b 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.5.16' +VERSION = '0.5.17' AUTHOR = 'Codaone Oy' __version__ = VERSION From 8c2295938dbe5375f8542cff8545bb61108abeb9 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 30 Aug 2018 10:19:41 +0300 Subject: [PATCH 0678/1846] Fix string divided by float error --- dexbot/basestrategy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 99abd466e..33d113698 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -431,7 +431,7 @@ def get_updated_limit_order(limit_order): """ o = copy.deepcopy(limit_order) price = float(o['sell_price']['base']['amount']) / float(o['sell_price']['quote']['amount']) - base_amount = o['for_sale'] + base_amount = float(o['for_sale']) quote_amount = base_amount / price o['sell_price']['base']['amount'] = base_amount o['sell_price']['quote']['amount'] = quote_amount From fa2e9e3a8ec24fb82c3ef29965dee492674170f7 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 30 Aug 2018 10:20:15 +0300 Subject: [PATCH 0679/1846] Change max limits of strategy fields --- dexbot/strategies/staggered_orders.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 3d31e6036..fcb786060 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -54,13 +54,16 @@ def configure(cls, return_base_config=True): 'Begin strategy with center price obtained from the market. Use with mature markets', None), ConfigElement( 'center_price', 'float', 0, 'Manual center price', - 'In an immature market, give a center price manually to begin with. BASE/QUOTE', (0, None, 8, '')), + 'In an immature market, give a center price manually to begin with. BASE/QUOTE', + (0, 1000000000, 8, '')), ConfigElement( 'lower_bound', 'float', 1, 'Lower bound', - 'The bottom price in the range', (0, None, 8, '')), + 'The bottom price in the range', + (0, 1000000000, 8, '')), ConfigElement( 'upper_bound', 'float', 1000000, 'Upper bound', - 'The top price in the range', (0, None, 8, '')), + 'The top price in the range', + (0, 1000000000, 8, '')), ConfigElement( 'instant_fill', 'bool', True, 'Allow instant fill', 'Allow to execute orders by market', None) From 0fdc5bb05e70b6f33e99f63abc80a04321bb5901 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 30 Aug 2018 12:24:10 +0300 Subject: [PATCH 0680/1846] Change dexbot version number to 0.6.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 83eb9651b..071269db9 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.5.17' +VERSION = '0.6.0' AUTHOR = 'Codaone Oy' __version__ = VERSION From 71f5d9dbf08cfef3abaefaea1eac0708327af322 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 30 Aug 2018 13:16:38 +0500 Subject: [PATCH 0681/1846] Limit size increase when partially filled order detected Reserve funds for next order when there is partially filled order on the opposite side. --- dexbot/strategies/staggered_orders.py | 30 +++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index fcb786060..b87ce38f8 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -388,10 +388,21 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): # Do not try to do anything than placing higher buy whether there is no sell orders return elif lowest_buy_order_price / (1 + self.increment) < self.lower_bound: - self.bootstrapping = False # Lower bound has been reached and now will start allocating rest of the base balance. + self.bootstrapping = False self.log.debug('Increasing orders sizes for BASE asset') - self.increase_order_sizes('base', base_balance, self.buy_orders) + lowest_sell_order = self.sell_orders[0] + if not self.check_partial_fill(lowest_sell_order): + """ Detect partially filled order on the opposite side and reserve appropriate amount to place + higher buy order + """ + higher_buy_order = self.place_higher_buy_order(highest_buy_order, place_order=False) + funds_to_reserve = higher_buy_order['amount'] * higher_buy_order['price'] + self.log.debug('Partially filled order on opposite side, reserving funds for next buy order: ' + '{:.8f} {}'.format(funds_to_reserve, self.market['base']['symbol'])) + base_balance -= funds_to_reserve + if base_balance > self.base_asset_threshold: + self.increase_order_sizes('base', base_balance, self.buy_orders) else: # Lower bound is not reached, we need to add additional orders at the extremes self.bootstrapping = False @@ -460,10 +471,21 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): # Do not try to do anything than placing lower sell whether there is no buy orders return elif highest_sell_order_price * (1 + self.increment) > self.upper_bound: - self.bootstrapping = False # Upper bound has been reached and now will start allocating rest of the quote balance. + self.bootstrapping = False self.log.debug('Increasing orders sizes for QUOTE asset') - self.increase_order_sizes('quote', quote_balance, self.sell_orders) + highest_buy_order = self.buy_orders[0] + if not self.check_partial_fill(highest_buy_order): + """ Detect partially filled order on the opposite side and reserve appropriate amount to place + lower sell order + """ + # Base amount of sell order is actually QUOTE + funds_to_reserve = lowest_sell_order['base']['amount'] + self.log.debug('Partially filled order on opposite side, reserving funds for next sell order: ' + '{:.8f} {}'.format(funds_to_reserve, self.market['quote']['symbol'])) + quote_balance -= funds_to_reserve + if quote_balance > self.quote_asset_threshold: + self.increase_order_sizes('quote', quote_balance, self.sell_orders) else: # Higher bound is not reached, we need to add additional orders at the extremes self.bootstrapping = False From dfc815e74eaa8cd877ac8a409f5240a2293525f1 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 29 Aug 2018 22:04:46 +0500 Subject: [PATCH 0682/1846] Initial implementation of valley mode --- dexbot/strategies/staggered_orders.py | 199 +++++++++++++++++++------- 1 file changed, 147 insertions(+), 52 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index b87ce38f8..655021dd6 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -34,7 +34,7 @@ def configure(cls, return_base_config=True): modes = [ ('mountain', 'Mountain'), # ('neutral', 'Neutral'), - # ('valley', 'Valley'), + ('valley', 'Valley'), # ('buy_slope', 'Buy Slope'), # ('sell_slope', 'Sell Slope') ] @@ -693,7 +693,66 @@ def increase_order_sizes(self, asset, asset_balance, orders): # One increase at a time. This prevents running more than one increment round simultaneously. return elif self.mode == 'valley': - pass + """ Starting from the closest-to-center order, for each order, see if it is approximately + maximum size. + If it is, move on to next. + If not, cancel it and replace with maximum size order. Maximum order size will be a + size of higher order. Then return. + If furthest is reached, increase it to maximum size. + + Maximum size is (example for buy orders): + 1. As many "base" as the order below (further_bound) + """ + if asset == 'quote': + total_balance = self.quote_total_balance + order_type = 'sell' + elif asset == 'base': + total_balance = self.base_total_balance + order_type = 'buy' + + orders_count = len(orders) + + for order in orders: + order_index = orders.index(order) + order_amount = order['base']['amount'] + + if order_index + 1 < orders_count: + further_order = orders[order_index + 1] + further_bound = further_order['base']['amount'] + else: + """ Special processing for least order. + + Calculte new order amount based on orders count, but do not allow to perform too small increase + rounds. New lowest buy / highest sell should be higher by at least one increment. + """ + further_bound = order_amount * (1 + self.increment) + new_amount = (total_balance / orders_count) / (1 + self.increment / 1000) + if new_amount > further_bound: + # Maximize order whether we can break further_bound limit + further_bound = new_amount + + if (order_amount * (1 + self.increment / 10) < further_bound and + asset_balance + order_amount >= further_bound): + # Replace order only when we have the balance to place new full-sized order + amount_base = further_bound + + if asset == 'quote': + price = (order['price'] ** -1) + elif asset == 'base': + price = order['price'] + self.log.debug('Cancelling {} order in increase_order_sizes(); ' + 'mode: {}, amount: {}, price: {:.8f}'.format(order_type, self.mode, order_amount, + price)) + self.cancel(order) + + if asset == 'quote': + self.market_sell(amount_base, price) + elif asset == 'base': + amount_quote = amount_base / price + self.market_buy(amount_quote, price) + # One increase at a time. This prevents running more than one increment round simultaneously. + return + elif self.mode == 'neutral': pass elif self.mode == 'buy_slope': @@ -720,9 +779,6 @@ def check_partial_fill(self, order): def place_higher_buy_order(self, order, place_order=True, allow_partial=False, base_limit=None, limit=None): """ Place higher buy order - Mode: MOUNTAIN - amount (QUOTE) = lower_buy_order_amount - price (BASE) = lower_buy_order_price * (1 + increment) :param order: Previously highest buy order :param bool | place_order: True = Places order to the market, False = returns amount and price @@ -735,15 +791,20 @@ def place_higher_buy_order(self, order, place_order=True, allow_partial=False, b self.disabled = True return None - amount = order['quote']['amount'] price = order['price'] * (1 + self.increment) - # How many BASE we need to buy QUOTE `amount` - base_amount = amount * price if not self.is_instant_fill_enabled and price > float(self.ticker['lowestAsk']): self.log.info('Refusing to place an order which crosses lowestAsk') return None + if self.mode == 'mountain': + amount = order['quote']['amount'] + # How many BASE we need to buy QUOTE `amount` + base_amount = amount * price + elif self.mode == 'valley': + base_amount = order['base']['amount'] + amount = base_amount / price + if base_limit and base_limit < base_amount: base_amount = base_limit amount = base_limit / price @@ -768,16 +829,18 @@ def place_higher_buy_order(self, order, place_order=True, allow_partial=False, b def place_higher_sell_order(self, order, place_order=True, allow_partial=False): """ Place higher sell order - Mode: MOUNTAIN - amount (BASE) = higher_sell_order_amount / (1 + increment) - price (BASE) = higher_sell_order_price * (1 + increment) :param order: highest_sell_order :param bool | place_order: True = Places order to the market, False = returns amount and price :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance """ - amount = order['base']['amount'] / (1 + self.increment) price = (order['price'] ** -1) * (1 + self.increment) + + if self.mode == 'mountain': + amount = order['base']['amount'] / (1 + self.increment) + elif self.mode == 'valley': + amount = order['base']['amount'] + if amount > self.quote_balance['amount']: if place_order and not allow_partial: self.log.debug('Not enough balance to place_higher_sell_order; need/avail: {}/{}' @@ -794,16 +857,18 @@ def place_higher_sell_order(self, order, place_order=True, allow_partial=False): def place_lower_buy_order(self, order, place_order=True, allow_partial=False): """ Place lower buy order - Mode: MOUNTAIN - amount (QUOTE) = lowest_buy_order_amount - price (BASE) = Order's base price / (1 + increment) :param order: Previously lowest buy order :param bool | place_order: True = Places order to the market, False = returns amount and price :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance """ - amount = order['quote']['amount'] price = order['price'] / (1 + self.increment) + + if self.mode == 'mountain': + amount = order['quote']['amount'] + elif self.mode == 'valley': + amount = order['base']['amount'] / price + # How many BASE we need to buy QUOTE `amount` base_amount = amount * price @@ -823,9 +888,6 @@ def place_lower_buy_order(self, order, place_order=True, allow_partial=False): def place_lower_sell_order(self, order, place_order=True, allow_partial=False, base_limit=None, limit=None): """ Place lower sell order - Mode: MOUNTAIN - amount (BASE) = higher_sell_order_amount * (1 + increment) - price (BASE) = higher_sell_order_price / (1 + increment) :param order: Previously higher sell order :param bool | place_order: True = Places order to the market, False = returns amount and price @@ -838,14 +900,20 @@ def place_lower_sell_order(self, order, place_order=True, allow_partial=False, b self.disabled = True return None - amount = order['base']['amount'] * (1 + self.increment) price = (order['price'] ** -1) / (1 + self.increment) + if self.mode == 'mountain': + amount = order['base']['amount'] * (1 + self.increment) + elif self.mode == 'valley': + amount = order['base']['amount'] + + base_amount = amount * price + if not self.is_instant_fill_enabled and price < float(self.ticker['highestBid']): self.log.info('Refusing to place an order which crosses highestBid') return None - if base_limit: + if base_limit and base_limit < base_amount: amount = base_limit / price elif limit and limit < amount: amount = limit @@ -866,7 +934,7 @@ def place_lower_sell_order(self, order, place_order=True, allow_partial=False, b def place_highest_sell_order(self, quote_balance, place_order=True, market_center_price=None): """ Places sell order furthest to the market center price - Mode: MOUNTAIN + :param Amount | quote_balance: Available QUOTE asset balance :param bool | place_order: True = Places order to the market, False = returns amount and price :param float | market_center_price: Optional market center price, used to to check order @@ -876,29 +944,43 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente market_center_price = self.market_center_price price = market_center_price * math.sqrt(1 + self.target_spread) - previous_price = price - orders_sum = 0 - - amount = quote_balance['amount'] * self.increment - previous_amount = amount if price > self.upper_bound: - self.log.info('Not placing highest sell order because price will exceed higher bound. Market center ' - 'price: {:.8f}, closest order price: {:.8f}, higher_bound: {}'.format(market_center_price, - price, self.higher_bound)) + self.log.info( + 'Not placing highest sell order because price will exceed higher bound. Market center ' + 'price: {:.8f}, closest order price: {:.8f}, higher_bound: {}'.format(market_center_price, + price, self.higher_bound)) return - while price <= self.upper_bound: - orders_sum += previous_amount + if self.mode == 'mountain': previous_price = price + orders_sum = 0 + amount = quote_balance['amount'] * self.increment previous_amount = amount - price = price * (1 + self.increment) - amount = amount / (1 + self.increment) + while price <= self.upper_bound: + orders_sum += previous_amount + previous_price = price + previous_amount = amount + price = price * (1 + self.increment) + amount = amount / (1 + self.increment) + + price = previous_price + amount_quote = previous_amount * (self.quote_total_balance / orders_sum) * (1 + self.increment * 0.75) + + elif self.mode == 'valley': + orders_count = 0 + while price <= self.upper_bound: + previous_price = price + orders_count += 1 + price = price * (1 + self.increment) + + price = previous_price + amount_quote = quote_balance / orders_count + # Slightly reduce order amount to avoid rounding issues + amount_quote = amount_quote / (1 + self.increment / 1000) precision = self.market['quote']['precision'] - price = previous_price - amount_quote = previous_amount * (self.quote_total_balance / orders_sum) * (1 + self.increment * 0.75) amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) if place_order: @@ -939,7 +1021,6 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p Buy orders same as mountain Sell orders same as valley - Mode: MOUNTAIN :param Amount | base_balance: Available BASE asset balance :param bool | place_order: True = Places order to the market, False = returns amount and price :param float | market_center_price: Optional market center price, used to to check order @@ -949,30 +1030,44 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p market_center_price = self.market_center_price price = market_center_price / math.sqrt(1 + self.target_spread) - previous_price = price - orders_sum = 0 - - amount = base_balance['amount'] * self.increment - previous_amount = amount if price < self.lower_bound: - self.log.info('Not placing lowest buy order because price will exceed lower bound. Market center price: ' - '{:.8f}, closest order price: {:.8f}, lower bound: {}'.format(market_center_price, price, - self.lower_bound)) + self.log.info( + 'Not placing lowest buy order because price will exceed lower bound. Market center price: ' + '{:.8f}, closest order price: {:.8f}, lower bound: {}'.format(market_center_price, price, + self.lower_bound)) return - while price >= self.lower_bound: - orders_sum += previous_amount + if self.mode == 'mountain': previous_price = price + orders_sum = 0 + amount = base_balance['amount'] * self.increment previous_amount = amount - price = price / (1 + self.increment) - amount = amount / (1 + self.increment) + while price >= self.lower_bound: + orders_sum += previous_amount + previous_price = price + previous_amount = amount + price = price / (1 + self.increment) + amount = amount / (1 + self.increment) + + amount_base = previous_amount * (self.base_total_balance / orders_sum) * (1 + self.increment * 0.75) + price = previous_price + amount_quote = amount_base / price + elif self.mode == 'valley': + orders_count = 0 + while price >= self.lower_bound: + previous_price = price + price = price / (1 + self.increment) + orders_count += 1 + + price = previous_price + amount_base = self.base_total_balance / orders_count + amount_quote = amount_base / price + # Slightly reduce order amount to avoid rounding issues + amount_quote = amount_quote / (1 + self.increment / 1000) precision = self.market['quote']['precision'] - amount_base = previous_amount * (self.base_total_balance / orders_sum) * (1 + self.increment * 0.75) - price = previous_price - amount_quote = amount_base / price amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) if place_order: From f18cd80a12e211dcb95062e90bf16791a45e0a21 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 29 Aug 2018 23:40:51 +0500 Subject: [PATCH 0683/1846] Fix logic of order increases for valley mode --- dexbot/strategies/staggered_orders.py | 32 +++++++++++++++------------ 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 655021dd6..c09fff9b5 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -516,14 +516,16 @@ def increase_order_sizes(self, asset, asset_balance, orders): depends on mode in question. Mountain: - Maximize order size as close to center as possible + Maximize order size as close to center as possible. When all orders are max, the new increase round is + started from the furthest order. Neutral: Try to flatten everything by increasing order sizes to neutral. When everything is correct, maximize closest orders and then increase other orders to match that. Valley: - Maximize order sizes as far as possible from center + Maximize order sizes as far as possible from center first. When all orders are max, the new increase round + is started from the closest-to-center order. Buy slope: Maximize order size as low as possible. Buy orders as far, and sell orders as close as possible to cp. @@ -693,7 +695,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): # One increase at a time. This prevents running more than one increment round simultaneously. return elif self.mode == 'valley': - """ Starting from the closest-to-center order, for each order, see if it is approximately + """ Starting from the furthest order, for each order, see if it is approximately maximum size. If it is, move on to next. If not, cancel it and replace with maximum size order. Maximum order size will be a @@ -701,7 +703,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): If furthest is reached, increase it to maximum size. Maximum size is (example for buy orders): - 1. As many "base" as the order below (further_bound) + 1. As many "base" as the order below (closer_order_bound) """ if asset == 'quote': total_balance = self.quote_total_balance @@ -711,30 +713,32 @@ def increase_order_sizes(self, asset, asset_balance, orders): order_type = 'buy' orders_count = len(orders) + orders = list(reversed(orders)) for order in orders: order_index = orders.index(order) order_amount = order['base']['amount'] if order_index + 1 < orders_count: - further_order = orders[order_index + 1] - further_bound = further_order['base']['amount'] + # Closer order is an order which one-step closer to the center + closer_order = orders[order_index + 1] + closer_order_bound = closer_order['base']['amount'] else: - """ Special processing for least order. + """ Special processing for the closest order. Calculte new order amount based on orders count, but do not allow to perform too small increase rounds. New lowest buy / highest sell should be higher by at least one increment. """ - further_bound = order_amount * (1 + self.increment) + closer_order_bound = order_amount * (1 + self.increment) new_amount = (total_balance / orders_count) / (1 + self.increment / 1000) - if new_amount > further_bound: - # Maximize order whether we can break further_bound limit - further_bound = new_amount + if new_amount > closer_order_bound: + # Maximize order up to max possible amount if we can + closer_order_bound = new_amount - if (order_amount * (1 + self.increment / 10) < further_bound and - asset_balance + order_amount >= further_bound): + if (order_amount * (1 + self.increment / 10) < closer_order_bound and + asset_balance + order_amount >= closer_order_bound): # Replace order only when we have the balance to place new full-sized order - amount_base = further_bound + amount_base = closer_order_bound if asset == 'quote': price = (order['price'] ** -1) From fad8107f15541ee1aec2f4a53c0f53bab17bd65a Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 30 Aug 2018 15:37:32 +0500 Subject: [PATCH 0684/1846] Adjust furthest orders reduce factor This is needed to not turn off bootstrap prematurely and let to keep enough funds for comissions. --- dexbot/strategies/staggered_orders.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c09fff9b5..43376c80d 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -730,7 +730,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): rounds. New lowest buy / highest sell should be higher by at least one increment. """ closer_order_bound = order_amount * (1 + self.increment) - new_amount = (total_balance / orders_count) / (1 + self.increment / 1000) + new_amount = (total_balance / orders_count) / (1 + self.increment / 100) if new_amount > closer_order_bound: # Maximize order up to max possible amount if we can closer_order_bound = new_amount @@ -982,7 +982,7 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente price = previous_price amount_quote = quote_balance / orders_count # Slightly reduce order amount to avoid rounding issues - amount_quote = amount_quote / (1 + self.increment / 1000) + amount_quote = amount_quote / (1 + self.increment / 100) precision = self.market['quote']['precision'] amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) @@ -1068,8 +1068,10 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p price = previous_price amount_base = self.base_total_balance / orders_count amount_quote = amount_base / price - # Slightly reduce order amount to avoid rounding issues - amount_quote = amount_quote / (1 + self.increment / 1000) + """ Slightly reduce order amount to avoid rounding issues AND to leave some free balance after initial + allocation to not turn bootstrap off prematurely + """ + amount_quote = amount_quote / (1 + self.increment / 100) precision = self.market['quote']['precision'] amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) From aa28d1472d5867d2f5a458745432d1dc17504497 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 30 Aug 2018 17:47:27 +0500 Subject: [PATCH 0685/1846] Allow increasing order sizes with avail balance fallback This change is for valley mode, and it makes valley mode increase behavior consistent with mountain mode. --- dexbot/strategies/staggered_orders.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 43376c80d..bcf3db233 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -735,11 +735,14 @@ def increase_order_sizes(self, asset, asset_balance, orders): # Maximize order up to max possible amount if we can closer_order_bound = new_amount - if (order_amount * (1 + self.increment / 10) < closer_order_bound and - asset_balance + order_amount >= closer_order_bound): - # Replace order only when we have the balance to place new full-sized order + if order_amount * (1 + self.increment / 10) < closer_order_bound: amount_base = closer_order_bound + # Limit order to available balance + if asset_balance < amount_base - order_amount: + amount_base = order_amount + asset_balance['amount'] + self.log.info('Limiting new order to avail asset balance: {}'.format(amount_base)) + if asset == 'quote': price = (order['price'] ** -1) elif asset == 'base': From 9da84e1b0321629306d22eed0b32b190ecc5a307 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 30 Aug 2018 20:28:44 +0500 Subject: [PATCH 0686/1846] Fix import styling --- dexbot/strategies/staggered_orders.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index bcf3db233..307690bb9 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1,6 +1,5 @@ import math -from datetime import datetime -from datetime import timedelta +from datetime import datetime, timedelta from dexbot.basestrategy import BaseStrategy, ConfigElement from dexbot.qt_queue.idle_queue import idle_add From 90340e8a8da3feae81206a12c37260b30b408a76 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 30 Aug 2018 20:34:47 +0500 Subject: [PATCH 0687/1846] Add workaround for wrong CER in library Woraround for https://github.com/bitshares/python-bitshares/issues/138 --- dexbot/strategies/staggered_orders.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 307690bb9..2db9170ac 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1,5 +1,6 @@ import math from datetime import datetime, timedelta +from bitshares.market import Market from dexbot.basestrategy import BaseStrategy, ConfigElement from dexbot.qt_queue.idle_queue import idle_add @@ -259,7 +260,15 @@ def refresh_balances(self): # Reserve transaction fee equivalent in BTS self.ticker = self.market.ticker() - core_exchange_rate = self.ticker['core_exchange_rate'] + + # FIXME: this is a temporal workaround for https://github.com/bitshares/python-bitshares/issues/138 + if self.market['quote']['id'] == '1.3.0': + temp_market = Market(base=self.market['quote'], quote=self.market['base'], + bitshare_instance=self.bitshares) + ticker = temp_market.ticker() + core_exchange_rate = ticker['core_exchange_rate'].invert() + else: + core_exchange_rate = self.ticker['core_exchange_rate'] # Todo: order_creation_fee(BTS) = 0.01 for now self.quote_fee_reserve = 0.01 * core_exchange_rate['quote']['amount'] * 100 self.base_fee_reserve = 0.01 * core_exchange_rate['base']['amount'] * 100 From 7ebd3951c7b4c3200b51d2ac4ed861689d9ce50f Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 31 Aug 2018 16:51:35 +0300 Subject: [PATCH 0688/1846] Add base.py as new base for strategies --- dexbot/strategies/base.py | 1079 +++++++++++++++++++++++++++++++++++++ 1 file changed, 1079 insertions(+) create mode 100644 dexbot/strategies/base.py diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py new file mode 100644 index 000000000..154bcbbf8 --- /dev/null +++ b/dexbot/strategies/base.py @@ -0,0 +1,1079 @@ +import datetime +import copy +import collections +import logging +import math +import time + +from dexbot.config import Config +from dexbot.storage import Storage +from dexbot.statemachine import StateMachine +from dexbot.helper import truncate + +from events import Events +import bitshares.exceptions +import bitsharesapi +import bitsharesapi.exceptions +from bitshares.account import Account +from bitshares.amount import Amount, Asset +from bitshares.instance import shared_bitshares_instance +from bitshares.market import Market +from bitshares.price import FilledOrder, Order, UpdateCallOrder + +# Number of maximum retries used to retry action before failing +MAX_TRIES = 3 + +""" Strategies need to specify their own configuration values, so each strategy can have a class method 'configure' + which returns a list of ConfigElement named tuples. + + Tuple fields as follows: + - Key: The key in the bot config dictionary that gets saved back to config.yml + - Type: "int", "float", "bool", "string" or "choice" + - Default: The default value, must be same type as the Type defined + - Title: Name shown to the user, preferably not too long + - Description: Comments to user, full sentences encouraged + - Extra: + :int: a (min, max, suffix) tuple + :float: a (min, max, precision, suffix) tuple + :string: a regular expression, entries must match it, can be None which equivalent to .* + :bool, ignored + :choice: a list of choices, choices are in turn (tag, label) tuples. + labels get presented to user, and tag is used as the value saved back to the config dict +""" +ConfigElement = collections.namedtuple('ConfigElement', 'key type default title description extra') + + +class StrategyBase(Storage, StateMachine, Events): + """ A strategy based on this class is intended to work in one market. This class contains + most common methods needed by the strategy. + + All prices are passed and returned as BASE/QUOTE. + (In the BREAD:USD market that would be USD/BREAD, 2.5 USD / 1 BREAD). + - Buy orders reserve BASE + - Sell orders reserve QUOTE + + Todo: This is copy / paste from old, update this if needed! + Strategy inherits: + * :class:`dexbot.storage.Storage` : Stores data to sqlite database + * :class:`dexbot.statemachine.StateMachine` + * ``Events`` + + Todo: This is copy / paste from old, update this if needed! + Available attributes: + * ``worker.bitshares``: instance of ´`bitshares.BitShares()`` + * ``worker.add_state``: Add a specific state + * ``worker.set_state``: Set finite state machine + * ``worker.get_state``: Change state of state machine + * ``worker.account``: The Account object of this worker + * ``worker.market``: The market used by this worker + * ``worker.orders``: List of open orders of the worker's account in the worker's market + * ``worker.balance``: List of assets and amounts available in the worker's account + * ``worker.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, Worker inherits :class:`dexbot.storage.Storage` + which allows to permanently store data in a sqlite database + using: + + ``worker["key"] = "value"`` + + .. note:: This applies a ``json.loads(json.dumps(value))``! + + 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. + """ + + __events__ = [ + 'onAccount', + 'onMarketUpdate', + 'onOrderMatched', + 'onOrderPlaced', + 'ontick', + 'onUpdateCallOrder', + 'error_onAccount', + 'error_onMarketUpdate', + 'error_ontick', + ] + + @classmethod + def configure(cls, return_base_config=True): + """ Return a list of ConfigElement objects defining the configuration values for this class. + + User interfaces should then generate widgets based on these values, gather data and save back to + the config dictionary for the worker. + + NOTE: When overriding you almost certainly will want to call the ancestor and then + add your config values to the list. + + :param return_base_config: bool: + :return: Returns a list of config elements + """ + + # Common configs + base_config = [ + ConfigElement("account", "string", "", "Account", + "BitShares account name for the bot to operate with", + ""), + ConfigElement("market", "string", "USD:BTS", "Market", + "BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"", + r"[A-Z\.]+[:\/][A-Z\.]+"), + ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset', + 'Asset to be used to pay transaction fees', + r'[A-Z\.]+') + ] + + # Todo: Is there any case / strategy where the base config would NOT be needed, making this unnecessary? + if return_base_config: + return base_config + return [] + + def __init__(self, + worker_name, + config=None, + on_account=None, + on_order_matched=None, + on_order_placed=None, + on_market_update=None, + on_update_call_order=None, + ontick=None, + bitshares_instance=None, + *args, + **kwargs): + + # BitShares instance + self.bitshares = bitshares_instance or shared_bitshares_instance() + + # Storage + Storage.__init__(self, worker_name) + + # Statemachine + StateMachine.__init__(self, worker_name) + + # Events + Events.__init__(self) + + if ontick: + self.ontick += ontick + if on_market_update: + self.onMarketUpdate += on_market_update + if on_account: + self.onAccount += on_account + if on_order_matched: + self.onOrderMatched += on_order_matched + if on_order_placed: + self.onOrderPlaced += on_order_placed + if on_update_call_order: + self.onUpdateCallOrder += on_update_call_order + + # Redirect this event to also call order placed and order matched + self.onMarketUpdate += self._callbackPlaceFillOrders + + if config: + self.config = config + else: + self.config = config = Config.get_worker_config_file(worker_name) + + # Get worker's parameters from the config + self.worker = config["workers"][worker_name] + + # Get Bitshares account and market for this worker + self._account = Account(self.worker["account"], full=True, bitshares_instance=self.bitshares) + self._market = Market(config["workers"][worker_name]["market"], bitshares_instance=self.bitshares) + + # Recheck flag - Tell the strategy to check for updated orders + self.recheck_orders = False + + # Set fee asset + fee_asset_symbol = self.worker.get('fee_asset') + + if fee_asset_symbol: + try: + self.fee_asset = Asset(fee_asset_symbol) + except bitshares.exceptions.AssetDoesNotExistsException: + self.fee_asset = Asset('1.3.0') + else: + # If there is no fee asset, use BTS + self.fee_asset = Asset('1.3.0') + + # 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 will be reset to False after reset only + self.disabled = False + + # Order expiration time in seconds + self.expiration = 60 * 60 * 24 * 365 * 5 + + # A private logger that adds worker identify data to the LogRecord + self.log = logging.LoggerAdapter( + logging.getLogger('dexbot.per_worker'), + { + 'worker_name': worker_name, + 'account': self.worker['account'], + 'market': self.worker['market'], + 'is_disabled': lambda: self.disabled + } + ) + + self.orders_log = logging.LoggerAdapter( + logging.getLogger('dexbot.orders_log'), {} + ) + + def _calculate_center_price(self, suppress_errors=False): + """ + + :param suppress_errors: + :return: + """ + # Todo: Add documentation + ticker = self.market.ticker() + highest_bid = ticker.get("highestBid") + lowest_ask = ticker.get("lowestAsk") + + if highest_bid is None or highest_bid == 0.0: + 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: + if not suppress_errors: + self.log.critical( + "Cannot estimate center price, there is no lowest ask." + ) + self.disabled = True + return None + + center_price = highest_bid['price'] * math.sqrt(lowest_ask['price'] / highest_bid['price']) + return center_price + + def _callbackPlaceFillOrders(self, d): + """ This method distinguishes notifications caused by Matched orders from those caused by placed orders + Todo: can this be renamed to _instantFill()? + """ + # Todo: Add documentation + if isinstance(d, FilledOrder): + self.onOrderMatched(d) + elif isinstance(d, Order): + self.onOrderPlaced(d) + elif isinstance(d, UpdateCallOrder): + self.onUpdateCallOrder(d) + else: + pass + + def _cancel_orders(self, orders): + """ + + :param orders: + :return: + """ + # Todo: Add documentation + try: + self.retry_action( + self.bitshares.cancel, + orders, account=self.account, fee_asset=self.fee_asset['id'] + ) + except bitsharesapi.exceptions.UnhandledRPCError as exception: + if str(exception).startswith('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: + self.log.exception("Unable to cancel order") + except bitshares.exceptions.MissingKeyError: + self.log.exception('Unable to cancel order(s), private key missing.') + + return True + + def account_total_value(self, return_asset): + """ Returns the total value of the account in given asset + + :param string | return_asset: Balance is returned as this asset + :return: float: Value of the account in one asset + """ + total_value = 0 + + # Total balance calculation + for balance in self.balances: + if balance['symbol'] != return_asset: + # Convert to asset if different + total_value += self.convert_asset(balance['amount'], balance['symbol'], return_asset) + else: + total_value += balance['amount'] + + # Orders balance calculation + for order in self.all_own_orders: + updated_order = self.get_updated_order(order['id']) + + if not order: + continue + if updated_order['base']['symbol'] == return_asset: + total_value += updated_order['base']['amount'] + else: + total_value += self.convert_asset( + updated_order['base']['amount'], + updated_order['base']['symbol'], + return_asset + ) + + return total_value + + def balance(self, asset, fee_reservation=False): + """ Return the balance of your worker's account for a specific asset + + :param bool | fee_reservation: + :return: Balance of specific asset + """ + # Todo: Add documentation + return self._account.balance(asset) + + def calculate_center_price(self, center_price=None, asset_offset=False, spread=None, + order_ids=None, manual_offset=0, suppress_errors=False): + # Todo: Fix comment + """ Calculate center price which shifts based on available funds + """ + if center_price is None: + # No center price was given so we simply calculate the center price + calculated_center_price = self._calculate_center_price(suppress_errors) + else: + # 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 center_price: + calculated_center_price = center_price + + if asset_offset: + total_balance = self.get_allocated_assets(order_ids) + total = (total_balance['quote'] * calculated_center_price) + total_balance['base'] + + if not total: # Prevent division by zero + balance = 0 + else: + # Returns a value between -1 and 1 + balance = (total_balance['base'] / total) * 2 - 1 + + if balance < 0: + # With less of base asset center price should be offset downward + calculated_center_price = calculated_center_price / math.sqrt(1 + spread * (balance * -1)) + elif balance > 0: + # With more of base asset center price will be offset upwards + calculated_center_price = calculated_center_price * math.sqrt(1 + spread * balance) + else: + calculated_center_price = calculated_center_price + + # Calculate final_offset_price if manual center price offset is given + if manual_offset: + calculated_center_price = calculated_center_price + (calculated_center_price * manual_offset) + + return calculated_center_price + + 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 calculate_worker_value(self, unit_of_measure, refresh=True): + """ Returns the combined value of allocated and available QUOTE and BASE, measured in "unit_of_measure". + + :param unit_of_measure: + :param refresh: + :return: + """ + # Todo: Insert logic here + + def cancel_all_orders(self): + """ Cancel all orders of the worker's account + """ + self.log.info('Canceling all orders') + + if self.all_own_orders: + self.cancel(self.all_own_orders) + + self.log.info("Orders canceled") + + def cancel_orders(self, orders, batch_only=False): + """ Cancel specific order(s) + + :param list | orders: List of orders to cancel + :param bool | batch_only: Try cancel orders only in batch mode without one-by-one fallback + :return: + """ + # Todo: Add documentation + if not isinstance(orders, (list, set, tuple)): + orders = [orders] + + orders = [order['id'] for order in orders if 'id' in order] + + success = self._cancel_orders(orders) + if not success and batch_only: + return False + if not success and len(orders) > 1 and not batch_only: + # One of the order cancels failed, cancel the orders one by one + for order in orders: + self._cancel_orders(order) + return True + + def count_asset(self, order_ids=None, return_asset=False, refresh=True): + """ Returns the combined amount of the given order ids and the account balance + The amounts are returned in quote and base assets of the market + + :param list | order_ids: list of order ids to be added to the balance + :param bool | return_asset: true if returned values should be Amount instances + :return: dict with keys quote and base + Todo: When would we want the sum of a subset of orders? Why order_ids? Maybe just specify asset? + """ + quote = 0 + base = 0 + quote_asset = self.market['quote']['id'] + base_asset = self.market['base']['id'] + + # Total balance calculation + for balance in self.balances: + if balance.asset['id'] == quote_asset: + quote += balance['amount'] + elif balance.asset['id'] == base_asset: + base += balance['amount'] + + if order_ids is None: + # Get all orders from Blockchain + order_ids = [order['id'] for order in self.current_market_own_orders] + if order_ids: + 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 get_allocated_assets(self, order_ids, return_asset=False, refresh=True): + # Todo: + """ Returns the amount of QUOTE and BASE allocated in orders, and that do not show up in available balance + + :param order_ids: + :param return_asset: + :param refresh: + :return: + """ + # Todo: Add documentation + 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} + + def get_lowest_market_sell(self, refresh=False): + """ Returns the lowest sell order that is not own, regardless of order size. + + :param refresh: + :return: + """ + # Todo: Insert logic here + + def get_highest_market_buy(self, refresh=False): + """ Returns the highest buy order not owned by worker account, regardless of order size. + + :param refresh: + :return: + """ + # Todo: Insert logic here + + def get_lowest_own_sell(self, refresh=False): + """ Returns lowest own sell order. + + :param refresh: + :return: + """ + # Todo: Insert logic here + + def get_highest_own_buy(self, refresh=False): + """ Returns highest own buy order. + + :param refresh: + :return: + """ + # Todo: Insert logic here + + def get_price_for_amount_buy(self, amount=None, refresh=False): + """ Returns the cumulative price for which you could buy the specified amount of QUOTE. + This method must take into account market fee. + + :param amount: + :param refresh: + :return: + """ + # Todo: Insert logic here + + def get_price_for_amount_sell(self, amount=None, refresh=False): + """ Returns the cumulative price for which you could sell the specified amount of QUOTE + + :param amount: + :param refresh: + :return: + """ + # Todo: Insert logic here + + def get_market_center_price(self, depth=0, refresh=False): + """ Returns the center price of market including own orders. + + :param depth: 0 = calculate from closest opposite orders. non-zero = calculate from specified depth (quote or base?) + :param refresh: + :return: + """ + # Todo: Insert logic here + + def get_external_price(self, source): + """ Returns the center price of market including own orders. + + :param source: + :return: + """ + # Todo: Insert logic here + + def get_market_spread(self, method, refresh=False): + """ Get spread from closest opposite orders, including own. + + :param method: + :param refresh: + :return: float: Market spread in BASE + """ + # Todo: Insert logic here + + def get_own_spread(self, method, refresh=False): + """ Returns the difference between own closest opposite orders. + lowest_own_sell_price / highest_own_buy_price - 1 + + :param method: + :param refresh: + :return: + """ + # Todo: Insert logic here + + def get_order_creation_fee(self, fee_asset): + """ Returns the cost of creating an order in the asset specified + + :param fee_asset: QUOTE, BASE, BTS, or any other + :return: + """ + # Todo: Insert logic here + + def get_order_cancellation_fee(self, fee_asset): + """ Returns the order cancellation fee in the specified asset. + :param fee_asset: + :return: + """ + # Todo: Insert logic here + + def get_market_fee(self, asset): + """ Returns the fee percentage for buying specified asset. + :param asset: + :return: Fee percentage in decimal form (0.025) + """ + # Todo: Insert logic here + + def get_own_buy_orders(self, sort=None, orders=None): + """ Return own buy orders from list of orders. Can be used to pick buy orders from a list + that is not up to date with the blockchain data. + + :param string | sort: DESC or ASC will sort the orders accordingly, default None. + :param list | orders: List of orders. If None given get all orders from Blockchain. + :return list | buy_orders: List of buy orders only. + """ + buy_orders = [] + + if not orders: + orders = self.current_market_own_orders + + # Find buy orders + for order in orders: + if not self.is_sell_order(order): + buy_orders.append(order) + if sort: + buy_orders = self.sort_orders(buy_orders, sort) + + return buy_orders + + def get_own_sell_orders(self, sort=None, orders=None): + """ Return own sell orders from list of orders. Can be used to pick sell orders from a list + that is not up to date with the blockchain data. + + :param string | sort: DESC or ASC will sort the orders accordingly, default None. + :param list | orders: List of orders. If None given get all orders from Blockchain. + :return list | sell_orders: List of sell orders only. + """ + sell_orders = [] + + if not orders: + orders = self.current_market_own_orders + + # Find sell orders + for order in orders: + if self.is_sell_order(order): + sell_orders.append(order) + + if sort: + sell_orders = self.sort_orders(sell_orders, sort) + + return sell_orders + + def get_updated_order(self, order_id): + """ Tries to get the updated order from the API. Returns None if the order doesn't exist + + :param str|dict order_id: blockchain object id of the order + can be an order dict with the id key in it + """ + if isinstance(order_id, dict): + order_id = order_id['id'] + + # Get the limited order by id + order = None + for limit_order in self.account['limit_orders']: + if order_id == limit_order['id']: + order = limit_order + break + else: + return order + + order = self.get_updated_limit_order(order) + return Order(order, bitshares_instance=self.bitshares) + + def enhance_center_price(self, reference=None, manual_offset=False, balance_based_offset=False, + moving_average=0, weighted_average=0): + """ Returns the passed reference price shifted up or down based on arguments. + + :param float | reference: Center price to enhance + :param bool | manual_offset: + :param bool | balance_based_offset: + :param int or float | moving_average: + :param int or float | weighted_average: + :return: + """ + # Todo: Insert logic here + + def execute_bundle(self): + # Todo: Is this still needed? + # Apparently old naming was "execute", and was used by walls strategy. + """ Execute a bundle of operations + """ + self.bitshares.blocking = "head" + r = self.bitshares.txbuffer.broadcast() + self.bitshares.blocking = False + return r + + def is_buy_order(self, order): + """ Checks if the order is a buy order. Returns False if not. + + :param order: Buy / Sell order + :return: + """ + if order['base']['symbol'] == self.market['base']['symbol']: + return True + return False + + def is_sell_order(self, order): + """ Checks if the order is Sell order. Returns False if Buy order + + :param order: Buy / Sell order + :return: bool: True = Sell order, False = Buy order + """ + if order['base']['symbol'] != self.market['base']['symbol']: + return True + return False + + def is_current_market(self, base_asset_id, quote_asset_id): + """ Returns True if given asset id's are of the current market + + :return: bool: True = Current market, False = Not current market + """ + if quote_asset_id == self.market['quote']['id']: + if base_asset_id == self.market['base']['id']: + return True + return False + # Todo: Should we return true if market is opposite? + if quote_asset_id == self.market['base']['id']: + if base_asset_id == self.market['quote']['id']: + return True + return False + return False + + def pause_worker(self): + """ Pause the worker + + Note: By default, just call cancel_all(), strategies may override this method. + """ + # Cancel all orders from the market + self.cancel_all() + + # Removes worker's orders from local database + self.clear_orders() + + def purge_all_worker_data(self): + """ Clear all the worker data from the database and cancel all orders + """ + # Removes worker's orders from local database + self.clear_orders() + + # Cancel all orders from the market + self.cancel_all() + + # Finally clear all worker data from the database + self.clear() + + def place_market_buy_order(self, amount, price, return_none=False, *args, **kwargs): + """ Places a buy order in the market + + :param float | amount: Order amount in QUOTE + :param float | price: Order price in BASE + :param bool | return_none: + :param args: + :param kwargs: + :return: + """ + symbol = self.market['base']['symbol'] + precision = self.market['base']['precision'] + base_amount = truncate(price * amount, precision) + + # Don't try to place an order of size 0 + if not base_amount: + self.log.critical('Trying to buy 0') + self.disabled = True + return None + + # Make sure we have enough balance for the order + if self.balance(self.market['base']) < base_amount: + self.log.critical("Insufficient buy balance, needed {} {}".format(base_amount, symbol)) + self.disabled = True + return None + + self.log.info('Placing a buy order for {} {} @ {}'.format(base_amount, symbol, round(price, 8))) + + # Place the order + buy_transaction = self.retry_action( + self.market.buy, + price, + Amount(amount=amount, asset=self.market["quote"]), + account=self.account.name, + expiration=self.expiration, + returnOrderId="head", + fee_asset=self.fee_asset['id'], + *args, + **kwargs + ) + + self.log.debug('Placed buy order {}'.format(buy_transaction)) + buy_order = self.get_order(buy_transaction['orderid'], return_none=return_none) + if buy_order and 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 + + def place_market_sell_order(self, amount, price, return_none=False, *args, **kwargs): + """ Places a sell order in the market + + :param float | amount: Order amount in QUOTE + :param float | price: Order price in BASE + :param bool | return_none: + :param args: + :param kwargs: + :return: + """ + symbol = self.market['quote']['symbol'] + precision = self.market['quote']['precision'] + quote_amount = truncate(amount, precision) + + # Don't try to place an order of size 0 + if not quote_amount: + self.log.critical('Trying to sell 0') + self.disabled = True + return None + + # Make sure we have enough balance for the order + if self.balance(self.market['quote']) < quote_amount: + self.log.critical("Insufficient sell balance, needed {} {}".format(amount, symbol)) + self.disabled = True + return None + + self.log.info('Placing a sell order for {} {} @ {}'.format(quote_amount, symbol, round(price, 8))) + + # Place the order + sell_transaction = self.retry_action( + self.market.sell, + price, + Amount(amount=amount, asset=self.market["quote"]), + account=self.account.name, + expiration=self.expiration, + returnOrderId="head", + fee_asset=self.fee_asset['id'], + *args, + **kwargs + ) + + self.log.debug('Placed sell order {}'.format(sell_transaction)) + sell_order = self.get_order(sell_transaction['orderid'], return_none=return_none) + if sell_order and 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 restore_order(self, order): + """ If an order is partially or completely filled, this will make a new order of original size and price. + + :param order: + :return: + """ + # Todo: Insert logic here + + def retry_action(self, action, *args, **kwargs): + """ Perform an action, and if certain suspected-to-be-spurious grapheme 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 + + :param action: + :return: + """ + tries = 0 + while True: + try: + return action(*args, **kwargs) + except bitsharesapi.exceptions.UnhandledRPCError as exception: + if "Assert Exception: amount_to_sell.amount > 0" in str(exception): + if tries > MAX_TRIES: + raise + else: + tries += 1 + self.log.warning("Ignoring: '{}'".format(str(exception))) + self.bitshares.txbuffer.clear() + self.account.refresh() + time.sleep(2) + elif "now <= trx.expiration" in str(exception): # Usually loss of sync to blockchain + if tries > MAX_TRIES: + raise + else: + tries += 1 + self.log.warning("retrying on '{}'".format(str(exception))) + self.bitshares.txbuffer.clear() + time.sleep(6) # Wait at least a BitShares block + else: + raise + + def write_order_log(self, worker_name, order): + """ + + :param string | worker_name: Name of the worker + :param object | order: Order that was traded + """ + # Todo: Add documentation + operation_type = 'TRADE' + + if order['base']['symbol'] == self.market['base']['symbol']: + base_symbol = order['base']['symbol'] + base_amount = -order['base']['amount'] + quote_symbol = order['quote']['symbol'] + quote_amount = order['quote']['amount'] + else: + base_symbol = order['quote']['symbol'] + base_amount = order['quote']['amount'] + quote_symbol = order['base']['symbol'] + quote_amount = -order['base']['amount'] + + message = '{};{};{};{};{};{};{};{}'.format( + worker_name, + order['id'], + operation_type, + base_symbol, + base_amount, + quote_symbol, + quote_amount, + datetime.datetime.now().isoformat() + ) + + self.orders_log.info(message) + + @property + def account(self): + """ Return the full account as :class:`bitshares.account.Account` object! + Can be refreshed by using ``x.refresh()`` + + :return: object | Account + """ + return self._account + + @property + def balances(self): + """ Returns all the balances of the account assigned for the worker. + + :return: list: Balances in list where each asset is in their own Amount object + """ + return self._account.balances + + @property + def all_own_orders(self, refresh=True): + """ Return the worker's open orders in all markets + + :param bool | refresh: Use most resent data + :return: list: List of Order objects + """ + # Refresh account data + if refresh: + self.account.refresh() + + return [order for order in self.account.openorders] + + @property + def current_market_own_orders(self, refresh=False): + """ Return the account's open orders in the current market + + :return: list: List of Order objects + """ + orders = [] + + # Refresh account data + if refresh: + self.account.refresh() + + for order in self.account.openorders: + if self.worker["market"] == order.market and self.account.openorders: + orders.append(order) + + return orders + + @property + def get_updated_orders(self): + """ Returns all open orders as updated orders + Todo: What exactly? When orders are needed who wants out of date info? + """ + self.account.refresh() + + limited_orders = [] + for order in self.account['limit_orders']: + base_asset_id = order['sell_price']['base']['asset_id'] + quote_asset_id = order['sell_price']['quote']['asset_id'] + # Check if the order is in the current market + if not self.is_current_market(base_asset_id, quote_asset_id): + continue + + limited_orders.append(self.get_updated_limit_order(order)) + + return [Order(order, bitshares_instance=self.bitshares) for order in limited_orders] + + @property + def market(self): + """ Return the market object as :class:`bitshares.market.Market` + """ + return self._market + + @staticmethod + def convert_asset(from_value, from_asset, to_asset, refresh=False): + """ Converts asset to another based on the latest market value + + :param float | from_value: Amount of the input asset + :param string | from_asset: Symbol of the input asset + :param string | to_asset: Symbol of the output asset + :param bool | refresh: + :return: float Asset converted to another asset as float value + """ + market = Market('{}/{}'.format(from_asset, to_asset)) + ticker = market.ticker() + latest_price = ticker.get('latest', {}).get('price', None) + return from_value * latest_price + + @staticmethod + def get_original_order(order_id, return_none=True): + """ Returns the Order object for the order_id + + :param dict | order_id: blockchain object id of the order can be an order 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 + :return: + """ + if not order_id: + return None + + if 'id' in order_id: + order_id = order_id['id'] + + order = Order(order_id) + + if return_none and order['deleted']: + return None + + return order + + @staticmethod + def get_updated_limit_order(limit_order): + """ Returns a modified limit_order so that when passed to Order class, + will return an Order object with updated amount values + + :param limit_order: an item of Account['limit_orders'] + :return: Order + Todo: When would we not want an updated order? + """ + order = copy.deepcopy(limit_order) + price = order['sell_price']['base']['amount'] / order['sell_price']['quote']['amount'] + base_amount = order['for_sale'] + quote_amount = base_amount / price + order['sell_price']['base']['amount'] = base_amount + order['sell_price']['quote']['amount'] = quote_amount + return order + + @staticmethod + def purge_all_local_worker_data(worker_name): + # Todo: Confirm this being correct + """ Removes worker's data and orders from local sqlite database + + :param worker_name: Name of the worker to be removed + """ + Storage.clear_worker_data(worker_name) + + @staticmethod + def sort_orders(orders, sort='DESC'): + """ Return list of orders sorted ascending or descending + + :param list | orders: list of orders to be sorted + :param string | sort: ASC or DESC. Default DESC + :return list: Sorted list of orders + """ + if sort.upper() == 'ASC': + reverse = False + elif sort.upper() == 'DESC': + reverse = True + else: + return None + + # Sort orders by price + return sorted(orders, key=lambda order: order['price'], reverse=reverse) From ebea5e634eff6323fea675bca3e20f3865ec462a Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 31 Aug 2018 21:59:04 +0500 Subject: [PATCH 0689/1846] Don't turn bootstrap off whether there is no opposite-side orders --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 2db9170ac..e5e49fb4d 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -354,7 +354,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): self.actual_spread = (lowest_sell_price / highest_buy_price) - 1 if self.actual_spread >= self.target_spread + self.increment: - if self.quote_balance <= self.quote_asset_threshold and self.bootstrapping: + if self.quote_balance <= self.quote_asset_threshold and self.bootstrapping and self.sell_orders: """ During the bootstrap we're fist placing orders of some amounts, than we are reaching target spread and then turning bootstrap flag off and starting to allocate remaining balance by gradually increasing order sizes. After bootstrap is complete and following order size @@ -460,7 +460,7 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): self.actual_spread = (lowest_sell_price / highest_buy_price) - 1 if self.actual_spread >= self.target_spread + self.increment: - if self.base_balance <= self.base_asset_threshold and self.bootstrapping: + if self.base_balance <= self.base_asset_threshold and self.bootstrapping and self.buy_orders: self.log.debug('Turning bootstrapping off: actual_spread > target_spread, and not having ' 'opposite-side balance') self.bootstrapping = False From e88c41391e5614a2dd18b07c437f725f6400ac6c Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 31 Aug 2018 22:48:55 +0500 Subject: [PATCH 0690/1846] Tune precision in log messages --- dexbot/strategies/staggered_orders.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index e5e49fb4d..45a305df1 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -830,7 +830,7 @@ def place_higher_buy_order(self, order, place_order=True, allow_partial=False, b if base_amount > self.base_balance['amount']: if place_order and not allow_partial: - self.log.debug('Not enough balance to place_higher_buy_order; need/avail: {}/{}' + self.log.debug('Not enough balance to place_higher_buy_order; need/avail: {:.8f}/{:.8f}' .format(base_amount, self.base_balance['amount'])) place_order = False elif allow_partial: @@ -858,7 +858,7 @@ def place_higher_sell_order(self, order, place_order=True, allow_partial=False): if amount > self.quote_balance['amount']: if place_order and not allow_partial: - self.log.debug('Not enough balance to place_higher_sell_order; need/avail: {}/{}' + self.log.debug('Not enough balance to place_higher_sell_order; need/avail: {:.8f}/{:.8f}' .format(amount, self.quote_balance['amount'])) place_order = False elif allow_partial: @@ -889,7 +889,7 @@ def place_lower_buy_order(self, order, place_order=True, allow_partial=False): if base_amount > self.base_balance['amount']: if place_order and not allow_partial: - self.log.debug('Not enough balance to place_lower_buy_order; need/avail: {}/{}' + self.log.debug('Not enough balance to place_lower_buy_order; need/avail: {:.8f}/{:.8f}' .format(base_amount, self.base_balance['amount'])) place_order = False elif allow_partial: @@ -935,7 +935,7 @@ def place_lower_sell_order(self, order, place_order=True, allow_partial=False, b if amount > self.quote_balance['amount']: if place_order and not allow_partial: - self.log.debug('Not enough balance to place_lower_sell_order; need/avail: {}/{}' + self.log.debug('Not enough balance to place_lower_sell_order; need/avail: {:.8f}/{:.8f}' .format(amount, self.quote_balance['amount'])) place_order = False elif allow_partial: From 6746c9a6b186b6a521a4d8141d5732e16a0c492a Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 31 Aug 2018 22:40:01 +0500 Subject: [PATCH 0691/1846] Refactor fee reserve Now we're reserving only specified fee asset. This commit also fixes the fee reserve bug when we're trading non-BTS markets like bitUSD/XXX.BTC. Due to bug in the pybitshares, fee reserve may exceed actual asset balance. --- dexbot/strategies/staggered_orders.py | 34 ++++++++++++++------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 45a305df1..5a7e11d4c 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1,6 +1,7 @@ import math from datetime import datetime, timedelta from bitshares.market import Market +from bitshares.asset import Asset from dexbot.basestrategy import BaseStrategy, ConfigElement from dexbot.qt_queue.idle_queue import idle_add @@ -112,8 +113,6 @@ def __init__(self, *args, **kwargs): self.buy_orders = [] self.sell_orders = [] self.actual_spread = self.target_spread + 1 - self.base_fee_reserve = None - self.quote_fee_reserve = None self.quote_total_balance = 0 self.base_total_balance = 0 self.quote_balance = 0 @@ -196,6 +195,9 @@ def maintain_strategy(self, *args, **kwargs): self.log_maintenance_time() return + # Get ticker data + self.ticker = self.market.ticker() + # BASE asset check if self.base_balance > self.base_asset_threshold: base_allocated = False @@ -258,23 +260,23 @@ def refresh_balances(self): self.base_balance = account_balances['base'] self.quote_balance = account_balances['quote'] - # Reserve transaction fee equivalent in BTS - self.ticker = self.market.ticker() - - # FIXME: this is a temporal workaround for https://github.com/bitshares/python-bitshares/issues/138 - if self.market['quote']['id'] == '1.3.0': - temp_market = Market(base=self.market['quote'], quote=self.market['base'], - bitshare_instance=self.bitshares) + # Todo: order_creation_fee(BTS) = 0.01 for now. Reserve fee for 200 orders + fee_reserve = 0.01 * 200 + if self.fee_asset['id'] == '1.3.0': + fee_reserve = fee_reserve + else: + temp_market = Market(base=Asset('1.3.0'), quote=self.fee_asset) ticker = temp_market.ticker() + """ Inverted CER gives us Price like 10 BTS/FEE_ASSET, where BTS is quote + We're using invert() as workaround for https://github.com/bitshares/python-bitshares/issues/138 + """ core_exchange_rate = ticker['core_exchange_rate'].invert() - else: - core_exchange_rate = self.ticker['core_exchange_rate'] - # Todo: order_creation_fee(BTS) = 0.01 for now - self.quote_fee_reserve = 0.01 * core_exchange_rate['quote']['amount'] * 100 - self.base_fee_reserve = 0.01 * core_exchange_rate['base']['amount'] * 100 + fee_reserve = fee_reserve * core_exchange_rate['base']['amount'] - self.quote_balance['amount'] = self.quote_balance['amount'] - self.quote_fee_reserve - self.base_balance['amount'] = self.base_balance['amount'] - self.base_fee_reserve + if self.fee_asset['id'] == self.market['base']['id']: + self.base_balance['amount'] = self.base_balance['amount'] - fee_reserve + elif self.fee_asset['id'] == self.market['quote']['id']: + self.quote_balance['amount'] = self.quote_balance['amount'] - fee_reserve # Balance per asset from orders order_ids = [order['id'] for order in self.orders] From 10c4f4563b7607dc41df2dd88c15c3b255d03ad8 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 1 Sep 2018 22:03:07 +0500 Subject: [PATCH 0692/1846] Properly limit order amounts for valley mode --- dexbot/strategies/staggered_orders.py | 32 ++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 5a7e11d4c..dfad8c002 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -391,9 +391,20 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): else: # Place order limited by size of the opposite-side order lowest_sell_order = self.sell_orders[0] - limit = lowest_sell_order['quote']['amount'] - self.log.debug('Limiting buy order base by opposite order base asset amount: {}'.format(limit)) - self.place_higher_buy_order(highest_buy_order, base_limit=limit, allow_partial=False) + if self.mode == 'mountain': + limit = lowest_sell_order['quote']['amount'] + self.place_higher_buy_order(highest_buy_order, base_limit=limit, allow_partial=False) + self.log.debug('Limiting buy order base by opposite order BASE asset amount: {}'.format( + limit)) + elif self.mode == 'valley': + limit = lowest_sell_order['base']['amount'] + self.log.debug('Limiting buy order base by opposite order QUOTE asset amount: {}'.format( + limit)) + self.place_higher_buy_order(highest_buy_order, limit=limit, allow_partial=False) + else: + self.log.warning('Using fallback order limiting') + limit = lowest_sell_order['quote']['amount'] + self.place_higher_buy_order(highest_buy_order, base_limit=limit, allow_partial=False) elif not self.sell_orders: # Do not try to do anything than placing higher buy whether there is no sell orders return @@ -474,9 +485,18 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): else: # Place order limited by opposite-side order highest_buy_order = self.buy_orders[0] - limit = self.buy_orders[0]['quote']['amount'] - self.log.debug('Limiting sell order by opposite order quote: {}'.format(limit)) - self.place_lower_sell_order(lowest_sell_order, limit=limit, allow_partial=False) + if self.mode == 'mountain': + limit = highest_buy_order['quote']['amount'] + self.log.debug('Limiting sell order by opposite order QUOTE asset amount: {}'.format(limit)) + self.place_lower_sell_order(lowest_sell_order, limit=limit, allow_partial=False) + elif self.mode == 'valley': + limit = highest_buy_order['base']['amount'] + self.log.debug('Limiting sell order by opposite order BASE asset amount: {}'.format(limit)) + self.place_lower_sell_order(lowest_sell_order, base_limit=limit, allow_partial=False) + else: + self.log.warning('Using fallback order limiting') + limit = highest_buy_order['quote']['amount'] + self.place_lower_sell_order(lowest_sell_order, limit=limit, allow_partial=False) elif not self.buy_orders: # Do not try to do anything than placing lower sell whether there is no buy orders return From d66bb04cbfc7814893f12d5897e8f3526cf23a5d Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 3 Sep 2018 15:32:21 +0300 Subject: [PATCH 0693/1846] WIP Add logic to functions --- dexbot/strategies/base.py | 185 +++++++++++++++++++++++++++----------- 1 file changed, 135 insertions(+), 50 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 154bcbbf8..aa71b38e4 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -129,13 +129,13 @@ def configure(cls, return_base_config=True): return [] def __init__(self, - worker_name, + name, config=None, - on_account=None, - on_order_matched=None, - on_order_placed=None, - on_market_update=None, - on_update_call_order=None, + onAccount=None, + onOrderMatched=None, + onOrderPlaced=None, + onMarketUpdate=None, + onUpdateCallOrder=None, ontick=None, bitshares_instance=None, *args, @@ -145,26 +145,26 @@ def __init__(self, self.bitshares = bitshares_instance or shared_bitshares_instance() # Storage - Storage.__init__(self, worker_name) + Storage.__init__(self, name) # Statemachine - StateMachine.__init__(self, worker_name) + StateMachine.__init__(self, name) # Events Events.__init__(self) if ontick: self.ontick += ontick - if on_market_update: - self.onMarketUpdate += on_market_update - if on_account: - self.onAccount += on_account - if on_order_matched: - self.onOrderMatched += on_order_matched - if on_order_placed: - self.onOrderPlaced += on_order_placed - if on_update_call_order: - self.onUpdateCallOrder += on_update_call_order + if onMarketUpdate: + self.onMarketUpdate += onMarketUpdate + if onAccount: + self.onAccount += onAccount + if onOrderMatched: + self.onOrderMatched += onOrderMatched + if onOrderPlaced: + self.onOrderPlaced += onOrderPlaced + if onUpdateCallOrder: + self.onUpdateCallOrder += onUpdateCallOrder # Redirect this event to also call order placed and order matched self.onMarketUpdate += self._callbackPlaceFillOrders @@ -172,14 +172,14 @@ def __init__(self, if config: self.config = config else: - self.config = config = Config.get_worker_config_file(worker_name) + self.config = config = Config.get_worker_config_file(name) # Get worker's parameters from the config - self.worker = config["workers"][worker_name] + self.worker = config["workers"][name] # Get Bitshares account and market for this worker self._account = Account(self.worker["account"], full=True, bitshares_instance=self.bitshares) - self._market = Market(config["workers"][worker_name]["market"], bitshares_instance=self.bitshares) + self._market = Market(config["workers"][name]["market"], bitshares_instance=self.bitshares) # Recheck flag - Tell the strategy to check for updated orders self.recheck_orders = False @@ -209,7 +209,7 @@ def __init__(self, self.log = logging.LoggerAdapter( logging.getLogger('dexbot.per_worker'), { - 'worker_name': worker_name, + 'worker_name': name, 'account': self.worker['account'], 'market': self.worker['market'], 'is_disabled': lambda: self.disabled @@ -492,21 +492,37 @@ def get_allocated_assets(self, order_ids, return_asset=False, refresh=True): return {'quote': quote, 'base': base} - def get_lowest_market_sell(self, refresh=False): + def get_lowest_market_sell(self): """ Returns the lowest sell order that is not own, regardless of order size. - :param refresh: - :return: + :return: order or None: Lowest market sell order. """ - # Todo: Insert logic here + orders = self.market.orderbook(1) - def get_highest_market_buy(self, refresh=False): - """ Returns the highest buy order not owned by worker account, regardless of order size. + try: + order = orders['asks'][0] + self.log.info('Lowest market ask @ {}'.format(order.get('price'))) + except IndexError: + self.log.info('Market has no lowest ask.') + return None - :param refresh: - :return: + return order + + def get_highest_market_buy(self): + """ Returns the highest buy order that is not own, regardless of order size. + + :return: order or None: Highest market buy order. """ - # Todo: Insert logic here + orders = self.market.orderbook(1) + + try: + order = orders['bids'][0] + self.log.info('Highest market bid @ {}'.format(order.get('price'))) + except IndexError: + self.log.info('Market has no highest bid.') + return None + + return order def get_lowest_own_sell(self, refresh=False): """ Returns lowest own sell order. @@ -543,42 +559,107 @@ def get_price_for_amount_sell(self, amount=None, refresh=False): """ # Todo: Insert logic here - def get_market_center_price(self, depth=0, refresh=False): + def get_external_price(self, source): """ Returns the center price of market including own orders. - :param depth: 0 = calculate from closest opposite orders. non-zero = calculate from specified depth (quote or base?) - :param refresh: + :param source: :return: """ # Todo: Insert logic here - def get_external_price(self, source): - """ Returns the center price of market including own orders. + def get_market_ask(self, depth=0, moving_average=0, weighted_moving_average=0, refresh=False): + """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with moving average + or weighted moving average - :param source: + :param float | depth: + :param float | moving_average: + :param float | weighted_moving_average: + :param bool | refresh: :return: """ # Todo: Insert logic here - def get_market_spread(self, method, refresh=False): - """ Get spread from closest opposite orders, including own. + def get_market_bid(self, depth=0, moving_average=0, weighted_moving_average=0, refresh=False): + """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be sold, enhanced with moving average or + weighted moving average. - :param method: - :param refresh: - :return: float: Market spread in BASE + Depth = 0 means highest regardless of size + + :param float | depth: + :param float | moving_average: + :param float | weighted_moving_average: + :param bool | refresh: + :return: """ # Todo: Insert logic here - def get_own_spread(self, method, refresh=False): - """ Returns the difference between own closest opposite orders. - lowest_own_sell_price / highest_own_buy_price - 1 + def get_market_center_price(self, depth=0, refresh=False): + """ Returns the center price of market including own orders. - :param method: - :param refresh: + Depth: 0 = calculate from closest opposite orders. + Depth: non-zero = calculate from specified depth + + :param float | depth: + :param bool | refresh: :return: """ # Todo: Insert logic here + def get_market_spread(self, highest_market_buy_price=None, lowest_market_sell_price=None, + depth=0, refresh=False): + """ Returns the market spread %, including own orders, from specified depth, enhanced with moving average or + weighted moving average + + :param float | highest_market_buy_price: + :param float | lowest_market_sell_price: + :param float | depth: + :param bool | refresh: Use most resent data from Bitshares + :return: float or None: Market spread + """ + # Todo: Add depth + if refresh: + try: + # Try fetching orders from market + highest_market_buy_price = self.get_highest_own_buy().get('price') + lowest_market_sell_price = self.get_highest_own_buy().get('price') + except AttributeError: + # This error is given if there is no market buy or sell order + return None + else: + # If orders are given, use them instead newest data from the blockchain + highest_market_buy_price = highest_market_buy_price + lowest_market_sell_price = lowest_market_sell_price + + # Calculate market spread + market_spread = lowest_market_sell_price / highest_market_buy_price - 1 + return market_spread + + def get_own_spread(self, highest_own_buy_price=None, lowest_own_sell_price=None, depth=0, refresh=False): + """ Returns the difference between own closest opposite orders. + + :param float | highest_own_buy_price: + :param float | lowest_own_sell_price: + :param float | depth: Use most resent data from Bitshares + :param bool | refresh: + :return: float or None: Own spread + """ + # Todo: Add depth + if refresh: + try: + # Try fetching own orders + highest_own_buy_price = self.get_highest_market_buy().get('price') + lowest_own_sell_price = self.get_lowest_own_sell().get('price') + except AttributeError: + return None + else: + # If orders are given, use them instead newest data from the blockchain + highest_own_buy_price = highest_own_buy_price + lowest_own_sell_price = lowest_own_sell_price + + # Calculate actual spread + actual_spread = lowest_own_sell_price / highest_own_buy_price - 1 + return actual_spread + def get_order_creation_fee(self, fee_asset): """ Returns the cost of creating an order in the asset specified @@ -602,8 +683,10 @@ def get_market_fee(self, asset): # Todo: Insert logic here def get_own_buy_orders(self, sort=None, orders=None): + # Todo: I might combine this with the get_own_sell_orders and have 2 functions to call it with different returns """ Return own buy orders from list of orders. Can be used to pick buy orders from a list - that is not up to date with the blockchain data. + that is not up to date with the blockchain data. If list of orders is not passed, orders are fetched from + blockchain. :param string | sort: DESC or ASC will sort the orders accordingly, default None. :param list | orders: List of orders. If None given get all orders from Blockchain. @@ -625,7 +708,8 @@ def get_own_buy_orders(self, sort=None, orders=None): def get_own_sell_orders(self, sort=None, orders=None): """ Return own sell orders from list of orders. Can be used to pick sell orders from a list - that is not up to date with the blockchain data. + that is not up to date with the blockchain data. If list of orders is not passed, orders are fetched from + blockchain. :param string | sort: DESC or ASC will sort the orders accordingly, default None. :param list | orders: List of orders. If None given get all orders from Blockchain. @@ -647,6 +731,7 @@ def get_own_sell_orders(self, sort=None, orders=None): return sell_orders def get_updated_order(self, order_id): + # Todo: This needed? """ Tries to get the updated order from the API. Returns None if the order doesn't exist :param str|dict order_id: blockchain object id of the order @@ -729,7 +814,7 @@ def is_current_market(self, base_asset_id, quote_asset_id): def pause_worker(self): """ Pause the worker - Note: By default, just call cancel_all(), strategies may override this method. + Note: By default pause cancels orders, but this can be overridden by strategy """ # Cancel all orders from the market self.cancel_all() From b1b119b17e9fc7533586acdf4813a14e71ffa9ed Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 6 Sep 2018 10:29:08 +0300 Subject: [PATCH 0694/1846] Add documentation to truncate() in helper.py --- dexbot/helper.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dexbot/helper.py b/dexbot/helper.py index 8812cd7c4..832ddba6e 100644 --- a/dexbot/helper.py +++ b/dexbot/helper.py @@ -35,6 +35,10 @@ def remove(path): def truncate(number, decimals): """ Change the decimal point of a number without rounding + + :param float | number: A float number to be cut down + :param int | decimals: Number of decimals to be left to the float number + :return: Price with specified precision """ return math.floor(number * 10 ** decimals) / 10 ** decimals From 9cccdd87e35ea431e685b18b5537858996531d54 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 6 Sep 2018 10:31:43 +0300 Subject: [PATCH 0695/1846] Refactor double to single quotes in ConfigElemet --- dexbot/strategies/base.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index aa71b38e4..f34a7d3e1 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -112,12 +112,12 @@ def configure(cls, return_base_config=True): # Common configs base_config = [ - ConfigElement("account", "string", "", "Account", - "BitShares account name for the bot to operate with", - ""), - ConfigElement("market", "string", "USD:BTS", "Market", - "BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"", - r"[A-Z\.]+[:\/][A-Z\.]+"), + ConfigElement('account', 'string', '', 'Account', + 'BitShares account name for the bot to operate with', + ''), + ConfigElement('market', 'string', 'USD:BTS', 'Market', + 'BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"', + r'[A-Z\.]+[:\/][A-Z\.]+'), ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset', 'Asset to be used to pay transaction fees', r'[A-Z\.]+') From d2ab2e482f08ef1b50ad6deca02841eda1fd37b3 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 6 Sep 2018 10:33:01 +0300 Subject: [PATCH 0696/1846] WIP Change base.py functions and structure --- dexbot/strategies/base.py | 466 ++++++++++++++++++++++---------------- 1 file changed, 272 insertions(+), 194 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index f34a7d3e1..70d62be84 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -38,7 +38,7 @@ :string: a regular expression, entries must match it, can be None which equivalent to .* :bool, ignored :choice: a list of choices, choices are in turn (tag, label) tuples. - labels get presented to user, and tag is used as the value saved back to the config dict + NOTE: 'labels' get presented to user, and 'tag' is used as the value saved back to the config dict! """ ConfigElement = collections.namedtuple('ConfigElement', 'key type default title description extra') @@ -184,6 +184,9 @@ def __init__(self, # Recheck flag - Tell the strategy to check for updated orders self.recheck_orders = False + # Count of orders to be fetched from the API + self.fetch_depth = 8 + # Set fee asset fee_asset_symbol = self.worker.get('fee_asset') @@ -323,10 +326,12 @@ def account_total_value(self, return_asset): def balance(self, asset, fee_reservation=False): """ Return the balance of your worker's account for a specific asset + :param string | asset: :param bool | fee_reservation: :return: Balance of specific asset """ # Todo: Add documentation + # Todo: Add logic here, fee_reservation return self._account.balance(asset) def calculate_center_price(self, center_price=None, asset_offset=False, spread=None, @@ -492,147 +497,283 @@ def get_allocated_assets(self, order_ids, return_asset=False, refresh=True): return {'quote': quote, 'base': base} - def get_lowest_market_sell(self): - """ Returns the lowest sell order that is not own, regardless of order size. + def get_external_price(self, source): + """ Returns the center price of market including own orders. - :return: order or None: Lowest market sell order. + :param source: + :return: """ - orders = self.market.orderbook(1) + # Todo: Insert logic here - try: - order = orders['asks'][0] - self.log.info('Lowest market ask @ {}'.format(order.get('price'))) - except IndexError: - self.log.info('Market has no lowest ask.') - return None + def get_market_fee(self): + """ Returns the fee percentage for buying specified asset - return order + :return: Fee percentage in decimal form (0.025) + """ + return self.fee_asset.market_fee_percent - def get_highest_market_buy(self): + def get_market_buy_orders(self): + """ + + :return: List of market buy orders + """ + return self.get_market_orders()['bids'] + + def get_market_sell_orders(self, depth=1): + """ + + :return: List of market sell orders + """ + return self.get_market_orders(depth=depth)['asks'] + + def get_highest_market_buy_order(self, orders=None): """ Returns the highest buy order that is not own, regardless of order size. - :return: order or None: Highest market buy order. + :param list | orders: Optional list of orders, if none given fetch newest from market + :return: Highest market buy order or None """ - orders = self.market.orderbook(1) + if not orders: + orders = self.market.orderbook(1) try: order = orders['bids'][0] - self.log.info('Highest market bid @ {}'.format(order.get('price'))) except IndexError: - self.log.info('Market has no highest bid.') + self.log.info('Market has no buy orders.') return None return order - def get_lowest_own_sell(self, refresh=False): - """ Returns lowest own sell order. + def get_highest_own_buy(self, orders=None): + """ Returns highest own buy order. - :param refresh: - :return: + :param list | orders: + :return: Highest own buy order by price at the market or None """ - # Todo: Insert logic here + if not orders: + orders = self.get_own_buy_orders() - def get_highest_own_buy(self, refresh=False): - """ Returns highest own buy order. + try: + return orders[0] + except IndexError: + return None - :param refresh: - :return: + def get_lowest_market_sell_order(self, orders=None): + """ Returns the lowest sell order that is not own, regardless of order size. + + :param list | orders: Optional list of orders, if none given fetch newest from market + :return: Lowest market sell order or None """ - # Todo: Insert logic here + if not orders: + orders = self.market.orderbook(1) - def get_price_for_amount_buy(self, amount=None, refresh=False): - """ Returns the cumulative price for which you could buy the specified amount of QUOTE. - This method must take into account market fee. + try: + order = orders['asks'][0] + except IndexError: + self.log.info('Market has no sell orders.') + return None - :param amount: - :param refresh: - :return: - """ - # Todo: Insert logic here + return order - def get_price_for_amount_sell(self, amount=None, refresh=False): - """ Returns the cumulative price for which you could sell the specified amount of QUOTE + def get_lowest_own_sell_order(self, orders=None): + """ Returns lowest own sell order. - :param amount: - :param refresh: - :return: + :param list | orders: + :return: Lowest own sell order by price at the market """ - # Todo: Insert logic here + if not orders: + orders = self.get_own_sell_orders() - def get_external_price(self, source): + try: + return orders[0] + except IndexError: + return None + + def get_market_center_price(self, quote_amount=0, refresh=False): """ Returns the center price of market including own orders. - :param source: + Depth: 0 = calculate from closest opposite orders. + Depth: non-zero = calculate from specified depth + + :param float | quote_amount: + :param bool | refresh: :return: """ # Todo: Insert logic here - def get_market_ask(self, depth=0, moving_average=0, weighted_moving_average=0, refresh=False): - """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with moving average - or weighted moving average + def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0, + refresh=False): + """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with + moving average or weighted moving average - :param float | depth: + :param float | quote_amount: + :param float | base_amount: :param float | moving_average: :param float | weighted_moving_average: :param bool | refresh: :return: """ - # Todo: Insert logic here + # Todo: Logic here - def get_market_bid(self, depth=0, moving_average=0, weighted_moving_average=0, refresh=False): - """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be sold, enhanced with moving average or - weighted moving average. + def get_market_orders(self, depth=1): + """ Returns orders from the current market split in bids and asks. Orders are sorted by price. + + bids = buy orders + asks = sell orders + + :param int | depth: Amount of orders per side will be fetched, default=1 + :return: Returns a dictionary of orders or None + """ + return self.market.orderbook(depth) + + def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0, + refresh=False): + """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be sold, enhanced with moving + average or weighted moving average. Depth = 0 means highest regardless of size - :param float | depth: + :param float | quote_amount: + :param float | base_amount: :param float | moving_average: :param float | weighted_moving_average: :param bool | refresh: :return: """ - # Todo: Insert logic here + # In case depth is not given, return price of the lowest sell order on the market + if depth == 0: + lowest_market_sell_order = self.get_lowest_market_sell_order() + return lowest_market_sell_order['price'] + + sum_quote = 0 + sum_base = 0 + order_number = 0 + + market_sell_orders = self.get_market_sell_orders(depth=depth) + market_fee = self.get_market_fee() + lacking = depth * (1 + market_fee) + + while lacking > 0: + sell_quote = float(market_sell_orders[order_number]['quote']) + + if sell_quote > lacking: + sum_quote += lacking + sum_base += lacking * market_sell_orders[order_number]['price'] # Make sure price is not inverted + lacking = 0 + else: + sum_quote += float(market_sell_orders[order_number]['quote']) + sum_base += float(market_sell_orders[order_number]['base']) + lacking = depth - sell_quote - def get_market_center_price(self, depth=0, refresh=False): - """ Returns the center price of market including own orders. + price = sum_base / sum_quote - Depth: 0 = calculate from closest opposite orders. - Depth: non-zero = calculate from specified depth + return price - :param float | depth: - :param bool | refresh: + def get_market_spread(self, quote_amount=0): + """ Returns the market spread %, including own orders, from specified depth, enhanced with moving average or + weighted moving average. + + :param int | quote_amount: + :return: Market spread as float or None + """ + # Decides how many orders need to be fetched for the market spread calculation + if quote_amount == 0: + # Get only the closest orders + fetch_depth = 1 + elif quote_amount > 0: + fetch_depth = self.fetch_depth + + # Raise the fetch depth each time. int() rounds the count + self.fetch_depth = int(self.fetch_depth * 1.5) + + market_orders = self.get_market_orders(fetch_depth) + + ask = self.get_market_ask() + bid = self.get_market_bid() + + # Calculate market spread + market_spread = ask / bid - 1 + + return market_spread + + def get_order_cancellation_fee(self, fee_asset): + """ Returns the order cancellation fee in the specified asset. + :param fee_asset: :return: """ # Todo: Insert logic here - def get_market_spread(self, highest_market_buy_price=None, lowest_market_sell_price=None, - depth=0, refresh=False): - """ Returns the market spread %, including own orders, from specified depth, enhanced with moving average or - weighted moving average + def get_order_creation_fee(self, fee_asset): + """ Returns the cost of creating an order in the asset specified - :param float | highest_market_buy_price: - :param float | lowest_market_sell_price: - :param float | depth: - :param bool | refresh: Use most resent data from Bitshares - :return: float or None: Market spread + :param fee_asset: QUOTE, BASE, BTS, or any other + :return: """ - # Todo: Add depth - if refresh: - try: - # Try fetching orders from market - highest_market_buy_price = self.get_highest_own_buy().get('price') - lowest_market_sell_price = self.get_highest_own_buy().get('price') - except AttributeError: - # This error is given if there is no market buy or sell order - return None - else: - # If orders are given, use them instead newest data from the blockchain - highest_market_buy_price = highest_market_buy_price - lowest_market_sell_price = lowest_market_sell_price + # Todo: Insert logic here - # Calculate market spread - market_spread = lowest_market_sell_price / highest_market_buy_price - 1 - return market_spread + def filter_buy_orders(self, orders, sort=None): + """ Return own buy orders from list of orders. Can be used to pick buy orders from a list + that is not up to date with the blockchain data. + + :param list | orders: List of orders + :param string | sort: DESC or ASC will sort the orders accordingly, default None + :return list | buy_orders: List of buy orders only + """ + buy_orders = [] + + # Filter buy orders + for order in orders: + # Check if the order is buy order, by comparing asset symbol of the order and the market + if order['base']['symbol'] == self.market['base']['symbol']: + buy_orders.append(order) + + if sort: + buy_orders = self.sort_orders_by_price(buy_orders, sort) + + return buy_orders + + def filter_sell_orders(self, orders, sort=None): + """ Return sell orders from list of orders. Can be used to pick sell orders from a list + that is not up to date with the blockchain data. + + :param list | orders: List of orders + :param string | sort: DESC or ASC will sort the orders accordingly, default None + :return list | sell_orders: List of sell orders only + """ + sell_orders = [] + + # Filter sell orders + for order in orders: + # Check if the order is buy order, by comparing asset symbol of the order and the market + if order['base']['symbol'] != self.market['base']['symbol']: + # Invert order before appending to the list, this gives easier comparison in strategy logic + sell_orders.append(order.invert()) + + if sort: + sell_orders = self.sort_orders_by_price(sell_orders, sort) + + return sell_orders + + def get_own_buy_orders(self, orders=None): + """ Get own buy orders from current market + + :return: List of buy orders + """ + if not orders: + # List of orders was not given so fetch everything from the market + orders = self.current_market_own_orders + + return self.filter_buy_orders(orders) + + def get_own_sell_orders(self, orders=None): + """ Get own sell orders from current market + + :return: List of sell orders + """ + if not orders: + # List of orders was not given so fetch everything from the market + orders = self.current_market_own_orders + + return self.filter_sell_orders(orders) def get_own_spread(self, highest_own_buy_price=None, lowest_own_sell_price=None, depth=0, refresh=False): """ Returns the difference between own closest opposite orders. @@ -647,8 +788,8 @@ def get_own_spread(self, highest_own_buy_price=None, lowest_own_sell_price=None, if refresh: try: # Try fetching own orders - highest_own_buy_price = self.get_highest_market_buy().get('price') - lowest_own_sell_price = self.get_lowest_own_sell().get('price') + highest_own_buy_price = self.get_highest_market_buy_order().get('price') + lowest_own_sell_price = self.get_lowest_own_sell_order().get('price') except AttributeError: return None else: @@ -660,76 +801,23 @@ def get_own_spread(self, highest_own_buy_price=None, lowest_own_sell_price=None, actual_spread = lowest_own_sell_price / highest_own_buy_price - 1 return actual_spread - def get_order_creation_fee(self, fee_asset): - """ Returns the cost of creating an order in the asset specified + def get_price_for_amount_buy(self, amount=None): + """ Returns the cumulative price for which you could buy the specified amount of QUOTE. + This method must take into account market fee. - :param fee_asset: QUOTE, BASE, BTS, or any other + :param amount: :return: """ # Todo: Insert logic here - def get_order_cancellation_fee(self, fee_asset): - """ Returns the order cancellation fee in the specified asset. - :param fee_asset: - :return: - """ - # Todo: Insert logic here + def get_price_for_amount_sell(self, amount=None): + """ Returns the cumulative price for which you could sell the specified amount of QUOTE - def get_market_fee(self, asset): - """ Returns the fee percentage for buying specified asset. - :param asset: - :return: Fee percentage in decimal form (0.025) + :param amount: + :return: """ # Todo: Insert logic here - def get_own_buy_orders(self, sort=None, orders=None): - # Todo: I might combine this with the get_own_sell_orders and have 2 functions to call it with different returns - """ Return own buy orders from list of orders. Can be used to pick buy orders from a list - that is not up to date with the blockchain data. If list of orders is not passed, orders are fetched from - blockchain. - - :param string | sort: DESC or ASC will sort the orders accordingly, default None. - :param list | orders: List of orders. If None given get all orders from Blockchain. - :return list | buy_orders: List of buy orders only. - """ - buy_orders = [] - - if not orders: - orders = self.current_market_own_orders - - # Find buy orders - for order in orders: - if not self.is_sell_order(order): - buy_orders.append(order) - if sort: - buy_orders = self.sort_orders(buy_orders, sort) - - return buy_orders - - def get_own_sell_orders(self, sort=None, orders=None): - """ Return own sell orders from list of orders. Can be used to pick sell orders from a list - that is not up to date with the blockchain data. If list of orders is not passed, orders are fetched from - blockchain. - - :param string | sort: DESC or ASC will sort the orders accordingly, default None. - :param list | orders: List of orders. If None given get all orders from Blockchain. - :return list | sell_orders: List of sell orders only. - """ - sell_orders = [] - - if not orders: - orders = self.current_market_own_orders - - # Find sell orders - for order in orders: - if self.is_sell_order(order): - sell_orders.append(order) - - if sort: - sell_orders = self.sort_orders(sell_orders, sort) - - return sell_orders - def get_updated_order(self, order_id): # Todo: This needed? """ Tries to get the updated order from the API. Returns None if the order doesn't exist @@ -775,26 +863,6 @@ def execute_bundle(self): self.bitshares.blocking = False return r - def is_buy_order(self, order): - """ Checks if the order is a buy order. Returns False if not. - - :param order: Buy / Sell order - :return: - """ - if order['base']['symbol'] == self.market['base']['symbol']: - return True - return False - - def is_sell_order(self, order): - """ Checks if the order is Sell order. Returns False if Buy order - - :param order: Buy / Sell order - :return: bool: True = Sell order, False = Buy order - """ - if order['base']['symbol'] != self.market['base']['symbol']: - return True - return False - def is_current_market(self, base_asset_id, quote_asset_id): """ Returns True if given asset id's are of the current market @@ -1023,28 +1091,40 @@ def account(self): def balances(self): """ Returns all the balances of the account assigned for the worker. - :return: list: Balances in list where each asset is in their own Amount object + :return: Balances in list where each asset is in their own Amount object """ return self._account.balances + @property + def base_asset(self): + return self.worker['market'].split('/')[1] + + @property + def quote_asset(self): + return self.worker['market'].split('/')[0] + @property def all_own_orders(self, refresh=True): """ Return the worker's open orders in all markets :param bool | refresh: Use most resent data - :return: list: List of Order objects + :return: List of Order objects """ # Refresh account data if refresh: self.account.refresh() - return [order for order in self.account.openorders] + orders = [] + for order in self.account.openorders: + orders.append(order) + + return orders @property def current_market_own_orders(self, refresh=False): """ Return the account's open orders in the current market - :return: list: List of Order objects + :return: List of Order objects """ orders = [] @@ -1058,25 +1138,6 @@ def current_market_own_orders(self, refresh=False): return orders - @property - def get_updated_orders(self): - """ Returns all open orders as updated orders - Todo: What exactly? When orders are needed who wants out of date info? - """ - self.account.refresh() - - limited_orders = [] - for order in self.account['limit_orders']: - base_asset_id = order['sell_price']['base']['asset_id'] - quote_asset_id = order['sell_price']['quote']['asset_id'] - # Check if the order is in the current market - if not self.is_current_market(base_asset_id, quote_asset_id): - continue - - limited_orders.append(self.get_updated_limit_order(order)) - - return [Order(order, bitshares_instance=self.bitshares) for order in limited_orders] - @property def market(self): """ Return the market object as :class:`bitshares.market.Market` @@ -1084,13 +1145,12 @@ def market(self): return self._market @staticmethod - def convert_asset(from_value, from_asset, to_asset, refresh=False): + def convert_asset(from_value, from_asset, to_asset): """ Converts asset to another based on the latest market value :param float | from_value: Amount of the input asset :param string | from_asset: Symbol of the input asset :param string | to_asset: Symbol of the output asset - :param bool | refresh: :return: float Asset converted to another asset as float value """ market = Market('{}/{}'.format(from_asset, to_asset)) @@ -1098,6 +1158,24 @@ def convert_asset(from_value, from_asset, to_asset, refresh=False): latest_price = ticker.get('latest', {}).get('price', None) return from_value * latest_price + @staticmethod + def get_order(order_id, return_none=True): + """ Returns the Order object for the order_id + + :param str|dict order_id: blockchain object id of the order + can be an order 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 not order_id: + return None + if 'id' in order_id: + order_id = order_id['id'] + order = Order(order_id) + if return_none and order['deleted']: + return None + return order + @staticmethod def get_original_order(order_id, return_none=True): """ Returns the Order object for the order_id @@ -1146,8 +1224,8 @@ def purge_all_local_worker_data(worker_name): Storage.clear_worker_data(worker_name) @staticmethod - def sort_orders(orders, sort='DESC'): - """ Return list of orders sorted ascending or descending + def sort_orders_by_price(orders, sort='DESC'): + """ Return list of orders sorted ascending or descending by price :param list | orders: list of orders to be sorted :param string | sort: ASC or DESC. Default DESC From bc4cdce7202b9f7d4274e3544a7e14e12ca3497f Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 6 Sep 2018 10:48:48 +0300 Subject: [PATCH 0697/1846] Refactor get market buy and sell orders --- dexbot/strategies/base.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 70d62be84..ffaff3ce3 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -512,16 +512,18 @@ def get_market_fee(self): """ return self.fee_asset.market_fee_percent - def get_market_buy_orders(self): - """ + def get_market_buy_orders(self, depth=10): + """ Fetches most reset data and returns list of buy orders. - :return: List of market buy orders + :param int | depth: Amount of buy orders returned, Default=10 + :return: List of market sell orders """ - return self.get_market_orders()['bids'] + return self.get_market_orders(depth=depth)['bids'] - def get_market_sell_orders(self, depth=1): - """ + def get_market_sell_orders(self, depth=10): + """ Fetches most reset data and returns list of sell orders. + :param int | depth: Amount of sell orders returned, Default=10 :return: List of market sell orders """ return self.get_market_orders(depth=depth)['asks'] @@ -533,10 +535,10 @@ def get_highest_market_buy_order(self, orders=None): :return: Highest market buy order or None """ if not orders: - orders = self.market.orderbook(1) + orders = self.get_market_buy_orders(1) try: - order = orders['bids'][0] + order = orders[0] except IndexError: self.log.info('Market has no buy orders.') return None @@ -564,10 +566,10 @@ def get_lowest_market_sell_order(self, orders=None): :return: Lowest market sell order or None """ if not orders: - orders = self.market.orderbook(1) + orders = self.get_market_sell_orders(1) try: - order = orders['asks'][0] + order = orders[0] except IndexError: self.log.info('Market has no sell orders.') return None From ad6b57ce0b0cf4208c653f5178d92a8f8d2edd60 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 6 Sep 2018 12:40:31 +0300 Subject: [PATCH 0698/1846] Remove execute_bundle() from base.py --- dexbot/strategies/base.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index ffaff3ce3..0f2b0607b 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -855,16 +855,6 @@ def enhance_center_price(self, reference=None, manual_offset=False, balance_base """ # Todo: Insert logic here - def execute_bundle(self): - # Todo: Is this still needed? - # Apparently old naming was "execute", and was used by walls strategy. - """ Execute a bundle of operations - """ - self.bitshares.blocking = "head" - r = self.bitshares.txbuffer.broadcast() - self.bitshares.blocking = False - return r - def is_current_market(self, base_asset_id, quote_asset_id): """ Returns True if given asset id's are of the current market From bbf21a3cd7a73e0246d1ddf8670a21aafd3c9f30 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 3 Sep 2018 14:12:05 +0500 Subject: [PATCH 0699/1846] Implement regulation of check interval We don't need to run maintenance ofter when balances are allocated because every maintenance cycle consumes CPU resources. On the other hand, we need to allocate funds more quickly when we have them. --- dexbot/strategies/staggered_orders.py | 28 +++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index dfad8c002..233156a2b 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -128,6 +128,8 @@ def __init__(self, *args, **kwargs): # Minimal check interval is needed to prevent event queue accumulation self.min_check_interval = 1 + self.max_check_interval = 120 + self.current_check_interval = self.min_check_interval if self.view: self.update_gui_slider() @@ -141,8 +143,7 @@ def maintain_strategy(self, *args, **kwargs): delta = self.start - self.last_check # Only allow to maintain whether minimal time passed. - if delta < timedelta(seconds=self.min_check_interval): - self.log.debug('Ignoring event as min_check_interval has not passed') + if delta < timedelta(seconds=self.current_check_interval): return # Get all user's orders on current market @@ -175,6 +176,11 @@ def maintain_strategy(self, *args, **kwargs): # Calculate balances self.refresh_balances() + # Save current balances for futher checks. Save exactly key value instead of full key because it may be modified + # later on + previous_base_balance = self.base_balance['amount'] + previous_quote_balance = self.quote_balance['amount'] + # Calculate asset thresholds self.quote_asset_threshold = self.quote_total_balance / 20000 self.base_asset_threshold = self.base_total_balance / 20000 @@ -214,6 +220,24 @@ def maintain_strategy(self, *args, **kwargs): else: quote_allocated = True + # Greatly increase check interval to lower CPU load whether there is no funds to allocate or we cannot + # allocate funds for some reason + self.refresh_balances() + if (self.current_check_interval == self.min_check_interval and + previous_base_balance == self.base_balance['amount'] and + previous_quote_balance == self.quote_balance['amount']): + # Balance didn't changed, so we can reduce maintenance frequency + self.log.debug('Raising check interval up to {} seconds to reduce CPU usage'.format( + self.max_check_interval)) + self.current_check_interval = self.max_check_interval + elif (self.current_check_interval == self.max_check_interval and + (previous_base_balance != self.base_balance['amount'] or + previous_quote_balance != self.quote_balance['amount'])): + # Balance changed, increase maintenance frequency to allocate more quickly + self.log.debug('Reducing check interval to {} seconds because of changed ' + 'balances'.format(self.min_check_interval)) + self.current_check_interval = self.min_check_interval + # Do not continue whether assets is not fully allocated if (not base_allocated or not quote_allocated) or self.bootstrapping: # Further checks should be performed on next maintenance From bbd938978873c06d9c27d015c4e2bed65ff0ab4a Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 3 Sep 2018 14:18:08 +0500 Subject: [PATCH 0700/1846] Get rid of unneeded return --- dexbot/strategies/staggered_orders.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 233156a2b..5e8c1d4c4 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -155,9 +155,8 @@ def maintain_strategy(self, *args, **kwargs): elif not self.market_center_price: # On empty market we have to pass the user specified center price self.market_center_price = self.calculate_center_price(center_price=self.center_price, suppress_errors=True) - self.log_maintenance_time() - return - elif self.market_center_price and not self.initial_market_center_price: + + if self.market_center_price and not self.initial_market_center_price: # Save initial market center price self.initial_market_center_price = self.market_center_price From e0588d1c820ebbd2e1d8a379cf8fe6e15c53df20 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 3 Sep 2018 15:20:59 +0500 Subject: [PATCH 0701/1846] Change logic of how we're reserving funds When we have free funds and also have partially filled order on the other side, we're reserving funds for next buy or sell order. Before this, it was true only whether lower or upper bounds are reached. This change forces to reserve funds whether bounds are not reached. --- dexbot/strategies/staggered_orders.py | 41 ++++++++++++++------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 5e8c1d4c4..46f95d35b 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -431,10 +431,7 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): elif not self.sell_orders: # Do not try to do anything than placing higher buy whether there is no sell orders return - elif lowest_buy_order_price / (1 + self.increment) < self.lower_bound: - # Lower bound has been reached and now will start allocating rest of the base balance. - self.bootstrapping = False - self.log.debug('Increasing orders sizes for BASE asset') + else: lowest_sell_order = self.sell_orders[0] if not self.check_partial_fill(lowest_sell_order): """ Detect partially filled order on the opposite side and reserve appropriate amount to place @@ -446,12 +443,16 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): '{:.8f} {}'.format(funds_to_reserve, self.market['base']['symbol'])) base_balance -= funds_to_reserve if base_balance > self.base_asset_threshold: - self.increase_order_sizes('base', base_balance, self.buy_orders) - else: - # Lower bound is not reached, we need to add additional orders at the extremes - self.bootstrapping = False - self.log.debug('Placing lower order than lowest_buy_order') - self.place_lower_buy_order(lowest_buy_order, allow_partial=True) + if lowest_buy_order_price / (1 + self.increment) < self.lower_bound: + # Lower bound has been reached and now will start allocating rest of the base balance. + self.bootstrapping = False + self.log.debug('Increasing orders sizes for BASE asset') + self.increase_order_sizes('base', base_balance, self.buy_orders) + else: + # Lower bound is not reached, we need to add additional orders at the extremes + self.bootstrapping = False + self.log.debug('Placing lower order than lowest_buy_order') + self.place_lower_buy_order(lowest_buy_order, allow_partial=True) else: # Make sure we have enough balance to replace partially filled order if base_balance + highest_buy_order['for_sale']['amount'] >= highest_buy_order['base']['amount']: @@ -523,10 +524,7 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): elif not self.buy_orders: # Do not try to do anything than placing lower sell whether there is no buy orders return - elif highest_sell_order_price * (1 + self.increment) > self.upper_bound: - # Upper bound has been reached and now will start allocating rest of the quote balance. - self.bootstrapping = False - self.log.debug('Increasing orders sizes for QUOTE asset') + else: highest_buy_order = self.buy_orders[0] if not self.check_partial_fill(highest_buy_order): """ Detect partially filled order on the opposite side and reserve appropriate amount to place @@ -538,11 +536,16 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): '{:.8f} {}'.format(funds_to_reserve, self.market['quote']['symbol'])) quote_balance -= funds_to_reserve if quote_balance > self.quote_asset_threshold: - self.increase_order_sizes('quote', quote_balance, self.sell_orders) - else: - # Higher bound is not reached, we need to add additional orders at the extremes - self.bootstrapping = False - self.place_higher_sell_order(highest_sell_order, allow_partial=True) + if highest_sell_order_price * (1 + self.increment) > self.upper_bound: + # Upper bound has been reached and now will start allocating rest of the quote balance. + self.bootstrapping = False + self.log.debug('Increasing orders sizes for QUOTE asset') + if quote_balance > self.quote_asset_threshold: + self.increase_order_sizes('quote', quote_balance, self.sell_orders) + else: + # Higher bound is not reached, we need to add additional orders at the extremes + self.bootstrapping = False + self.place_higher_sell_order(highest_sell_order, allow_partial=True) else: # Make sure we have enough balance to replace partially filled order if quote_balance + lowest_sell_order['for_sale']['amount'] >= lowest_sell_order['base']['amount']: From 3c128fff2f3a2193aacc22f6c5edb66c2308819f Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 3 Sep 2018 15:27:19 +0500 Subject: [PATCH 0702/1846] Fix bug in order reservation for QUOTE side This commit makes it consistent with reserve amount calculation for BASE side. --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 46f95d35b..0d21e542a 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -530,8 +530,8 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): """ Detect partially filled order on the opposite side and reserve appropriate amount to place lower sell order """ - # Base amount of sell order is actually QUOTE - funds_to_reserve = lowest_sell_order['base']['amount'] + lower_sell_order = self.place_lower_sell_order(lowest_sell_order, place_order=False) + funds_to_reserve = lower_sell_order['amount'] self.log.debug('Partially filled order on opposite side, reserving funds for next sell order: ' '{:.8f} {}'.format(funds_to_reserve, self.market['quote']['symbol'])) quote_balance -= funds_to_reserve From 3ec34397087e1202f74122827e5f3d69385b2ae1 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 5 Sep 2018 23:22:35 +0500 Subject: [PATCH 0703/1846] Refactor variable name When limiting orders, use clear variable names like base_limit and quote_limit to be able to easily distinguish where is what. --- dexbot/strategies/staggered_orders.py | 64 +++++++++++++++------------ 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 0d21e542a..3eb415b78 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -415,19 +415,21 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): # Place order limited by size of the opposite-side order lowest_sell_order = self.sell_orders[0] if self.mode == 'mountain': - limit = lowest_sell_order['quote']['amount'] - self.place_higher_buy_order(highest_buy_order, base_limit=limit, allow_partial=False) + quote_limit = None + base_limit = lowest_sell_order['quote']['amount'] self.log.debug('Limiting buy order base by opposite order BASE asset amount: {}'.format( - limit)) + base_limit)) elif self.mode == 'valley': - limit = lowest_sell_order['base']['amount'] + quote_limit = lowest_sell_order['base']['amount'] + base_limit = None self.log.debug('Limiting buy order base by opposite order QUOTE asset amount: {}'.format( - limit)) - self.place_higher_buy_order(highest_buy_order, limit=limit, allow_partial=False) + quote_limit)) else: self.log.warning('Using fallback order limiting') - limit = lowest_sell_order['quote']['amount'] - self.place_higher_buy_order(highest_buy_order, base_limit=limit, allow_partial=False) + quote_limit = None + base_limit = lowest_sell_order['quote']['amount'] + self.place_higher_buy_order(highest_buy_order, quote_limit=quote_limit, base_limit=base_limit, + allow_partial=False) elif not self.sell_orders: # Do not try to do anything than placing higher buy whether there is no sell orders return @@ -510,17 +512,21 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): # Place order limited by opposite-side order highest_buy_order = self.buy_orders[0] if self.mode == 'mountain': - limit = highest_buy_order['quote']['amount'] - self.log.debug('Limiting sell order by opposite order QUOTE asset amount: {}'.format(limit)) - self.place_lower_sell_order(lowest_sell_order, limit=limit, allow_partial=False) + quote_limit = highest_buy_order['quote']['amount'] + base_limit = None + self.log.debug('Limiting sell order by opposite order QUOTE asset amount: {}'.format( + quote_limit)) elif self.mode == 'valley': - limit = highest_buy_order['base']['amount'] - self.log.debug('Limiting sell order by opposite order BASE asset amount: {}'.format(limit)) - self.place_lower_sell_order(lowest_sell_order, base_limit=limit, allow_partial=False) + quote_limit = None + base_limit = highest_buy_order['base']['amount'] + self.log.debug('Limiting sell order by opposite order BASE asset amount: {}'.format( + base_limit)) else: self.log.warning('Using fallback order limiting') - limit = highest_buy_order['quote']['amount'] - self.place_lower_sell_order(lowest_sell_order, limit=limit, allow_partial=False) + quote_limit = highest_buy_order['quote']['amount'] + base_limit = None + self.place_lower_sell_order(lowest_sell_order, quote_limit=quote_limit, base_limit=base_limit, + allow_partial=False) elif not self.buy_orders: # Do not try to do anything than placing lower sell whether there is no buy orders return @@ -840,17 +846,17 @@ def check_partial_fill(self, order): return False return True - def place_higher_buy_order(self, order, place_order=True, allow_partial=False, base_limit=None, limit=None): + def place_higher_buy_order(self, order, place_order=True, allow_partial=False, base_limit=None, quote_limit=None): """ Place higher buy order :param order: Previously highest buy order :param bool | place_order: True = Places order to the market, False = returns amount and price :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance :param float | base_limit: order should be limited in size by this BASE amount - :param float | limit: order should be limited in size by this QUOTE amount + :param float | quote_limit: order should be limited in size by this QUOTE amount """ - if base_limit and limit: - self.log.error('Only base_limit or limit should be specified') + if base_limit and quote_limit: + self.log.error('Only base_limit or quote_limit should be specified') self.disabled = True return None @@ -871,10 +877,10 @@ def place_higher_buy_order(self, order, place_order=True, allow_partial=False, b if base_limit and base_limit < base_amount: base_amount = base_limit amount = base_limit / price - elif limit and limit < amount: + elif quote_limit and quote_limit < amount: # Limit order amount only when it is lower than amount - base_amount = limit * price - amount = limit + base_amount = quote_limit * price + amount = quote_limit if base_amount > self.base_balance['amount']: if place_order and not allow_partial: @@ -949,17 +955,17 @@ def place_lower_buy_order(self, order, place_order=True, allow_partial=False): else: return {"amount": amount, "price": price} - def place_lower_sell_order(self, order, place_order=True, allow_partial=False, base_limit=None, limit=None): + def place_lower_sell_order(self, order, place_order=True, allow_partial=False, base_limit=None, quote_limit=None): """ Place lower sell order :param order: Previously higher sell order :param bool | place_order: True = Places order to the market, False = returns amount and price :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance :param float | base_limit: order should be limited in size by this BASE amount - :param float | limit: order should be limited in size by this QUOTE amount + :param float | quote_limit: order should be limited in size by this QUOTE amount """ - if base_limit and limit: - self.log.error('Only base_limit or limit should be specified') + if base_limit and quote_limit: + self.log.error('Only base_limit or quote_limit should be specified') self.disabled = True return None @@ -978,8 +984,8 @@ def place_lower_sell_order(self, order, place_order=True, allow_partial=False, b if base_limit and base_limit < base_amount: amount = base_limit / price - elif limit and limit < amount: - amount = limit + elif quote_limit and quote_limit < amount: + amount = quote_limit if amount > self.quote_balance['amount']: if place_order and not allow_partial: From a333eb05baeeaae85ad1220ab6d5a8947f1a0a01 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 7 Sep 2018 12:51:50 +0500 Subject: [PATCH 0704/1846] Bump dependency on pybitshres New version is needed to remove temp CER workaround. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2d90561c6..1e06be4c2 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ cmd_class = {} console_scripts = ['dexbot-cli = dexbot.cli:main'] install_requires = [ - 'bitshares==0.1.19', + 'bitshares==0.1.22', 'uptick>=0.1.9', 'click', 'sqlalchemy', From 1d2f1f15c30edb86a0700d792b353579d924e32d Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 7 Sep 2018 12:52:34 +0500 Subject: [PATCH 0705/1846] Remove CER workaround as it was fixed library-wide --- dexbot/strategies/staggered_orders.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 3eb415b78..ac5ee78b3 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -283,19 +283,19 @@ def refresh_balances(self): self.base_balance = account_balances['base'] self.quote_balance = account_balances['quote'] - # Todo: order_creation_fee(BTS) = 0.01 for now. Reserve fee for 200 orders + # Todo: order_creation_fee(BTS) = 0.01 for now. Use OperationsFee in the feature. + # Reserve fee for 200 orders fee_reserve = 0.01 * 200 if self.fee_asset['id'] == '1.3.0': + # Fee asset is BTS, so no further calculations are needed fee_reserve = fee_reserve else: - temp_market = Market(base=Asset('1.3.0'), quote=self.fee_asset) - ticker = temp_market.ticker() - """ Inverted CER gives us Price like 10 BTS/FEE_ASSET, where BTS is quote - We're using invert() as workaround for https://github.com/bitshares/python-bitshares/issues/138 - """ - core_exchange_rate = ticker['core_exchange_rate'].invert() + # Determine how many fee_asset is needed for core-exchange + temp_market = Market(base=self.fee_asset, quote=Asset('1.3.0')) + core_exchange_rate = temp_market.ticker()['core_exchange_rate'] fee_reserve = fee_reserve * core_exchange_rate['base']['amount'] + # Finally, reserve only required asset if self.fee_asset['id'] == self.market['base']['id']: self.base_balance['amount'] = self.base_balance['amount'] - fee_reserve elif self.fee_asset['id'] == self.market['quote']['id']: From 88290ef1a3c0856589ad1118cf44cb825a5fd764 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 7 Sep 2018 12:53:11 +0500 Subject: [PATCH 0706/1846] Improve several log messages --- dexbot/strategies/staggered_orders.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index ac5ee78b3..0cdfee313 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -670,8 +670,8 @@ def increase_order_sizes(self, asset, asset_balance, orders): # Limit sell order to available balance if asset_balance < new_order_amount - order_amount: new_order_amount = order_amount + asset_balance['amount'] - self.log.info('Limiting new sell order to avail asset balance: {}'.format( - new_order_amount)) + self.log.info('Limiting new sell order to avail asset balance: {:.8f} {}'.format( + new_order_amount, asset_balance['symbol'])) price = (order['price'] ** -1) self.log.debug('Cancelling sell order in increase_order_sizes(); ' @@ -746,8 +746,8 @@ def increase_order_sizes(self, asset, asset_balance, orders): # Limit buy order to available balance if (asset_balance / price) < (new_base_amount - order_amount) / price: new_base_amount = order_amount + asset_balance['amount'] - self.log.info('Limiting new buy order to avail asset balance: {}'.format( - new_base_amount)) + self.log.info('Limiting new buy order to avail asset balance: {:.8f} {}'.format( + new_base_amount, asset_balance['symbol'])) new_order_amount = new_base_amount / price self.log.debug('Cancelling buy order in increase_order_sizes(); ' @@ -803,7 +803,8 @@ def increase_order_sizes(self, asset, asset_balance, orders): # Limit order to available balance if asset_balance < amount_base - order_amount: amount_base = order_amount + asset_balance['amount'] - self.log.info('Limiting new order to avail asset balance: {}'.format(amount_base)) + self.log.info('Limiting new order to avail asset balance: {:.8f} {}'.format(amount_base, + asset_balance['symbol'])) if asset == 'quote': price = (order['price'] ** -1) From af6c7ed40280b09d5ccf8d39ff698d160c26af3c Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 7 Sep 2018 13:26:14 +0300 Subject: [PATCH 0707/1846] Add calculate_worker_value() to base.py --- dexbot/strategies/base.py | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 0f2b0607b..a2c31ae05 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -385,14 +385,41 @@ def calculate_order_data(self, order, amount, price): order['base'] = base_asset return order - def calculate_worker_value(self, unit_of_measure, refresh=True): - """ Returns the combined value of allocated and available QUOTE and BASE, measured in "unit_of_measure". + def calculate_worker_value(self, unit_of_measure): + """ Returns the combined value of allocated and available BASE and QUOTE. Total value is + measured in "unit_of_measure", which is either BASE or QUOTE symbol. - :param unit_of_measure: - :param refresh: - :return: + :param string | unit_of_measure: Asset symbol + :return: Value of the worker as float """ - # Todo: Insert logic here + base_total = 0 + quote_total = 0 + + # Calculate total balances + balances = self.balances + for balance in balances: + if balance['symbol'] == self.base_asset: + base_total += balance['amount'] + elif balance['symbol'] == self.quote_asset: + quote_total += balance['amount'] + + # Calculate value of the orders in unit of measure + orders = self.current_market_own_orders + for order in orders: + if order['base']['symbol'] == self.quote_asset: + # Pick sell orders order's BASE amount, which is same as worker's QUOTE, to worker's BASE + quote_total += order['base']['amount'] + else: + base_total += order['base']['amount'] + + # Finally convert asset to another and return the sum + if unit_of_measure == self.base_asset: + quote_total = self.convert_asset(quote_total, self.quote_asset, unit_of_measure) + elif unit_of_measure == self.quote_asset: + base_total = self.convert_asset(base_total, self.base_asset, unit_of_measure) + + # Fixme: Make sure that decimal precision is correct. + return base_total + quote_total def cancel_all_orders(self): """ Cancel all orders of the worker's account From 3f0fa5c91c3c86f409c1cbddc4b1228b4e4132de Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 7 Sep 2018 13:26:42 +0300 Subject: [PATCH 0708/1846] Add fee_reservation parameter to balance() --- dexbot/strategies/base.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index a2c31ae05..62948c235 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -323,7 +323,7 @@ def account_total_value(self, return_asset): return total_value - def balance(self, asset, fee_reservation=False): + def balance(self, asset, fee_reservation=0): """ Return the balance of your worker's account for a specific asset :param string | asset: @@ -331,8 +331,13 @@ def balance(self, asset, fee_reservation=False): :return: Balance of specific asset """ # Todo: Add documentation - # Todo: Add logic here, fee_reservation - return self._account.balance(asset) + # Todo: Check that fee reservation was as intended, having it true / false made no sense + balance = self._account.balance(asset) + + if fee_reservation > 0: + balance['amount'] = balance['amount'] - fee_reservation + + return balance def calculate_center_price(self, center_price=None, asset_offset=False, spread=None, order_ids=None, manual_offset=0, suppress_errors=False): From b58a9e9feeab7081e40a3ee6e9e3f65ed5840f36 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 7 Sep 2018 13:53:43 +0300 Subject: [PATCH 0709/1846] Remove duplicate function for getting order --- dexbot/strategies/base.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 62948c235..fc681a088 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1182,23 +1182,6 @@ def convert_asset(from_value, from_asset, to_asset): latest_price = ticker.get('latest', {}).get('price', None) return from_value * latest_price - @staticmethod - def get_order(order_id, return_none=True): - """ Returns the Order object for the order_id - - :param str|dict order_id: blockchain object id of the order - can be an order 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 not order_id: - return None - if 'id' in order_id: - order_id = order_id['id'] - order = Order(order_id) - if return_none and order['deleted']: - return None - return order @staticmethod def get_original_order(order_id, return_none=True): From 9e386c7637d57359653b136a316b1d8221d273fc Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 7 Sep 2018 13:55:16 +0300 Subject: [PATCH 0710/1846] Remove old calculate center price method --- dexbot/strategies/base.py | 72 --------------------------------------- 1 file changed, 72 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index fc681a088..f899744bb 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -223,35 +223,6 @@ def __init__(self, logging.getLogger('dexbot.orders_log'), {} ) - def _calculate_center_price(self, suppress_errors=False): - """ - - :param suppress_errors: - :return: - """ - # Todo: Add documentation - ticker = self.market.ticker() - highest_bid = ticker.get("highestBid") - lowest_ask = ticker.get("lowestAsk") - - if highest_bid is None or highest_bid == 0.0: - 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: - if not suppress_errors: - self.log.critical( - "Cannot estimate center price, there is no lowest ask." - ) - self.disabled = True - return None - - center_price = highest_bid['price'] * math.sqrt(lowest_ask['price'] / highest_bid['price']) - return center_price - def _callbackPlaceFillOrders(self, d): """ This method distinguishes notifications caused by Matched orders from those caused by placed orders Todo: can this be renamed to _instantFill()? @@ -339,49 +310,6 @@ def balance(self, asset, fee_reservation=0): return balance - def calculate_center_price(self, center_price=None, asset_offset=False, spread=None, - order_ids=None, manual_offset=0, suppress_errors=False): - # Todo: Fix comment - """ Calculate center price which shifts based on available funds - """ - if center_price is None: - # No center price was given so we simply calculate the center price - calculated_center_price = self._calculate_center_price(suppress_errors) - else: - # 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 center_price: - calculated_center_price = center_price - - if asset_offset: - total_balance = self.get_allocated_assets(order_ids) - total = (total_balance['quote'] * calculated_center_price) + total_balance['base'] - - if not total: # Prevent division by zero - balance = 0 - else: - # Returns a value between -1 and 1 - balance = (total_balance['base'] / total) * 2 - 1 - - if balance < 0: - # With less of base asset center price should be offset downward - calculated_center_price = calculated_center_price / math.sqrt(1 + spread * (balance * -1)) - elif balance > 0: - # With more of base asset center price will be offset upwards - calculated_center_price = calculated_center_price * math.sqrt(1 + spread * balance) - else: - calculated_center_price = calculated_center_price - - # Calculate final_offset_price if manual center price offset is given - if manual_offset: - calculated_center_price = calculated_center_price + (calculated_center_price * manual_offset) - - return calculated_center_price - def calculate_order_data(self, order, amount, price): quote_asset = Amount(amount, self.market['quote']['symbol']) order['quote'] = quote_asset From c7be01bc71d4bc6192c11142350f8cfd5de2bd06 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 7 Sep 2018 14:27:57 +0300 Subject: [PATCH 0711/1846] WIP Change base.py comments and functions --- dexbot/strategies/base.py | 68 ++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index f899744bb..f3780c935 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -227,7 +227,6 @@ def _callbackPlaceFillOrders(self, d): """ This method distinguishes notifications caused by Matched orders from those caused by placed orders Todo: can this be renamed to _instantFill()? """ - # Todo: Add documentation if isinstance(d, FilledOrder): self.onOrderMatched(d) elif isinstance(d, Order): @@ -279,6 +278,7 @@ def account_total_value(self, return_asset): # Orders balance calculation for order in self.all_own_orders: + # Todo: What is the purpose of this? updated_order = self.get_updated_order(order['id']) if not order: @@ -421,13 +421,11 @@ def count_asset(self, order_ids=None, return_asset=False, refresh=True): return {'quote': quote, 'base': base} - def get_allocated_assets(self, order_ids, return_asset=False, refresh=True): - # Todo: + def get_allocated_assets(self, order_ids=None, return_asset=False): """ Returns the amount of QUOTE and BASE allocated in orders, and that do not show up in available balance :param order_ids: :param return_asset: - :param refresh: :return: """ # Todo: Add documentation @@ -574,7 +572,7 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, :param bool | refresh: :return: """ - # Todo: Logic here + # Todo: Insert logic here def get_market_orders(self, depth=1): """ Returns orders from the current market split in bids and asks. Orders are sorted by price. @@ -589,10 +587,10 @@ def get_market_orders(self, depth=1): def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0, refresh=False): - """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be sold, enhanced with moving - average or weighted moving average. + """ Returns the BASE/QUOTE price for which [quote_amount] worth of QUOTE could be bought, + enhanced with moving average or weighted moving average. - Depth = 0 means highest regardless of size + [quote/base]_amount = 0 means lowest regardless of size :param float | quote_amount: :param float | base_amount: @@ -601,18 +599,20 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, :param bool | refresh: :return: """ - # In case depth is not given, return price of the lowest sell order on the market - if depth == 0: + # Todo: Work in progress + # In case amount is not given, return price of the lowest sell order on the market + if base_amount == 0 or quote_amount == 0: lowest_market_sell_order = self.get_lowest_market_sell_order() return lowest_market_sell_order['price'] + # This calculation is for when quote_amount is given sum_quote = 0 sum_base = 0 order_number = 0 - market_sell_orders = self.get_market_sell_orders(depth=depth) + market_sell_orders = self.get_market_sell_orders(depth=self.fetch_depth) market_fee = self.get_market_fee() - lacking = depth * (1 + market_fee) + lacking = quote_amount * (1 + market_fee) while lacking > 0: sell_quote = float(market_sell_orders[order_number]['quote']) @@ -624,7 +624,7 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, else: sum_quote += float(market_sell_orders[order_number]['quote']) sum_base += float(market_sell_orders[order_number]['base']) - lacking = depth - sell_quote + lacking -= sell_quote price = sum_base / sum_quote @@ -649,8 +649,8 @@ def get_market_spread(self, quote_amount=0): market_orders = self.get_market_orders(fetch_depth) - ask = self.get_market_ask() - bid = self.get_market_bid() + ask = self.get_market_sell_price() + bid = self.get_market_buy_price() # Calculate market spread market_spread = ask / bid - 1 @@ -659,6 +659,7 @@ def get_market_spread(self, quote_amount=0): def get_order_cancellation_fee(self, fee_asset): """ Returns the order cancellation fee in the specified asset. + :param fee_asset: :return: """ @@ -764,10 +765,10 @@ def get_own_spread(self, highest_own_buy_price=None, lowest_own_sell_price=None, return actual_spread def get_price_for_amount_buy(self, amount=None): - """ Returns the cumulative price for which you could buy the specified amount of QUOTE. - This method must take into account market fee. + """ Returns the cumulative price for which you could buy the specified amount of QUOTE with market fee taken in + account. - :param amount: + :param float | amount: Amount to buy in QUOTE :return: """ # Todo: Insert logic here @@ -775,7 +776,7 @@ def get_price_for_amount_buy(self, amount=None): def get_price_for_amount_sell(self, amount=None): """ Returns the cumulative price for which you could sell the specified amount of QUOTE - :param amount: + :param float | amount: Amount to sell in QUOTE :return: """ # Todo: Insert logic here @@ -784,8 +785,7 @@ def get_updated_order(self, order_id): # Todo: This needed? """ Tries to get the updated order from the API. Returns None if the order doesn't exist - :param str|dict order_id: blockchain object id of the order - can be an order dict with the id key in it + :param str|dict order_id: blockchain object id of the order can be an order dict with the id key in it """ if isinstance(order_id, dict): order_id = order_id['id'] @@ -816,6 +816,7 @@ def enhance_center_price(self, reference=None, manual_offset=False, balance_base # Todo: Insert logic here def is_current_market(self, base_asset_id, quote_asset_id): + # Todo: Is this useful? """ Returns True if given asset id's are of the current market :return: bool: True = Current market, False = Not current market @@ -824,11 +825,13 @@ def is_current_market(self, base_asset_id, quote_asset_id): if base_asset_id == self.market['base']['id']: return True return False + # Todo: Should we return true if market is opposite? if quote_asset_id == self.market['base']['id']: if base_asset_id == self.market['quote']['id']: return True return False + return False def pause_worker(self): @@ -963,6 +966,7 @@ def restore_order(self, order): :return: """ # Todo: Insert logic here + # Todo: Is this something that is commonly used and thus needed? def retry_action(self, action, *args, **kwargs): """ Perform an action, and if certain suspected-to-be-spurious grapheme bugs occur, @@ -998,10 +1002,10 @@ def retry_action(self, action, *args, **kwargs): raise def write_order_log(self, worker_name, order): - """ + """ F :param string | worker_name: Name of the worker - :param object | order: Order that was traded + :param object | order: Order that was fulfilled """ # Todo: Add documentation operation_type = 'TRADE' @@ -1108,28 +1112,25 @@ def convert_asset(from_value, from_asset, to_asset): market = Market('{}/{}'.format(from_asset, to_asset)) ticker = market.ticker() latest_price = ticker.get('latest', {}).get('price', None) - return from_value * latest_price + precision = market['base']['precision'] + return truncate((from_value * latest_price), precision) @staticmethod - def get_original_order(order_id, return_none=True): - """ Returns the Order object for the order_id + def get_order(order_id, return_none=True): + """ Get Order object with order_id - :param dict | order_id: blockchain object id of the order can be an order 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 - :return: + :param str | dict order_id: blockchain object id of the order can be an order 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 + :return: Order object """ if not order_id: return None - if 'id' in order_id: order_id = order_id['id'] - order = Order(order_id) - if return_none and order['deleted']: return None - return order @staticmethod @@ -1140,6 +1141,7 @@ def get_updated_limit_order(limit_order): :param limit_order: an item of Account['limit_orders'] :return: Order Todo: When would we not want an updated order? + Todo: If get_updated_order is removed, this can be removed as well. """ order = copy.deepcopy(limit_order) price = order['sell_price']['base']['amount'] / order['sell_price']['quote']['amount'] From 3803412d883cc6fd184e491af081d3669443062f Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 8 Sep 2018 11:22:34 +0500 Subject: [PATCH 0712/1846] Instant fill check should work only when place_order=True --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 0cdfee313..7ab13e66f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -863,7 +863,7 @@ def place_higher_buy_order(self, order, place_order=True, allow_partial=False, b price = order['price'] * (1 + self.increment) - if not self.is_instant_fill_enabled and price > float(self.ticker['lowestAsk']): + if not self.is_instant_fill_enabled and place_order and price > float(self.ticker['lowestAsk']): self.log.info('Refusing to place an order which crosses lowestAsk') return None @@ -979,7 +979,7 @@ def place_lower_sell_order(self, order, place_order=True, allow_partial=False, b base_amount = amount * price - if not self.is_instant_fill_enabled and price < float(self.ticker['highestBid']): + if not self.is_instant_fill_enabled and place_order and price < float(self.ticker['highestBid']): self.log.info('Refusing to place an order which crosses highestBid') return None From a57e7cd142a8cace0070f8704c44db05c4aa2a84 Mon Sep 17 00:00:00 2001 From: The Hapax <39811582+thehapax@users.noreply.github.com> Date: Sun, 9 Sep 2018 13:10:27 -0700 Subject: [PATCH 0713/1846] update to include OSX and Raspberry pi Updated description with links to OSX and Raspberry pi install instructions on the Wiki --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 813a2d548..e7f564738 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ master: ## Installing and running the software -See instructions in the [Wiki](https://github.com/Codaone/DEXBot/wiki) for [Linux](https://github.com/Codaone/DEXBot/wiki/Setup-Guide-for-Linux), and [Windows](https://github.com/Codaone/DEXBot/wiki/Setup-Guide-for-Windows). OSX users can try downloading the package or following the Linux guide. +See instructions in the [Wiki](https://github.com/Codaone/DEXBot/wiki) for [Linux](https://github.com/Codaone/DEXBot/wiki/Setup-Guide-for-Linux), [Windows](https://github.com/Codaone/DEXBot/wiki/Setup-Guide-for-Windows), [OSX](https://github.com/Codaone/DEXBot/wiki/Setup-Guide-for-Mac-OS-X). [Raspberry Pi](https://github.com/Codaone/DEXBot/wiki/Setup-guide-for-Raspberry-Pi). Other users can try downloading the package or following the Linux guide. ## Contributing From 137c16447ca6bedb56278551b2c9da6059543e00 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 10 Sep 2018 11:05:56 +0500 Subject: [PATCH 0714/1846] Handle bootstrap on empty market again --- dexbot/strategies/staggered_orders.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 7ab13e66f..45be3a00e 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -160,6 +160,11 @@ def maintain_strategy(self, *args, **kwargs): # Save initial market center price self.initial_market_center_price = self.market_center_price + # Still not have market_center_price? Empty market, don't continue + if not self.market_center_price: + self.log.warning('Cannot calculate center price on empty market, please set is manually') + return + # Get highest buy and lowest sell prices from orders highest_buy_price = 0 lowest_sell_price = 0 From 02bceb69afc381124893d4fa15f606a6c3f1aaf6 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 10 Sep 2018 10:49:39 +0300 Subject: [PATCH 0715/1846] Modify comment and fix a typo --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 45be3a00e..b0a2307fd 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -180,8 +180,8 @@ def maintain_strategy(self, *args, **kwargs): # Calculate balances self.refresh_balances() - # Save current balances for futher checks. Save exactly key value instead of full key because it may be modified - # later on + # Save current balances for further checks. + # Save exactly key value instead of full key because it may be modified later on. previous_base_balance = self.base_balance['amount'] previous_quote_balance = self.quote_balance['amount'] From 921c4af962f82ad692d54ae5672a346f580f63e6 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 10 Sep 2018 11:34:29 +0300 Subject: [PATCH 0716/1846] Change manual center price field behavior --- dexbot/controllers/strategy_controller.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index b2f7e19a8..396dec803 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -220,9 +220,22 @@ def __init__(self, view, configure, worker_controller, worker_data): if not self.view.strategy_widget.center_price_dynamic_input.isChecked(): self.view.strategy_widget.center_price_input.setDisabled(False) - # Do this after the event connecting super().__init__(view, configure, worker_controller, worker_data) + widget = self.view.strategy_widget + + # Event connecting + widget.center_price_dynamic_input.clicked.connect(self.onchange_center_price_dynamic_input) + + # Trigger the onchange events once + self.onchange_center_price_dynamic_input(widget.center_price_dynamic_input.isChecked()) + + def onchange_center_price_dynamic_input(self, checked): + if checked: + self.view.strategy_widget.center_price_input.setDisabled(True) + else: + self.view.strategy_widget.center_price_input.setDisabled(False) + def set_required_base(self, text): self.view.strategy_widget.required_base_text.setText(text) From 2ccd7ebae96d9670216fca4c4014265d2e15f1ba Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 10 Sep 2018 11:35:44 +0300 Subject: [PATCH 0717/1846] Add comment to purge_worker_data() --- dexbot/basestrategy.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 33d113698..bedd72506 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -662,6 +662,7 @@ def purge(self): @staticmethod def purge_worker_data(worker_name): + """ Remove worker data from database only """ Storage.clear_worker_data(worker_name) def total_balance(self, order_ids=None, return_asset=False): From 8fcc3358b35b9f1857c98d330c50fabe15efbd64 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 10 Sep 2018 13:08:58 +0300 Subject: [PATCH 0718/1846] Change worker to not remove orders when editing --- dexbot/views/worker_item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 064724520..19a3aa2fe 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -161,7 +161,7 @@ def handle_edit_worker(self): if return_value: new_worker_name = edit_worker_dialog.worker_name self.view.change_worker_widget_name(self.worker_name, new_worker_name) - self.main_ctrl.remove_worker(self.worker_name) + self.main_ctrl.pause_worker(self.worker_name) self.main_ctrl.config.replace_worker_config(self.worker_name, new_worker_name, edit_worker_dialog.worker_data) From 3b4a5828bc1abb076c0c9bd694e0db6deff53095 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 10 Sep 2018 13:13:36 +0300 Subject: [PATCH 0719/1846] Change dexbot version number to 0.6.1 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 071269db9..292bef55b 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.6.0' +VERSION = '0.6.1' AUTHOR = 'Codaone Oy' __version__ = VERSION From 94f38b4192947721f8134d83df2f67bb4fa72148 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 11 Sep 2018 09:47:57 +0300 Subject: [PATCH 0720/1846] Fix error when editing offline worker --- dexbot/controllers/main_controller.py | 7 +++++-- dexbot/views/worker_item.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index ef738656d..29daf26b1 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -50,8 +50,11 @@ def start_worker(self, worker_name, config, view): self.worker_manager.daemon = True self.worker_manager.start() - def pause_worker(self, worker_name): - self.worker_manager.stop(worker_name, pause=True) + def pause_worker(self, worker_name, config=None): + if self.worker_manager and self.worker_manager.is_alive(): + self.worker_manager.stop(worker_name, pause=True) + else: + self.worker_manager = WorkerInfrastructure(config, self.bitshares_instance) def remove_worker(self, worker_name): # Todo: Add some threading here so that the GUI doesn't freeze diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 19a3aa2fe..84d0fae6c 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -161,7 +161,7 @@ def handle_edit_worker(self): if return_value: new_worker_name = edit_worker_dialog.worker_name self.view.change_worker_widget_name(self.worker_name, new_worker_name) - self.main_ctrl.pause_worker(self.worker_name) + self.main_ctrl.pause_worker(self.worker_name, config=self.worker_config) self.main_ctrl.config.replace_worker_config(self.worker_name, new_worker_name, edit_worker_dialog.worker_data) From 3f1be7f2260175c1edc4e92564d070417134caa8 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 11 Sep 2018 10:10:09 +0300 Subject: [PATCH 0721/1846] Fix staggered orders crash at price > upper_bound --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index b0a2307fd..228b2d130 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1023,8 +1023,8 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente if price > self.upper_bound: self.log.info( 'Not placing highest sell order because price will exceed higher bound. Market center ' - 'price: {:.8f}, closest order price: {:.8f}, higher_bound: {}'.format(market_center_price, - price, self.higher_bound)) + 'price: {:.8f}, closest order price: {:.8f}, upper_bound: {}'.format(market_center_price, + price, self.upper_bound)) return if self.mode == 'mountain': From 4e15ccb8aa8ad18e01125e485a9118ecf29fb88a Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 11 Sep 2018 10:31:20 +0300 Subject: [PATCH 0722/1846] Change code style to match PEP guidelines --- dexbot/strategies/staggered_orders.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 228b2d130..6cf1186c2 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -793,8 +793,8 @@ def increase_order_sizes(self, asset, asset_balance, orders): else: """ Special processing for the closest order. - Calculte new order amount based on orders count, but do not allow to perform too small increase - rounds. New lowest buy / highest sell should be higher by at least one increment. + Calculate new order amount based on orders count, but do not allow to perform too small + increase rounds. New lowest buy / highest sell should be higher by at least one increment. """ closer_order_bound = order_amount * (1 + self.increment) new_amount = (total_balance / orders_count) / (1 + self.increment / 100) @@ -808,16 +808,15 @@ def increase_order_sizes(self, asset, asset_balance, orders): # Limit order to available balance if asset_balance < amount_base - order_amount: amount_base = order_amount + asset_balance['amount'] - self.log.info('Limiting new order to avail asset balance: {:.8f} {}'.format(amount_base, - asset_balance['symbol'])) + self.log.info('Limiting new order to avail asset balance: {:.8f} {}' + .format(amount_base, asset_balance['symbol'])) if asset == 'quote': price = (order['price'] ** -1) elif asset == 'base': price = order['price'] - self.log.debug('Cancelling {} order in increase_order_sizes(); ' - 'mode: {}, amount: {}, price: {:.8f}'.format(order_type, self.mode, order_amount, - price)) + self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}, amount: {}, price: {:.8f}' + .format(order_type, self.mode, order_amount, price)) self.cancel(order) if asset == 'quote': @@ -1023,8 +1022,8 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente if price > self.upper_bound: self.log.info( 'Not placing highest sell order because price will exceed higher bound. Market center ' - 'price: {:.8f}, closest order price: {:.8f}, upper_bound: {}'.format(market_center_price, - price, self.upper_bound)) + 'price: {:.8f}, closest order price: {:.8f}, upper_bound: {}' + .format(market_center_price, price, self.upper_bound)) return if self.mode == 'mountain': @@ -1090,7 +1089,7 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p Buy slope: Buy orders same as valley - Sell orders same asmountain + Sell orders same as mountain Sell slope: Buy orders same as mountain @@ -1109,8 +1108,8 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p if price < self.lower_bound: self.log.info( 'Not placing lowest buy order because price will exceed lower bound. Market center price: ' - '{:.8f}, closest order price: {:.8f}, lower bound: {}'.format(market_center_price, price, - self.lower_bound)) + '{:.8f}, closest order price: {:.8f}, lower bound: {}' + .format(market_center_price, price, self.lower_bound)) return if self.mode == 'mountain': From fd374dfa46700703f4216188b0e18510811993de Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 11 Sep 2018 10:33:49 +0300 Subject: [PATCH 0723/1846] Change dexbot version number to 0.6.2 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 292bef55b..1000f6b7a 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.6.1' +VERSION = '0.6.2' AUTHOR = 'Codaone Oy' __version__ = VERSION From b0aef0ac69a85e6fa6e0193dd0232c3de127e0f8 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 11 Sep 2018 15:28:41 +0300 Subject: [PATCH 0724/1846] Change comments on functions --- dexbot/strategies/base.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index f3780c935..77df6511f 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -301,7 +301,6 @@ def balance(self, asset, fee_reservation=0): :param bool | fee_reservation: :return: Balance of specific asset """ - # Todo: Add documentation # Todo: Check that fee reservation was as intended, having it true / false made no sense balance = self._account.balance(asset) @@ -311,6 +310,14 @@ def balance(self, asset, fee_reservation=0): return balance def calculate_order_data(self, order, amount, price): + """ + + :param order: + :param amount: + :param price: + :return: + """ + # Todo: Add documentation quote_asset = Amount(amount, self.market['quote']['symbol']) order['quote'] = quote_asset order['price'] = price @@ -365,13 +372,12 @@ def cancel_all_orders(self): self.log.info("Orders canceled") def cancel_orders(self, orders, batch_only=False): - """ Cancel specific order(s) + """ Cancel specific order or orders :param list | orders: List of orders to cancel :param bool | batch_only: Try cancel orders only in batch mode without one-by-one fallback - :return: + :return: Todo: Add documentation """ - # Todo: Add documentation if not isinstance(orders, (list, set, tuple)): orders = [orders] @@ -424,11 +430,10 @@ def count_asset(self, order_ids=None, return_asset=False, refresh=True): def get_allocated_assets(self, order_ids=None, return_asset=False): """ Returns the amount of QUOTE and BASE allocated in orders, and that do not show up in available balance - :param order_ids: - :param return_asset: - :return: + :param list | order_ids: + :param bool | return_asset: + :return: Dictionary of QUOTE and BASE amounts """ - # Todo: Add documentation if not order_ids: order_ids = [] elif isinstance(order_ids, str): @@ -449,6 +454,7 @@ def get_allocated_assets(self, order_ids=None, return_asset=False): elif asset_id == base_asset: base += order['base']['amount'] + # Return as Amount objects instead of only float values if return_asset: quote = Amount(quote, quote_asset) base = Amount(base, base_asset) @@ -596,7 +602,6 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, :param float | base_amount: :param float | moving_average: :param float | weighted_moving_average: - :param bool | refresh: :return: """ # Todo: Work in progress @@ -619,6 +624,7 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, if sell_quote > lacking: sum_quote += lacking + # Fixme: Price is inverted to same format as buy orders. Should this be inverted for this calculation? sum_base += lacking * market_sell_orders[order_number]['price'] # Make sure price is not inverted lacking = 0 else: @@ -637,6 +643,7 @@ def get_market_spread(self, quote_amount=0): :param int | quote_amount: :return: Market spread as float or None """ + # Todo: Work in progress # Decides how many orders need to be fetched for the market spread calculation if quote_amount == 0: # Get only the closest orders @@ -813,7 +820,7 @@ def enhance_center_price(self, reference=None, manual_offset=False, balance_base :param int or float | weighted_average: :return: """ - # Todo: Insert logic here + # Todo: Remove this after market center price is done def is_current_market(self, base_asset_id, quote_asset_id): # Todo: Is this useful? From 7eecdfdc5b3bca3ac5bfd70d826b584746d48640 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 11 Sep 2018 15:29:20 +0300 Subject: [PATCH 0725/1846] Add functions to get order fees --- dexbot/strategies/base.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 77df6511f..479491763 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -16,6 +16,7 @@ import bitsharesapi.exceptions from bitshares.account import Account from bitshares.amount import Amount, Asset +from bitshares.dex import Dex from bitshares.instance import shared_bitshares_instance from bitshares.market import Market from bitshares.price import FilledOrder, Order, UpdateCallOrder @@ -144,6 +145,9 @@ def __init__(self, # BitShares instance self.bitshares = bitshares_instance or shared_bitshares_instance() + # Dex instance used to get different fees for the market + self.dex = Dex(self.bitshares) + # Storage Storage.__init__(self, name) @@ -667,10 +671,16 @@ def get_market_spread(self, quote_amount=0): def get_order_cancellation_fee(self, fee_asset): """ Returns the order cancellation fee in the specified asset. - :param fee_asset: - :return: + :param string | fee_asset: Asset in which the fee is wanted + :return: Cancellation fee as fee asset """ - # Todo: Insert logic here + # Get fee + fees = self.dex.returnFees() + limit_order_cancel = fees['limit_order_cancel'] + + # Convert fee + # Todo: Change 'TEST' to 'BTS' + return self.convert_asset(limit_order_cancel['fee'], 'TEST', fee_asset) def get_order_creation_fee(self, fee_asset): """ Returns the cost of creating an order in the asset specified @@ -678,7 +688,13 @@ def get_order_creation_fee(self, fee_asset): :param fee_asset: QUOTE, BASE, BTS, or any other :return: """ - # Todo: Insert logic here + # Get fee + fees = self.dex.returnFees() + limit_order_create = fees['limit_order_create'] + + # Convert fee + # Todo: Change 'TEST' to 'BTS' + return self.convert_asset(limit_order_create['fee'], 'TEST', fee_asset) def filter_buy_orders(self, orders, sort=None): """ Return own buy orders from list of orders. Can be used to pick buy orders from a list From 54a7460c4f13ae535e07c0185a187d65319d28fb Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 11 Sep 2018 15:30:05 +0300 Subject: [PATCH 0726/1846] Remove unused functions --- dexbot/strategies/base.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 479491763..8d6c9ee91 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -787,23 +787,6 @@ def get_own_spread(self, highest_own_buy_price=None, lowest_own_sell_price=None, actual_spread = lowest_own_sell_price / highest_own_buy_price - 1 return actual_spread - def get_price_for_amount_buy(self, amount=None): - """ Returns the cumulative price for which you could buy the specified amount of QUOTE with market fee taken in - account. - - :param float | amount: Amount to buy in QUOTE - :return: - """ - # Todo: Insert logic here - - def get_price_for_amount_sell(self, amount=None): - """ Returns the cumulative price for which you could sell the specified amount of QUOTE - - :param float | amount: Amount to sell in QUOTE - :return: - """ - # Todo: Insert logic here - def get_updated_order(self, order_id): # Todo: This needed? """ Tries to get the updated order from the API. Returns None if the order doesn't exist From be6e0128044920f7449647165890f2240a86b368 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 11 Sep 2018 15:30:34 +0300 Subject: [PATCH 0727/1846] WIP Change get_market_center_price() --- dexbot/strategies/base.py | 77 ++++++++++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 14 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 8d6c9ee91..b9216aa42 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -203,6 +203,9 @@ def __init__(self, # If there is no fee asset, use BTS self.fee_asset = Asset('1.3.0') + # Ticker + self.ticker = self.market.ticker() + # Settings for bitshares instance self.bitshares.bundle = bool(self.worker.get("bundle", False)) @@ -558,31 +561,78 @@ def get_lowest_own_sell_order(self, orders=None): except IndexError: return None - def get_market_center_price(self, quote_amount=0, refresh=False): + def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors=False): """ Returns the center price of market including own orders. Depth: 0 = calculate from closest opposite orders. - Depth: non-zero = calculate from specified depth + Depth: non-zero = calculate from specified depth of orders + :param float | base_amount: :param float | quote_amount: - :param bool | refresh: - :return: + :param bool | suppress_errors: + :return: Market center price as float """ - # Todo: Insert logic here + buy_price = 0 + sell_price = 0 + + if base_amount == 0: + # Get highest buy order from the market + highest_buy_order = self.ticker.get("highestBid") - def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0, - refresh=False): + if highest_buy_order is None or highest_buy_order == 0.0: + if not suppress_errors: + self.log.critical("Cannot estimate center price, there is no highest bid.") + self.disabled = True + return None + + # The highest buy price + buy_price = highest_buy_order['price'] + elif base_amount > 0: + buy_price = self.get_market_buy_price(base_amount=base_amount) + + if quote_amount == 0: + # Get lowest sell order from the market + lowest_sell_order = self.ticker.get("lowestAsk") + + if lowest_sell_order is None or lowest_sell_order == 0.0: + if not suppress_errors: + self.log.critical("Cannot estimate center price, there is no lowest ask.") + self.disabled = True + return None + + # The lowest sell price + sell_price = lowest_sell_order['price'] + elif quote_amount > 0: + sell_price = self.get_market_sell_price(quote_amount=quote_amount) + + # Calculate and return market center price + return buy_price * math.sqrt(sell_price / buy_price) + + def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0): """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with moving average or weighted moving average :param float | quote_amount: :param float | base_amount: - :param float | moving_average: - :param float | weighted_moving_average: - :param bool | refresh: + :param int | moving_average: Count of orders to be taken in to the calculations + :param int | weighted_moving_average: Count of orders to be taken in to the calculations :return: """ - # Todo: Insert logic here + """ + Buy orders: + 10 CODACOIN for 20 TEST + 15 CODACOIN for 30 TEST + 20 CODACOIN for 40 TEST + + (price + price + price) / moving_average + moving average = (2 + 3 + 4) / 3 = 3 + + ((amount * price) + (amount * price) + (amount * price)) / amount_total + weighted moving average = ((10 * 2) + (15 * 3) + (20 * 4)) / 45 = 3,222222 + + """ + # Todo: Work in progress + pass def get_market_orders(self, depth=1): """ Returns orders from the current market split in bids and asks. Orders are sorted by price. @@ -595,8 +645,7 @@ def get_market_orders(self, depth=1): """ return self.market.orderbook(depth) - def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0, - refresh=False): + def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0): """ Returns the BASE/QUOTE price for which [quote_amount] worth of QUOTE could be bought, enhanced with moving average or weighted moving average. @@ -851,7 +900,7 @@ def pause_worker(self): # Removes worker's orders from local database self.clear_orders() - def purge_all_worker_data(self): + def clear_all_worker_data(self): """ Clear all the worker data from the database and cancel all orders """ # Removes worker's orders from local database From b7d3b727fa1ff704412a6bd007a0e6a2cf6c47e8 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Tue, 11 Sep 2018 21:48:23 +0300 Subject: [PATCH 0728/1846] Made center price and spread methods use get_market_sell/buy_price() logic for depth/amount --- dexbot/strategies/base.py | 111 +++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 61 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index b9216aa42..36b500074 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -572,38 +572,20 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors :param bool | suppress_errors: :return: Market center price as float """ - buy_price = 0 - sell_price = 0 - - if base_amount == 0: - # Get highest buy order from the market - highest_buy_order = self.ticker.get("highestBid") - - if highest_buy_order is None or highest_buy_order == 0.0: - if not suppress_errors: - self.log.critical("Cannot estimate center price, there is no highest bid.") - self.disabled = True - return None - - # The highest buy price - buy_price = highest_buy_order['price'] - elif base_amount > 0: - buy_price = self.get_market_buy_price(base_amount=base_amount) - - if quote_amount == 0: - # Get lowest sell order from the market - lowest_sell_order = self.ticker.get("lowestAsk") + if highest_buy_order is None or highest_buy_order == 0.0: + if not suppress_errors: + self.log.critical("Cannot estimate center price, there is no highest bid.") + self.disabled = True + return None - if lowest_sell_order is None or lowest_sell_order == 0.0: - if not suppress_errors: - self.log.critical("Cannot estimate center price, there is no lowest ask.") - self.disabled = True - return None + if lowest_sell_order is None or lowest_sell_order == 0.0: + if not suppress_errors: + self.log.critical("Cannot estimate center price, there is no lowest ask.") + self.disabled = True + return None - # The lowest sell price - sell_price = lowest_sell_order['price'] - elif quote_amount > 0: - sell_price = self.get_market_sell_price(quote_amount=quote_amount) + buy_price = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount) + sell_price = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount) # Calculate and return market center price return buy_price * math.sqrt(sell_price / buy_price) @@ -632,6 +614,7 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, """ # Todo: Work in progress + # Like get_market_sell_price(), but defaulting to base_amount if both base and quote are specified. pass def get_market_orders(self, depth=1): @@ -659,37 +642,54 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, """ # Todo: Work in progress # In case amount is not given, return price of the lowest sell order on the market - if base_amount == 0 or quote_amount == 0: - lowest_market_sell_order = self.get_lowest_market_sell_order() - return lowest_market_sell_order['price'] + if base_amount == 0 and quote_amount == 0: + return self.ticker.get("lowestAsk") - # This calculation is for when quote_amount is given sum_quote = 0 sum_base = 0 order_number = 0 market_sell_orders = self.get_market_sell_orders(depth=self.fetch_depth) market_fee = self.get_market_fee() - lacking = quote_amount * (1 + market_fee) - - while lacking > 0: - sell_quote = float(market_sell_orders[order_number]['quote']) - if sell_quote > lacking: - sum_quote += lacking - # Fixme: Price is inverted to same format as buy orders. Should this be inverted for this calculation? - sum_base += lacking * market_sell_orders[order_number]['price'] # Make sure price is not inverted - lacking = 0 - else: - sum_quote += float(market_sell_orders[order_number]['quote']) - sum_base += float(market_sell_orders[order_number]['base']) - lacking -= sell_quote + # This calculation is for when quote_amount is given (whether or not base_amount was given + if quote_amount > 0: + lacking = quote_amount * (1 + market_fee) + while lacking > 0: + sell_quote = float(market_sell_orders[order_number]['quote']) + + if sell_quote > lacking: + sum_quote += lacking + # Fixme: Price is inverted to same format as buy orders. Should this be inverted for this calculation? + sum_base += lacking / market_sell_orders[order_number]['price'] # I swapped * to /. Is it right now? + lacking = 0 + else: + sum_quote += float(market_sell_orders[order_number]['quote']) + sum_base += float(market_sell_orders[order_number]['base']) + lacking -= sell_quote + + # This calculation is for when quote_amount isn't given, so we go with the given base_amount + if quote_amount = 0: + lacking = base_amount * (1 + market_fee) + while lacking > 0: + buy_base = float(market_sell_orders[order_number]['base']) + + if buy_base > lacking: + sum_base += lacking + # Fixme: Price is inverted to same format as buy orders. Should this be inverted for this calculation? + sum_quote += lacking * market_sell_orders[order_number][ + 'price'] # Make sure price is not inverted + lacking = 0 + else: + sum_quote += float(market_sell_orders[order_number]['quote']) + sum_base += float(market_sell_orders[order_number]['base']) + lacking -= buy_base price = sum_base / sum_quote return price - def get_market_spread(self, quote_amount=0): + def get_market_spread(self, quote_amount=0, base_amount=0): """ Returns the market spread %, including own orders, from specified depth, enhanced with moving average or weighted moving average. @@ -697,20 +697,9 @@ def get_market_spread(self, quote_amount=0): :return: Market spread as float or None """ # Todo: Work in progress - # Decides how many orders need to be fetched for the market spread calculation - if quote_amount == 0: - # Get only the closest orders - fetch_depth = 1 - elif quote_amount > 0: - fetch_depth = self.fetch_depth - - # Raise the fetch depth each time. int() rounds the count - self.fetch_depth = int(self.fetch_depth * 1.5) - - market_orders = self.get_market_orders(fetch_depth) - ask = self.get_market_sell_price() - bid = self.get_market_buy_price() + ask = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount) + bid = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount) # Calculate market spread market_spread = ask / bid - 1 From 07695bd153c266ec989aa12f60080808f8fdec47 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 12 Sep 2018 16:58:43 +0300 Subject: [PATCH 0729/1846] WIP Change market price calculations --- dexbot/strategies/base.py | 127 ++++++++++++++++++++++---------------- 1 file changed, 75 insertions(+), 52 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 36b500074..002de14cc 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -564,14 +564,15 @@ def get_lowest_own_sell_order(self, orders=None): def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors=False): """ Returns the center price of market including own orders. - Depth: 0 = calculate from closest opposite orders. - Depth: non-zero = calculate from specified depth of orders - :param float | base_amount: :param float | quote_amount: :param bool | suppress_errors: :return: Market center price as float """ + + highest_buy_order = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount) + lowest_sell_order = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount) + if highest_buy_order is None or highest_buy_order == 0.0: if not suppress_errors: self.log.critical("Cannot estimate center price, there is no highest bid.") @@ -584,11 +585,8 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors self.disabled = True return None - buy_price = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount) - sell_price = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount) - # Calculate and return market center price - return buy_price * math.sqrt(sell_price / buy_price) + return highest_buy_order * math.sqrt(lowest_sell_order / highest_buy_order) def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0): """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with @@ -615,7 +613,42 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, """ # Todo: Work in progress # Like get_market_sell_price(), but defaulting to base_amount if both base and quote are specified. - pass + # In case amount is not given, return price of the lowest sell order on the market + if quote_amount == 0 and base_amount == 0: + return self.ticker.get('highestBid') + + asset_amount = base_amount + + """ Since the purpose is never get both quote and base amounts, favor base amount if both given because + this function is looking for buy price. + """ + if base_amount > quote_amount: + base = True + else: + asset_amount = quote_amount + base = False + + market_buy_orders = self.get_market_buy_orders(depth=self.fetch_depth) + market_fee = self.get_market_fee() + + target_amount = asset_amount * (1 + market_fee) + + quote_amount = 0 + base_amount = 0 + # Todo: Work in progress, THIS IS EXPERIEMENTAL WAY TO CALCULATE THIS. Revert if incorrect. + for order in market_buy_orders: + if base: + # BASE amount was given + if base_amount < target_amount: + quote_amount += order['quote']['amount'] + base_amount += order['base']['amount'] + elif not base: + # QUOTE amount was given + if quote_amount < target_amount: + quote_amount += order['quote']['amount'] + base_amount += order['base']['amount'] + + return base_amount / quote_amount def get_market_orders(self, depth=1): """ Returns orders from the current market split in bids and asks. Orders are sorted by price. @@ -642,69 +675,59 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, """ # Todo: Work in progress # In case amount is not given, return price of the lowest sell order on the market - if base_amount == 0 and quote_amount == 0: - return self.ticker.get("lowestAsk") + if quote_amount == 0 and base_amount == 0: + return self.ticker.get('lowestAsk') - sum_quote = 0 - sum_base = 0 - order_number = 0 + asset_amount = quote_amount + + """ Since the purpose is never get both quote and base amounts, favor quote amount if both given because + this function is looking for sell price. + """ + if quote_amount > base_amount: + quote = True + else: + asset_amount = base_amount + quote = False market_sell_orders = self.get_market_sell_orders(depth=self.fetch_depth) market_fee = self.get_market_fee() - # This calculation is for when quote_amount is given (whether or not base_amount was given - if quote_amount > 0: - lacking = quote_amount * (1 + market_fee) - while lacking > 0: - sell_quote = float(market_sell_orders[order_number]['quote']) - - if sell_quote > lacking: - sum_quote += lacking - # Fixme: Price is inverted to same format as buy orders. Should this be inverted for this calculation? - sum_base += lacking / market_sell_orders[order_number]['price'] # I swapped * to /. Is it right now? - lacking = 0 - else: - sum_quote += float(market_sell_orders[order_number]['quote']) - sum_base += float(market_sell_orders[order_number]['base']) - lacking -= sell_quote - - # This calculation is for when quote_amount isn't given, so we go with the given base_amount - if quote_amount = 0: - lacking = base_amount * (1 + market_fee) - while lacking > 0: - buy_base = float(market_sell_orders[order_number]['base']) - - if buy_base > lacking: - sum_base += lacking - # Fixme: Price is inverted to same format as buy orders. Should this be inverted for this calculation? - sum_quote += lacking * market_sell_orders[order_number][ - 'price'] # Make sure price is not inverted - lacking = 0 - else: - sum_quote += float(market_sell_orders[order_number]['quote']) - sum_base += float(market_sell_orders[order_number]['base']) - lacking -= buy_base + target_amount = asset_amount * (1 + market_fee) + + quote_amount = 0 + base_amount = 0 - price = sum_base / sum_quote + # Todo: Work in progress, THIS IS EXPERIEMENTAL WAY TO CALCULATE THIS. Revert if incorrect. + for order in market_sell_orders: + if quote: + # QUOTE amount was given + if quote_amount < target_amount: + quote_amount += order['quote']['amount'] + base_amount += order['base']['amount'] + elif not quote: + # BASE amount was given + if base_amount < target_amount: + quote_amount += order['quote']['amount'] + base_amount += order['base']['amount'] - return price + return base_amount / quote_amount def get_market_spread(self, quote_amount=0, base_amount=0): """ Returns the market spread %, including own orders, from specified depth, enhanced with moving average or weighted moving average. - :param int | quote_amount: + :param float | quote_amount: + :param float | base_amount: :return: Market spread as float or None """ - # Todo: Work in progress - ask = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount) bid = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount) # Calculate market spread - market_spread = ask / bid - 1 + if ask == 0 or bid == 0: + return None - return market_spread + return ask / bid - 1 def get_order_cancellation_fee(self, fee_asset): """ Returns the order cancellation fee in the specified asset. From e6c9277169ce661455c69ed20fa31e409598b6e1 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 8 Sep 2018 11:50:04 +0500 Subject: [PATCH 0730/1846] Refactor order increasement for mountain mode This is a code otimization. Get rid of code doubling for buy and sell orders. --- dexbot/strategies/staggered_orders.py | 234 ++++++++++---------------- 1 file changed, 88 insertions(+), 146 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 6cf1186c2..7b2b83edc 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -607,160 +607,102 @@ def increase_order_sizes(self, asset, asset_balance, orders): """ # Mountain mode: if self.mode == 'mountain': - if asset == 'quote': - """ Starting from the lowest SELL order. For each order, see if it is approximately - maximum size. - If it is, move on to next. - If not, cancel it and replace with maximum size order. Then return. - If highest_sell_order is reached, increase it to maximum size - - Maximum size is: - 1. As many "quote * (1 + increment)" as the order below (higher_bound) - AND - 2. As many "quote as the order above (lower_bound) - - Also when making an order it's size always will be limited by available free balance - """ - # Get orders and amounts to be compared. Note: orders are sorted from low price to high - for order in orders: - order_index = orders.index(order) - order_amount = order['base']['amount'] - - # This check prevents choosing order with index lower than the list length - if order_index == 0: - # In case checking the first order, use the same order, but increased by 1 increment - # This allows our lowest sell order amount exceed highest buy order - lower_order = order - lower_bound = lower_order['base']['amount'] * (1 + self.increment) - else: - lower_order = orders[order_index - 1] - lower_bound = lower_order['base']['amount'] + """ Starting from the furthest order. For each order, see if it is approximately + maximum size. + If it is, move on to next. + If not, cancel it and replace with maximum size order. Then return. + If highest_sell_order is reached, increase it to maximum size - # This check prevents choosing order with index higher than the list length - if order_index + 1 < len(orders): - higher_order = orders[order_index + 1] - is_least_order = False - else: - higher_order = orders[order_index] - is_least_order = True - - higher_bound = higher_order['base']['amount'] * (1 + self.increment) - - self.log.debug('QUOTE: lower_bound: {}, order_amount: {}, higher_bound: {}'.format( - lower_bound, order_amount * (1 + self.increment / 10), higher_bound)) - - if lower_bound > order_amount * (1 + self.increment / 10) < higher_bound: - # Calculate new order size and place the order to the market - new_order_amount = higher_bound - - if is_least_order: - new_orders_sum = 0 - amount = order_amount - for o in orders: - amount = amount * (1 + self.increment) - new_orders_sum += amount - # To reduce allocation rounds, increase furthest order more - new_order_amount = order_amount * (self.quote_total_balance / new_orders_sum) \ - * (1 + self.increment * 0.75) - - if new_order_amount < lower_bound: - """ This is for situations when calculated new_order_amount is not big enough to - allocate all funds. Use partial-increment increase, so we'll got at least one full - increase round. Whether we will just use `new_order_amount = lower_bound`, we will - get less than one full allocation round, thus leaving lowest sell order not - increased. - """ - new_order_amount = lower_bound / (1 + self.increment * 0.2) - - # Limit sell order to available balance - if asset_balance < new_order_amount - order_amount: - new_order_amount = order_amount + asset_balance['amount'] - self.log.info('Limiting new sell order to avail asset balance: {:.8f} {}'.format( - new_order_amount, asset_balance['symbol'])) + Maximum size is: + 1. As many "amount * (1 + increment)" as the order further (further_bound) + AND + 2. As many "amount" as the order closer to center (closer_bound) - price = (order['price'] ** -1) - self.log.debug('Cancelling sell order in increase_order_sizes(); ' - 'mode: mountain, quote: {}, price: {:.8f}'.format(order_amount, price)) - self.cancel(order) - self.market_sell(new_order_amount, price) - # Only one increase at a time. This prevents running more than one increment round - # simultaneously - return + Note: for buy orders "amount" is BASE asset amount, and for sell order "amount" is QUOTE. + + Also when making an order it's size always will be limited by available free balance + """ + if asset == 'quote': + total_balance = self.quote_total_balance + order_type = 'sell' elif asset == 'base': - """ Starting from the highest BUY order, for each order, see if it is approximately - maximum size. - If it is, move on to next. - If not, cancel it and replace with maximum size order. Maximum order size will be a - size of higher order. Then return. - If lowest_buy_order is reached, increase it to maximum size. - - Maximum size is: - 1. As many "base * (1 + increment)" as the order below (lower_bound) - AND - 2. As many "base" as the order above (higher_bound) - - Also when making an order it's size always will be limited by available free balance - """ - # Get orders and amounts to be compared. Note: orders are sorted from high price to low - for order in orders: - order_index = orders.index(order) - order_amount = order['base']['amount'] - - # This check prevents choosing order with index lower than the list length - if order_index == 0: - # In case checking the first order, use the same order, but increased by 1 increment - # This allows our highest buy order amount exceed lowest sell order - higher_order = order - higher_bound = higher_order['base']['amount'] * (1 + self.increment) - else: - higher_order = orders[order_index - 1] - higher_bound = higher_order['base']['amount'] - - # This check prevents choosing order with index higher than the list length - if order_index + 1 < len(orders): - # If this is not a lowest_buy_order, lower order is a next order down - lower_order = orders[order_index + 1] - is_least_order = False - else: - # Current order - lower_order = orders[order_index] - is_least_order = True + total_balance = self.base_total_balance + order_type = 'buy' - lower_bound = lower_order['base']['amount'] * (1 + self.increment) + # Get orders and amounts to be compared. Note: orders are sorted from low price to high + for order in orders: + order_index = orders.index(order) + order_amount = order['base']['amount'] - self.log.debug('BASE: lower_bound: {}, order_amount: {}, higher_bound: {}'.format( - lower_bound, order_amount * (1 + self.increment / 10), higher_bound)) + # This check prevents choosing order with index lower than the list length + if order_index == 0: + # In case checking the first order, use the same order, but increased by 1 increment + # This allows our closest order amount exceed highest opposite-side order amount + closer_order = order + closer_bound = closer_order['base']['amount'] * (1 + self.increment) + else: + closer_order = orders[order_index - 1] + closer_bound = closer_order['base']['amount'] + + # This check prevents choosing order with index higher than the list length + if order_index + 1 < len(orders): + # Current order is a not furthest order + further_order = orders[order_index + 1] + is_least_order = False + else: + # Current order is furthest order + further_order = orders[order_index] + is_least_order = True + + further_bound = further_order['base']['amount'] * (1 + self.increment) + + if further_bound > order_amount * (1 + self.increment / 10) < closer_bound: + # Calculate new order size and place the order to the market + new_order_amount = further_bound + + if is_least_order: + new_orders_sum = 0 + amount = order_amount + for o in orders: + amount = amount * (1 + self.increment) + new_orders_sum += amount + # To reduce allocation rounds, increase furthest order more + new_order_amount = order_amount * (total_balance / new_orders_sum) \ + * (1 + self.increment * 0.75) + + if new_order_amount < closer_bound: + """ This is for situations when calculated new_order_amount is not big enough to + allocate all funds. Use partial-increment increase, so we'll got at least one full + increase round. Whether we will just use `new_order_amount = further_bound`, we will + get less than one full allocation round, thus leaving closest-to-center order not + increased. + """ + new_order_amount = closer_bound / (1 + self.increment * 0.2) + + # Limit sell order to available balance + if asset_balance < new_order_amount - order_amount: + new_order_amount = order_amount + asset_balance['amount'] + self.log.info('Limiting new {} order to avail asset balance: {:.8f} {}'.format( + order_type, new_order_amount, asset_balance['symbol'])) - if lower_bound > order_amount * (1 + self.increment / 10) < higher_bound: - # Calculate new order size and place the order to the market - new_base_amount = lower_bound + if asset == 'quote': + price = (order['price'] ** -1) + quote_amount = new_order_amount + elif asset == 'base': price = order['price'] + quote_amount = new_order_amount / price - if is_least_order: - # To reduce allocation rounds, increase furthest order more - new_orders_sum = 0 - amount = order_amount - for o in orders: - amount = amount * (1 + self.increment) - new_orders_sum += amount - new_base_amount = order_amount * (self.base_total_balance / new_orders_sum) \ - * (1 + self.increment * 0.75) - if new_base_amount < higher_bound: - new_base_amount = higher_bound / (1 + self.increment * 0.2) - - # Limit buy order to available balance - if (asset_balance / price) < (new_base_amount - order_amount) / price: - new_base_amount = order_amount + asset_balance['amount'] - self.log.info('Limiting new buy order to avail asset balance: {:.8f} {}'.format( - new_base_amount, asset_balance['symbol'])) - - new_order_amount = new_base_amount / price - self.log.debug('Cancelling buy order in increase_order_sizes(); ' - 'mode: mountain, base: {}, price: {:.8f}'.format(order_amount, order['price'])) - self.cancel(order) - self.market_buy(new_order_amount, price) - # One increase at a time. This prevents running more than one increment round simultaneously. - return + self.log.debug('Cancelling {} order in increase_order_sizes(); ' + 'mode: {}, amount: {}, price: {:.8f}'.format(order_type, self.mode, order_amount, + price)) + self.cancel(order) + if asset == 'quote': + self.market_sell(quote_amount, price) + elif asset == 'base': + self.market_buy(quote_amount, price) + # Only one increase at a time. This prevents running more than one increment round + # simultaneously + return elif self.mode == 'valley': """ Starting from the furthest order, for each order, see if it is approximately maximum size. From 80216e81a53b68fbec669dbc83ecce7bddb80b9a Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 8 Sep 2018 12:51:29 +0500 Subject: [PATCH 0731/1846] Remove fallback order limiting It's not really needed. --- dexbot/strategies/staggered_orders.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 7b2b83edc..41349dbcc 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -429,10 +429,6 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): base_limit = None self.log.debug('Limiting buy order base by opposite order QUOTE asset amount: {}'.format( quote_limit)) - else: - self.log.warning('Using fallback order limiting') - quote_limit = None - base_limit = lowest_sell_order['quote']['amount'] self.place_higher_buy_order(highest_buy_order, quote_limit=quote_limit, base_limit=base_limit, allow_partial=False) elif not self.sell_orders: @@ -526,10 +522,6 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): base_limit = highest_buy_order['base']['amount'] self.log.debug('Limiting sell order by opposite order BASE asset amount: {}'.format( base_limit)) - else: - self.log.warning('Using fallback order limiting') - quote_limit = highest_buy_order['quote']['amount'] - base_limit = None self.place_lower_sell_order(lowest_sell_order, quote_limit=quote_limit, base_limit=base_limit, allow_partial=False) elif not self.buy_orders: From d3eed833e8092b365c5636995d02c07d3b6cac1e Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 8 Sep 2018 12:51:59 +0500 Subject: [PATCH 0732/1846] Implement slope modes --- dexbot/strategies/staggered_orders.py | 51 +++++++++++++++------------ 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 41349dbcc..89afc37e6 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -36,8 +36,8 @@ def configure(cls, return_base_config=True): ('mountain', 'Mountain'), # ('neutral', 'Neutral'), ('valley', 'Valley'), - # ('buy_slope', 'Buy Slope'), - # ('sell_slope', 'Sell Slope') + ('buy_slope', 'Buy Slope'), + ('sell_slope', 'Sell Slope') ] return BaseStrategy.configure(return_base_config) + [ @@ -419,12 +419,12 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): else: # Place order limited by size of the opposite-side order lowest_sell_order = self.sell_orders[0] - if self.mode == 'mountain': + if self.mode == 'mountain' or self.mode == 'buy_slope': quote_limit = None base_limit = lowest_sell_order['quote']['amount'] self.log.debug('Limiting buy order base by opposite order BASE asset amount: {}'.format( base_limit)) - elif self.mode == 'valley': + elif self.mode == 'valley' or self.mode == 'sell_slope': quote_limit = lowest_sell_order['base']['amount'] base_limit = None self.log.debug('Limiting buy order base by opposite order QUOTE asset amount: {}'.format( @@ -512,12 +512,12 @@ def allocate_quote_asset(self, quote_balance, *args, **kwargs): else: # Place order limited by opposite-side order highest_buy_order = self.buy_orders[0] - if self.mode == 'mountain': + if self.mode == 'mountain' or self.mode == 'sell_slope': quote_limit = highest_buy_order['quote']['amount'] base_limit = None self.log.debug('Limiting sell order by opposite order QUOTE asset amount: {}'.format( quote_limit)) - elif self.mode == 'valley': + elif self.mode == 'valley' or self.mode == 'buy_slope': quote_limit = None base_limit = highest_buy_order['base']['amount'] self.log.debug('Limiting sell order by opposite order BASE asset amount: {}'.format( @@ -587,10 +587,12 @@ def increase_order_sizes(self, asset, asset_balance, orders): is started from the closest-to-center order. Buy slope: - Maximize order size as low as possible. Buy orders as far, and sell orders as close as possible to cp. + Maximize order size as low as possible. Buy orders maximized as far as possible (same as valley), and sell + orders as close as possible to cp (same as mountain). Sell slope: - Maximize order size as high as possible. Buy orders as close, and sell orders as far as possible from cp + Maximize order size as high as possible. Buy orders as close (same as mountain), and sell orders as far as + possible from cp (same as valley). :param str | asset: 'base' or 'quote', depending if checking sell or buy :param Amount | asset_balance: Balance of the account @@ -598,7 +600,9 @@ def increase_order_sizes(self, asset, asset_balance, orders): :return None """ # Mountain mode: - if self.mode == 'mountain': + if (self.mode == 'mountain' or + (self.mode == 'buy_slope' and asset == 'quote') or + (self.mode == 'sell_slope' and asset == 'base')): """ Starting from the furthest order. For each order, see if it is approximately maximum size. If it is, move on to next. @@ -695,7 +699,10 @@ def increase_order_sizes(self, asset, asset_balance, orders): # Only one increase at a time. This prevents running more than one increment round # simultaneously return - elif self.mode == 'valley': + elif (self.mode == 'valley' or + (self.mode == 'buy_slope' and asset == 'base') or + (self.mode == 'sell_slope' and asset == 'quote')): + """ Starting from the furthest order, for each order, see if it is approximately maximum size. If it is, move on to next. @@ -805,11 +812,11 @@ def place_higher_buy_order(self, order, place_order=True, allow_partial=False, b self.log.info('Refusing to place an order which crosses lowestAsk') return None - if self.mode == 'mountain': + if self.mode == 'mountain' or self.mode == 'sell_slope': amount = order['quote']['amount'] # How many BASE we need to buy QUOTE `amount` base_amount = amount * price - elif self.mode == 'valley': + elif self.mode == 'valley' or self.mode == 'buy_slope': base_amount = order['base']['amount'] amount = base_amount / price @@ -844,9 +851,9 @@ def place_higher_sell_order(self, order, place_order=True, allow_partial=False): """ price = (order['price'] ** -1) * (1 + self.increment) - if self.mode == 'mountain': + if self.mode == 'mountain' or self.mode == 'buy_slope': amount = order['base']['amount'] / (1 + self.increment) - elif self.mode == 'valley': + elif self.mode == 'valley' or self.mode == 'sell_slope': amount = order['base']['amount'] if amount > self.quote_balance['amount']: @@ -872,9 +879,9 @@ def place_lower_buy_order(self, order, place_order=True, allow_partial=False): """ price = order['price'] / (1 + self.increment) - if self.mode == 'mountain': + if self.mode == 'mountain' or self.mode == 'sell_slope': amount = order['quote']['amount'] - elif self.mode == 'valley': + elif self.mode == 'valley' or self.mode == 'buy_slope': amount = order['base']['amount'] / price # How many BASE we need to buy QUOTE `amount` @@ -910,9 +917,9 @@ def place_lower_sell_order(self, order, place_order=True, allow_partial=False, b price = (order['price'] ** -1) / (1 + self.increment) - if self.mode == 'mountain': + if self.mode == 'mountain' or self.mode == 'buy_slope': amount = order['base']['amount'] * (1 + self.increment) - elif self.mode == 'valley': + elif self.mode == 'valley' or self.mode == 'sell_slope': amount = order['base']['amount'] base_amount = amount * price @@ -960,7 +967,7 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente .format(market_center_price, price, self.upper_bound)) return - if self.mode == 'mountain': + if self.mode == 'mountain' or self.mode == 'buy_slope': previous_price = price orders_sum = 0 amount = quote_balance['amount'] * self.increment @@ -976,7 +983,7 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente price = previous_price amount_quote = previous_amount * (self.quote_total_balance / orders_sum) * (1 + self.increment * 0.75) - elif self.mode == 'valley': + elif self.mode == 'valley' or self.mode == 'sell_slope': orders_count = 0 while price <= self.upper_bound: previous_price = price @@ -1046,7 +1053,7 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p .format(market_center_price, price, self.lower_bound)) return - if self.mode == 'mountain': + if self.mode == 'mountain' or self.mode == 'sell_slope': previous_price = price orders_sum = 0 amount = base_balance['amount'] * self.increment @@ -1062,7 +1069,7 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p amount_base = previous_amount * (self.base_total_balance / orders_sum) * (1 + self.increment * 0.75) price = previous_price amount_quote = amount_base / price - elif self.mode == 'valley': + elif self.mode == 'valley' or self.mode == 'buy_slope': orders_count = 0 while price >= self.lower_bound: previous_price = price From eb61f9268f3a8e05623c4d9671830c9151095ec5 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 8 Sep 2018 14:10:41 +0500 Subject: [PATCH 0733/1846] Allow to control returnOrderId This is needed to be able to place orders in non-blocking mode. --- dexbot/basestrategy.py | 48 ++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index bedd72506..48017fd14 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -194,6 +194,9 @@ def __init__( # Order expiration time in seconds self.expiration = 60 * 60 * 24 * 365 * 5 + # buy/sell actions will return order id by default + self.returnOrderId = 'head' + # A private logger that adds worker identify data to the LogRecord self.log = logging.LoggerAdapter( logging.getLogger('dexbot.per_worker'), @@ -547,7 +550,7 @@ def market_buy(self, quote_amount, price, return_none=False, *args, **kwargs): return None # Make sure we have enough balance for the order - if self.balance(self.market['base']) < base_amount: + if self.returnOrderId and self.balance(self.market['base']) < base_amount: self.log.critical( "Insufficient buy balance, needed {} {}".format( base_amount, symbol) @@ -567,21 +570,24 @@ def market_buy(self, quote_amount, price, return_none=False, *args, **kwargs): Amount(amount=quote_amount, asset=self.market["quote"]), account=self.account.name, expiration=self.expiration, - returnOrderId="head", + returnOrderId=self.returnOrderId, fee_asset=self.fee_asset['id'], *args, **kwargs ) self.log.debug('Placed buy order {}'.format(buy_transaction)) - buy_order = self.get_order(buy_transaction['orderid'], return_none=return_none) - if buy_order and 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, quote_amount, price) - self.recheck_orders = True - return buy_order + if self.returnOrderId: + buy_order = self.get_order(buy_transaction['orderid'], return_none=return_none) + if buy_order and 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, quote_amount, price) + self.recheck_orders = True + return buy_order + else: + return True def market_sell(self, quote_amount, price, return_none=False, *args, **kwargs): symbol = self.market['quote']['symbol'] @@ -595,7 +601,7 @@ def market_sell(self, quote_amount, price, return_none=False, *args, **kwargs): return None # Make sure we have enough balance for the order - if self.balance(self.market['quote']) < quote_amount: + if self.returnOrderId and self.balance(self.market['quote']) < quote_amount: self.log.critical( "Insufficient sell balance, needed {} {}".format( quote_amount, symbol) @@ -615,22 +621,24 @@ def market_sell(self, quote_amount, price, return_none=False, *args, **kwargs): Amount(amount=quote_amount, asset=self.market["quote"]), account=self.account.name, expiration=self.expiration, - returnOrderId="head", + returnOrderId=self.returnOrderId, fee_asset=self.fee_asset['id'], *args, **kwargs ) self.log.debug('Placed sell order {}'.format(sell_transaction)) - sell_order = self.get_order(sell_transaction['orderid'], return_none=return_none) - if sell_order and 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, quote_amount, price) - sell_order.invert() - self.recheck_orders = True - - return sell_order + if self.returnOrderId: + sell_order = self.get_order(sell_transaction['orderid'], return_none=return_none) + if sell_order and 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, quote_amount, price) + sell_order.invert() + self.recheck_orders = True + return sell_order + else: + return True def calculate_order_data(self, order, amount, price): quote_asset = Amount(amount, self.market['quote']['symbol']) From 6c03b2213202fb250fef4ebb1e0e4d715e2a2be6 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 8 Sep 2018 14:14:26 +0500 Subject: [PATCH 0734/1846] Use operations bundling for single maintenance rounds --- dexbot/strategies/staggered_orders.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 89afc37e6..f8dea6e3b 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -126,6 +126,9 @@ def __init__(self, *args, **kwargs): self.start = datetime.now() self.last_check = datetime.now() + # We do not waiting for order ids to be able to bundle operations + self.returnOrderId = None + # Minimal check interval is needed to prevent event queue accumulation self.min_check_interval = 1 self.max_check_interval = 120 @@ -208,6 +211,9 @@ def maintain_strategy(self, *args, **kwargs): # Get ticker data self.ticker = self.market.ticker() + # Prepare to bundle operations into single transaction + self.bitshares.bundle = True + # BASE asset check if self.base_balance > self.base_asset_threshold: base_allocated = False @@ -224,6 +230,11 @@ def maintain_strategy(self, *args, **kwargs): else: quote_allocated = True + # Send pending operations + if not self.bitshares.txbuffer.is_empty(): + self.execute() + self.bitshares.bundle = False + # Greatly increase check interval to lower CPU load whether there is no funds to allocate or we cannot # allocate funds for some reason self.refresh_balances() From 1015b06bba9e7e5953e4245b9b107edf69fc0117 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 8 Sep 2018 23:36:44 +0500 Subject: [PATCH 0735/1846] Refactor order placements in staggered_orders.py Reduce code doubling by using similar methods for base and quote allocation and order placement. --- dexbot/strategies/staggered_orders.py | 499 +++++++++++--------------- 1 file changed, 214 insertions(+), 285 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index f8dea6e3b..9d66197a2 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -218,7 +218,7 @@ def maintain_strategy(self, *args, **kwargs): if self.base_balance > self.base_asset_threshold: base_allocated = False # Allocate available BASE funds - self.allocate_base_asset(self.base_balance) + self.allocate_asset('base', self.base_balance) else: base_allocated = True @@ -226,7 +226,7 @@ def maintain_strategy(self, *args, **kwargs): if self.quote_balance > self.quote_asset_threshold: quote_allocated = False # Allocate available QUOTE funds - self.allocate_quote_asset(self.quote_balance) + self.allocate_asset('quote', self.quote_balance) else: quote_allocated = True @@ -369,33 +369,50 @@ def remove_outside_orders(self, sell_orders, buy_orders): return True - def allocate_base_asset(self, base_balance, *args, **kwargs): - """ Allocates available base asset as buy orders. - :param base_balance: Amount of the base asset available to use - :param args: - :param kwargs: + def allocate_asset(self, asset, asset_balance): + """ Allocates available asset balance as buy or sell orders. + :param str | asset: 'base' or 'quote' + :param Amount | asset_balance: Amount of the asset available to use """ - self.log.debug('Need to allocate base: {}'.format(base_balance)) - if self.buy_orders: - # Get currently the lowest and highest buy orders - lowest_buy_order = self.buy_orders[-1] - highest_buy_order = self.buy_orders[0] - lowest_buy_order_price = lowest_buy_order['price'] + self.log.debug('Need to allocate {}: {}'.format(asset, asset_balance)) + + if asset == 'base': + order_type = 'buy' + symbol = self.base_balance['symbol'] + own_orders = self.buy_orders + opposite_orders = self.sell_orders + opposite_balance = self.quote_balance + opposite_threshold = self.quote_asset_threshold + own_threshold = self.base_asset_threshold + elif asset == 'quote': + order_type = 'sell' + symbol = self.quote_balance['symbol'] + own_orders = self.sell_orders + opposite_orders = self.buy_orders + opposite_balance = self.base_balance + opposite_threshold = self.base_asset_threshold + own_threshold = self.quote_asset_threshold + + if own_orders: + # Get currently the furthest and closest orders + furthest_own_order = own_orders[-1] + closest_own_order = own_orders[0] + furthest_own_order_price = furthest_own_order['price'] # Check if the order size is correct - if self.check_partial_fill(highest_buy_order): + if self.check_partial_fill(closest_own_order): # Calculate actual spread - if self.sell_orders: - lowest_sell_price = self.sell_orders[0]['price'] ** -1 + if opposite_orders: + closest_opposite_price = opposite_orders[0]['price'] ** -1 else: - # For one-sided start, calculate lowest_sell_price empirically - lowest_sell_price = self.market_center_price * (1 + self.target_spread / 2) + # For one-sided start, calculate closest_opposite_price empirically + closest_opposite_price = self.market_center_price * (1 + self.target_spread / 2) - highest_buy_price = highest_buy_order['price'] - self.actual_spread = (lowest_sell_price / highest_buy_price) - 1 + closest_own_price = closest_own_order['price'] + self.actual_spread = (closest_opposite_price / closest_own_price) - 1 if self.actual_spread >= self.target_spread + self.increment: - if self.quote_balance <= self.quote_asset_threshold and self.bootstrapping and self.sell_orders: + if opposite_balance <= opposite_threshold and self.bootstrapping and opposite_orders: """ During the bootstrap we're fist placing orders of some amounts, than we are reaching target spread and then turning bootstrap flag off and starting to allocate remaining balance by gradually increasing order sizes. After bootstrap is complete and following order size @@ -423,159 +440,78 @@ def allocate_base_asset(self, base_balance, *args, **kwargs): 'opposite-side balance') self.bootstrapping = False # Place order closer to the center price - self.log.debug('Placing higher buy order; actual spread: {:.4%}, target + increment: {:.4%}'.format( - self.actual_spread, self.target_spread + self.increment)) + self.log.debug('Placing closer {} order; actual spread: {:.4%}, target + increment: {:.4%}'.format( + order_type, self.actual_spread, self.target_spread + self.increment)) if self.bootstrapping: - self.place_higher_buy_order(highest_buy_order) + self.place_closer_order(asset, closest_own_order) else: # Place order limited by size of the opposite-side order - lowest_sell_order = self.sell_orders[0] - if self.mode == 'mountain' or self.mode == 'buy_slope': - quote_limit = None - base_limit = lowest_sell_order['quote']['amount'] - self.log.debug('Limiting buy order base by opposite order BASE asset amount: {}'.format( - base_limit)) - elif self.mode == 'valley' or self.mode == 'sell_slope': - quote_limit = lowest_sell_order['base']['amount'] - base_limit = None - self.log.debug('Limiting buy order base by opposite order QUOTE asset amount: {}'.format( - quote_limit)) - self.place_higher_buy_order(highest_buy_order, quote_limit=quote_limit, base_limit=base_limit, - allow_partial=False) - elif not self.sell_orders: + closest_opposite_order = opposite_orders[0] + if (self.mode == 'mountain' or + (self.mode == 'buy_slope' and asset == 'base') or + (self.mode == 'sell_slope' and asset == 'quote')): + opposite_asset_limit = None + own_asset_limit = closest_opposite_order['quote']['amount'] + elif (self.mode == 'valley' or + (self.mode == 'buy_slope' and asset == 'quote') or + (self.mode == 'sell_slope' and asset == 'base')): + opposite_asset_limit = closest_opposite_order['base']['amount'] + own_asset_limit = None + self.log.debug('Limiting {} order by opposite order: {} {}'.format( + order_type, own_asset_limit, symbol)) + self.place_closer_order(asset, closest_own_order, own_asset_limit=own_asset_limit, + opposite_asset_limit=opposite_asset_limit, allow_partial=False) + elif not opposite_orders: # Do not try to do anything than placing higher buy whether there is no sell orders return else: - lowest_sell_order = self.sell_orders[0] - if not self.check_partial_fill(lowest_sell_order): + closest_opposite_order = opposite_orders[0] + if not self.check_partial_fill(closest_opposite_order): """ Detect partially filled order on the opposite side and reserve appropriate amount to place - higher buy order + closer order """ - higher_buy_order = self.place_higher_buy_order(highest_buy_order, place_order=False) - funds_to_reserve = higher_buy_order['amount'] * higher_buy_order['price'] + closer_own_order = self.place_closer_order(asset, closest_own_order, place_order=False) + if asset == 'base': + funds_to_reserve = closer_own_order['amount'] * closer_own_order['price'] + elif asset == 'quote': + funds_to_reserve = closer_own_order['amount'] self.log.debug('Partially filled order on opposite side, reserving funds for next buy order: ' - '{:.8f} {}'.format(funds_to_reserve, self.market['base']['symbol'])) - base_balance -= funds_to_reserve - if base_balance > self.base_asset_threshold: - if lowest_buy_order_price / (1 + self.increment) < self.lower_bound: - # Lower bound has been reached and now will start allocating rest of the base balance. + '{:.8f} {}'.format(funds_to_reserve, symbol)) + asset_balance -= funds_to_reserve + if asset_balance > own_threshold: + if ((asset == 'base' and furthest_own_order_price / (1 + self.increment) < self.lower_bound) or + (asset == 'quote' and furthest_own_order_price * (1 + self.increment) > self.upper_bound)): + # Lower/upper bound has been reached and now will start allocating rest of the balance. self.bootstrapping = False - self.log.debug('Increasing orders sizes for BASE asset') - self.increase_order_sizes('base', base_balance, self.buy_orders) + self.log.debug('Increasing sizes of {} orders'.format(order_type)) + self.increase_order_sizes(asset, asset_balance, own_orders) else: - # Lower bound is not reached, we need to add additional orders at the extremes + # Range bound is not reached, we need to add additional orders at the extremes self.bootstrapping = False - self.log.debug('Placing lower order than lowest_buy_order') - self.place_lower_buy_order(lowest_buy_order, allow_partial=True) + self.log.debug('Placing further order than current furthest {} order'.format(order_type)) + self.place_further_order(asset, furthest_own_order, allow_partial=True) else: # Make sure we have enough balance to replace partially filled order - if base_balance + highest_buy_order['for_sale']['amount'] >= highest_buy_order['base']['amount']: - # Cancel highest buy order and immediately replace it with new one. - self.log.info('Replacing partially filled buy order') - self.cancel(highest_buy_order) - self.market_buy(highest_buy_order['quote']['amount'], highest_buy_order['price']) + if asset_balance + closest_own_order['for_sale']['amount'] >= closest_own_order['base']['amount']: + # Cancel closest order and immediately replace it with new one. + self.log.info('Replacing partially filled {} order'.format(order_type)) + self.cancel(closest_own_order) + if asset == 'base': + self.market_buy(closest_own_order['quote']['amount'], closest_own_order['price']) + elif asset == 'quote': + price = closest_own_order['price'] ** -1 + self.market_sell(closest_own_order['base']['amount'], price) self.refresh_balances() else: self.log.debug('Not replacing partially filled order because there is not enough funds') else: # Place first buy order as close to the lower bound as possible self.bootstrapping = True - self.log.debug('Placing first buy order') - self.place_lowest_buy_order(base_balance) - - # Get latest orders - self.refresh_orders() - - def allocate_quote_asset(self, quote_balance, *args, **kwargs): - """ Allocates available quote asset as sell orders. - :param quote_balance: Amount of the base asset available to use - :param args: - :param kwargs: - """ - self.log.debug('Need to allocate quote: {}'.format(quote_balance)) - if self.sell_orders: - lowest_sell_order = self.sell_orders[0] - highest_sell_order = self.sell_orders[-1] - # Sell price is inverted so it can be compared to the upper bound - highest_sell_order_price = (highest_sell_order['price'] ** -1) - - # Check if the order size is correct - if self.check_partial_fill(lowest_sell_order): - # Calculate actual spread - if self.buy_orders: - highest_buy_price = self.buy_orders[0]['price'] - else: - # For one-sided start, calculate highest_buy_price empirically - highest_buy_price = self.market_center_price / (1 + self.target_spread / 2) - lowest_sell_price = lowest_sell_order['price'] ** -1 - self.actual_spread = (lowest_sell_price / highest_buy_price) - 1 - - if self.actual_spread >= self.target_spread + self.increment: - if self.base_balance <= self.base_asset_threshold and self.bootstrapping and self.buy_orders: - self.log.debug('Turning bootstrapping off: actual_spread > target_spread, and not having ' - 'opposite-side balance') - self.bootstrapping = False - # Place order closer to the center price - self.log.debug('Placing lower sell order; actual spread: {:.4%}, target + increment: {:.4%}'.format( - self.actual_spread, self.target_spread + self.increment)) - if self.bootstrapping: - self.place_lower_sell_order(lowest_sell_order) - else: - # Place order limited by opposite-side order - highest_buy_order = self.buy_orders[0] - if self.mode == 'mountain' or self.mode == 'sell_slope': - quote_limit = highest_buy_order['quote']['amount'] - base_limit = None - self.log.debug('Limiting sell order by opposite order QUOTE asset amount: {}'.format( - quote_limit)) - elif self.mode == 'valley' or self.mode == 'buy_slope': - quote_limit = None - base_limit = highest_buy_order['base']['amount'] - self.log.debug('Limiting sell order by opposite order BASE asset amount: {}'.format( - base_limit)) - self.place_lower_sell_order(lowest_sell_order, quote_limit=quote_limit, base_limit=base_limit, - allow_partial=False) - elif not self.buy_orders: - # Do not try to do anything than placing lower sell whether there is no buy orders - return - else: - highest_buy_order = self.buy_orders[0] - if not self.check_partial_fill(highest_buy_order): - """ Detect partially filled order on the opposite side and reserve appropriate amount to place - lower sell order - """ - lower_sell_order = self.place_lower_sell_order(lowest_sell_order, place_order=False) - funds_to_reserve = lower_sell_order['amount'] - self.log.debug('Partially filled order on opposite side, reserving funds for next sell order: ' - '{:.8f} {}'.format(funds_to_reserve, self.market['quote']['symbol'])) - quote_balance -= funds_to_reserve - if quote_balance > self.quote_asset_threshold: - if highest_sell_order_price * (1 + self.increment) > self.upper_bound: - # Upper bound has been reached and now will start allocating rest of the quote balance. - self.bootstrapping = False - self.log.debug('Increasing orders sizes for QUOTE asset') - if quote_balance > self.quote_asset_threshold: - self.increase_order_sizes('quote', quote_balance, self.sell_orders) - else: - # Higher bound is not reached, we need to add additional orders at the extremes - self.bootstrapping = False - self.place_higher_sell_order(highest_sell_order, allow_partial=True) - else: - # Make sure we have enough balance to replace partially filled order - if quote_balance + lowest_sell_order['for_sale']['amount'] >= lowest_sell_order['base']['amount']: - # Cancel lowest sell order and immediately replace it with new one. - self.log.info('Replacing partially filled sell order') - self.cancel(lowest_sell_order) - price = lowest_sell_order['price'] ** -1 - self.market_sell(lowest_sell_order['base']['amount'], price) - self.refresh_balances() - else: - self.log.debug('Not replacing partially filled order because there is not enough funds') - else: - # Place first order as close to the upper bound as possible - self.bootstrapping = True - self.log.debug('Placing first sell order') - self.place_highest_sell_order(quote_balance) + self.log.debug('Placing first {} order'.format(order_type)) + if asset == 'base': + self.place_lowest_buy_order(asset_balance) + elif asset == 'quote': + self.place_highest_sell_order(asset_balance) # Get latest orders self.refresh_orders() @@ -781,10 +717,6 @@ def increase_order_sizes(self, asset, asset_balance, orders): elif self.mode == 'neutral': pass - elif self.mode == 'buy_slope': - pass - elif self.mode == 'sell_slope': - pass return None def check_partial_fill(self, order): @@ -803,160 +735,157 @@ def check_partial_fill(self, order): return False return True - def place_higher_buy_order(self, order, place_order=True, allow_partial=False, base_limit=None, quote_limit=None): - """ Place higher buy order + def place_closer_order(self, asset, order, place_order=True, allow_partial=False, own_asset_limit=None, + opposite_asset_limit=None): + """ Place order closer to the center - :param order: Previously highest buy order + :param order: Previously closest order :param bool | place_order: True = Places order to the market, False = returns amount and price :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance - :param float | base_limit: order should be limited in size by this BASE amount - :param float | quote_limit: order should be limited in size by this QUOTE amount + :param float | own_asset_limit: order should be limited in size by amount of order's "base" + :param float | opposite_asset_limit: order should be limited in size by order's "quote" amount + """ - if base_limit and quote_limit: - self.log.error('Only base_limit or quote_limit should be specified') + if own_asset_limit and opposite_asset_limit: + self.log.error('Only own_asset_limit or opposite_asset_limit should be specified') self.disabled = True return None + # Define asset-dependent variables + if asset == 'base': + order_type = 'buy' + balance = self.base_balance['amount'] + symbol = self.base_balance['symbol'] + elif asset == 'quote': + order_type = 'sell' + balance = self.quote_balance['amount'] + symbol = self.quote_balance['symbol'] + + # Check for instant fill + if asset == 'base': + price = order['price'] * (1 + self.increment) + if not self.is_instant_fill_enabled and price > float(self.ticker['lowestAsk']): + self.log.info('Refusing to place an order which crosses lowestAsk') + return None + elif asset == 'quote': + price = (order['price'] ** -1) / (1 + self.increment) + if not self.is_instant_fill_enabled and price < float(self.ticker['highestBid']): + self.log.info('Refusing to place an order which crosses highestBid') + return None + + # For next steps we do not need inverted price for sell orders price = order['price'] * (1 + self.increment) - if not self.is_instant_fill_enabled and place_order and price > float(self.ticker['lowestAsk']): - self.log.info('Refusing to place an order which crosses lowestAsk') - return None - - if self.mode == 'mountain' or self.mode == 'sell_slope': - amount = order['quote']['amount'] - # How many BASE we need to buy QUOTE `amount` - base_amount = amount * price - elif self.mode == 'valley' or self.mode == 'buy_slope': - base_amount = order['base']['amount'] - amount = base_amount / price - - if base_limit and base_limit < base_amount: - base_amount = base_limit - amount = base_limit / price - elif quote_limit and quote_limit < amount: - # Limit order amount only when it is lower than amount - base_amount = quote_limit * price - amount = quote_limit - - if base_amount > self.base_balance['amount']: + # Calculate new order amounts depending on mode + if (self.mode == 'mountain' or + (self.mode == 'buy_slope' and asset == 'quote') or + (self.mode == 'sell_slope' and asset == 'base')): + opposite_asset_amount = order['quote']['amount'] + own_asset_amount = opposite_asset_amount * price + elif (self.mode == 'valley' or + (self.mode == 'buy_slope' and asset == 'base') or + (self.mode == 'sell_slope' and asset == 'quote')): + own_asset_amount = order['base']['amount'] + opposite_asset_amount = own_asset_amount / price + + # Apply limits. Limit order only whether passed limit is less than expected order size + if own_asset_limit and own_asset_limit < own_asset_amount: + own_asset_amount = own_asset_limit + opposite_asset_amount = own_asset_amount / price + elif opposite_asset_limit and opposite_asset_limit < opposite_asset_amount: + opposite_asset_amount = opposite_asset_limit + own_asset_amount = opposite_asset_amount * price + + if asset == 'base': + # Define amounts in terms of BASE and QUOTE + base_amount = own_asset_amount + quote_amount = opposite_asset_amount + limiter = base_amount + elif asset == 'quote': + base_amount = opposite_asset_amount + quote_amount = own_asset_amount + limiter = quote_amount + price = price ** -1 + + # Check whether new order will exceed available balance + if balance < limiter: if place_order and not allow_partial: - self.log.debug('Not enough balance to place_higher_buy_order; need/avail: {:.8f}/{:.8f}' - .format(base_amount, self.base_balance['amount'])) + self.log.debug('Not enough balance to place closer {} order; need/avail: {:.8f}/{:.8f}' + .format(order_type, limiter, balance)) place_order = False elif allow_partial: - self.log.debug('Limiting order amount to available balance: {}'.format(self.base_balance['amount'])) - amount = self.base_balance['amount'] / price + self.log.debug('Limiting {} order amount to available asset balance: {} {}'.format( + balance, symbol)) + if asset == 'base': + quote_amount = balance / price + elif asset == 'quote': + quote_amount = balance - if place_order: - self.market_buy(amount, price) + if place_order and asset == 'base': + self.market_buy(quote_amount, price) + elif place_order and asset == 'quote': + self.market_sell(quote_amount, price) - return {"amount": amount, "price": price} + return {"amount": quote_amount, "price": price} - def place_higher_sell_order(self, order, place_order=True, allow_partial=False): - """ Place higher sell order + def place_further_order(self, asset, order, place_order=True, allow_partial=False): + """ Place order further from specified order - :param order: highest_sell_order + :param order: furthest buy or sell order :param bool | place_order: True = Places order to the market, False = returns amount and price :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance """ - price = (order['price'] ** -1) * (1 + self.increment) - - if self.mode == 'mountain' or self.mode == 'buy_slope': - amount = order['base']['amount'] / (1 + self.increment) - elif self.mode == 'valley' or self.mode == 'sell_slope': - amount = order['base']['amount'] - - if amount > self.quote_balance['amount']: - if place_order and not allow_partial: - self.log.debug('Not enough balance to place_higher_sell_order; need/avail: {:.8f}/{:.8f}' - .format(amount, self.quote_balance['amount'])) - place_order = False - elif allow_partial: - self.log.debug('Limiting order amount to available balance: {}'.format(self.quote_balance['amount'])) - amount = self.quote_balance['amount'] - if place_order: - self.market_sell(amount, price) + # Define asset-dependent variables + if asset == 'base': + order_type = 'buy' + balance = self.base_balance['amount'] + symbol = self.base_balance['symbol'] + elif asset == 'quote': + order_type = 'sell' + balance = self.quote_balance['amount'] + symbol = self.quote_balance['symbol'] - return {"amount": amount, "price": price} - - def place_lower_buy_order(self, order, place_order=True, allow_partial=False): - """ Place lower buy order - - :param order: Previously lowest buy order - :param bool | place_order: True = Places order to the market, False = returns amount and price - :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance - """ price = order['price'] / (1 + self.increment) - if self.mode == 'mountain' or self.mode == 'sell_slope': - amount = order['quote']['amount'] - elif self.mode == 'valley' or self.mode == 'buy_slope': - amount = order['base']['amount'] / price - - # How many BASE we need to buy QUOTE `amount` - base_amount = amount * price - - if base_amount > self.base_balance['amount']: - if place_order and not allow_partial: - self.log.debug('Not enough balance to place_lower_buy_order; need/avail: {:.8f}/{:.8f}' - .format(base_amount, self.base_balance['amount'])) - place_order = False - elif allow_partial: - self.log.debug('Limiting order amount to available balance: {}'.format(self.base_balance['amount'])) - amount = self.base_balance['amount'] / price - - if place_order: - self.market_buy(amount, price) - else: - return {"amount": amount, "price": price} - - def place_lower_sell_order(self, order, place_order=True, allow_partial=False, base_limit=None, quote_limit=None): - """ Place lower sell order - - :param order: Previously higher sell order - :param bool | place_order: True = Places order to the market, False = returns amount and price - :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance - :param float | base_limit: order should be limited in size by this BASE amount - :param float | quote_limit: order should be limited in size by this QUOTE amount - """ - if base_limit and quote_limit: - self.log.error('Only base_limit or quote_limit should be specified') - self.disabled = True - return None - - price = (order['price'] ** -1) / (1 + self.increment) - + # Calculate new order amounts depending on mode if self.mode == 'mountain' or self.mode == 'buy_slope': - amount = order['base']['amount'] * (1 + self.increment) - elif self.mode == 'valley' or self.mode == 'sell_slope': - amount = order['base']['amount'] - - base_amount = amount * price - - if not self.is_instant_fill_enabled and place_order and price < float(self.ticker['highestBid']): - self.log.info('Refusing to place an order which crosses highestBid') - return None - - if base_limit and base_limit < base_amount: - amount = base_limit / price - elif quote_limit and quote_limit < amount: - amount = quote_limit - - if amount > self.quote_balance['amount']: + opposite_asset_amount = order['quote']['amount'] + own_asset_amount = opposite_asset_amount * price + elif self.mode == 'valley' or self.mode == 'buy_slope': + own_asset_amount = order['base']['amount'] + opposite_asset_amount = own_asset_amount / price + + if asset == 'base': + base_amount = own_asset_amount + quote_amount = opposite_asset_amount + limiter = base_amount + elif asset == 'quote': + base_amount = opposite_asset_amount + quote_amount = own_asset_amount + limiter = quote_amount + price = price ** -1 + + # Check whether new order will exceed available balance + if balance < limiter: if place_order and not allow_partial: - self.log.debug('Not enough balance to place_lower_sell_order; need/avail: {:.8f}/{:.8f}' - .format(amount, self.quote_balance['amount'])) + self.log.debug('Not enough balance to place furthest {} order; need/avail: {:.8f}/{:.8f}' + .format(order_type, limiter, balance)) place_order = False elif allow_partial: - self.log.debug('Limiting order amount to available balance: {}'.format(self.quote_balance['amount'])) - amount = self.quote_balance['amount'] - - if place_order: - self.market_sell(amount, price) - - return {"amount": amount, "price": price} + self.log.debug('Limiting {} order amount to available asset balance: {} {}'.format( + balance, symbol)) + if asset == 'base': + quote_amount = balance / price + elif asset == 'quote': + quote_amount = balance + + if place_order and asset == 'base': + self.market_buy(quote_amount, price) + elif place_order and asset == 'quote': + self.market_sell(quote_amount, price) + + return {"amount": quote_amount, "price": price} def place_highest_sell_order(self, quote_balance, place_order=True, market_center_price=None): """ Places sell order furthest to the market center price From 399879e9070d10cf4c6ea1410280bb0820e68630 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 9 Sep 2018 18:54:40 +0500 Subject: [PATCH 0736/1846] Use string formatting to round price --- dexbot/basestrategy.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 48017fd14..5e0f10047 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -559,8 +559,8 @@ def market_buy(self, quote_amount, price, return_none=False, *args, **kwargs): return None self.log.info( - 'Placing a buy order for {} {} @ {}'.format( - base_amount, symbol, round(price, 8)) + 'Placing a buy order for {} {} @ {:.8f}'.format( + base_amount, symbol, price) ) # Place the order @@ -610,8 +610,8 @@ def market_sell(self, quote_amount, price, return_none=False, *args, **kwargs): return None self.log.info( - 'Placing a sell order for {} {} @ {}'.format( - quote_amount, symbol, round(price, 8)) + 'Placing a sell order for {} {} @ {:.8f}'.format( + quote_amount, symbol, price) ) # Place the order From 72088422a7a57bf6dc48eb9d79f0fb6bc502dd22 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 10 Sep 2018 00:14:24 +0500 Subject: [PATCH 0737/1846] Fix debug messages when limiting order --- dexbot/strategies/staggered_orders.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 9d66197a2..11e9580ad 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -452,13 +452,15 @@ def allocate_asset(self, asset, asset_balance): (self.mode == 'sell_slope' and asset == 'quote')): opposite_asset_limit = None own_asset_limit = closest_opposite_order['quote']['amount'] + self.log.debug('Limiting {} order by opposite order: {} {}'.format( + order_type, own_asset_limit, symbol)) elif (self.mode == 'valley' or (self.mode == 'buy_slope' and asset == 'quote') or (self.mode == 'sell_slope' and asset == 'base')): opposite_asset_limit = closest_opposite_order['base']['amount'] own_asset_limit = None - self.log.debug('Limiting {} order by opposite order: {} {}'.format( - order_type, own_asset_limit, symbol)) + self.log.debug('Limiting {} order by opposite order: {} {}'.format( + order_type, opposite_asset_limit, symbol)) self.place_closer_order(asset, closest_own_order, own_asset_limit=own_asset_limit, opposite_asset_limit=opposite_asset_limit, allow_partial=False) elif not opposite_orders: From de53c98745041550939c33a149af5ee8da2fcd4a Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 10 Sep 2018 00:14:48 +0500 Subject: [PATCH 0738/1846] Fix order type in debug message --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 11e9580ad..ba6a97c55 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -477,8 +477,8 @@ def allocate_asset(self, asset, asset_balance): funds_to_reserve = closer_own_order['amount'] * closer_own_order['price'] elif asset == 'quote': funds_to_reserve = closer_own_order['amount'] - self.log.debug('Partially filled order on opposite side, reserving funds for next buy order: ' - '{:.8f} {}'.format(funds_to_reserve, symbol)) + self.log.debug('Partially filled order on opposite side, reserving funds for next {} order: ' + '{:.8f} {}'.format(order_type, funds_to_reserve, symbol)) asset_balance -= funds_to_reserve if asset_balance > own_threshold: if ((asset == 'base' and furthest_own_order_price / (1 + self.increment) < self.lower_bound) or From 4e300b89a6bc506a82257c6bee130b19f8ce3f52 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 10 Sep 2018 00:16:02 +0500 Subject: [PATCH 0739/1846] Improve order increases Increase order only whether it's amount is differ from closer order by 50% of increment. This will reduce allocation rounds drastically. --- dexbot/strategies/staggered_orders.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index ba6a97c55..c58039b9a 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -601,7 +601,8 @@ def increase_order_sizes(self, asset, asset_balance, orders): further_bound = further_order['base']['amount'] * (1 + self.increment) - if further_bound > order_amount * (1 + self.increment / 10) < closer_bound: + if (further_bound > order_amount * (1 + self.increment / 10) < closer_bound and + further_bound - order_amount >= order_amount * self.increment / 2): # Calculate new order size and place the order to the market new_order_amount = further_bound @@ -692,7 +693,13 @@ def increase_order_sizes(self, asset, asset_balance, orders): # Maximize order up to max possible amount if we can closer_order_bound = new_amount - if order_amount * (1 + self.increment / 10) < closer_order_bound: + """ Check whether order amount is less than closer order and the diff is more than 50% of one increment + Note: we can use only 50% or less diffs. Bigger will not work. For example, with diff 80% an order + may have an actual difference like 30% from closer and 70% from further. + """ + if (order_amount * (1 + self.increment / 10) < closer_order_bound and + closer_order_bound - order_amount >= order_amount * self.increment / 2): + amount_base = closer_order_bound # Limit order to available balance From 93f95310d71106e01dac6f2f5c0f52132ddd1a67 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 10 Sep 2018 23:42:35 +0500 Subject: [PATCH 0740/1846] Fix arguments in string formatting --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c58039b9a..2afdbdd74 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -824,7 +824,7 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False place_order = False elif allow_partial: self.log.debug('Limiting {} order amount to available asset balance: {} {}'.format( - balance, symbol)) + order_type, balance, symbol)) if asset == 'base': quote_amount = balance / price elif asset == 'quote': @@ -883,7 +883,7 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals place_order = False elif allow_partial: self.log.debug('Limiting {} order amount to available asset balance: {} {}'.format( - balance, symbol)) + order_type, balance, symbol)) if asset == 'base': quote_amount = balance / price elif asset == 'quote': From bd7385d66464f9435ec51031f3041a4bf15fd7cd Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 10 Sep 2018 23:52:16 +0500 Subject: [PATCH 0741/1846] Fix placing of range-expanding orders for quote asset --- dexbot/strategies/staggered_orders.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 2afdbdd74..2786075e5 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -398,6 +398,8 @@ def allocate_asset(self, asset, asset_balance): furthest_own_order = own_orders[-1] closest_own_order = own_orders[0] furthest_own_order_price = furthest_own_order['price'] + if asset == 'quote': + furthest_own_order_price = furthest_own_order_price ** -1 # Check if the order size is correct if self.check_partial_fill(closest_own_order): From 302b3075d0ad345aeecf671e136c9410628f58fc Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 11 Sep 2018 21:24:14 +0500 Subject: [PATCH 0742/1846] Small optimization by reducing same var definition --- dexbot/strategies/staggered_orders.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 2786075e5..a701b07a1 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -401,11 +401,12 @@ def allocate_asset(self, asset, asset_balance): if asset == 'quote': furthest_own_order_price = furthest_own_order_price ** -1 - # Check if the order size is correct + # Check if the order was partially filled if self.check_partial_fill(closest_own_order): # Calculate actual spread if opposite_orders: - closest_opposite_price = opposite_orders[0]['price'] ** -1 + closest_opposite_order = opposite_orders[0] + closest_opposite_price = closest_opposite_order['price'] ** -1 else: # For one-sided start, calculate closest_opposite_price empirically closest_opposite_price = self.market_center_price * (1 + self.target_spread / 2) @@ -448,7 +449,6 @@ def allocate_asset(self, asset, asset_balance): self.place_closer_order(asset, closest_own_order) else: # Place order limited by size of the opposite-side order - closest_opposite_order = opposite_orders[0] if (self.mode == 'mountain' or (self.mode == 'buy_slope' and asset == 'base') or (self.mode == 'sell_slope' and asset == 'quote')): @@ -469,7 +469,6 @@ def allocate_asset(self, asset, asset_balance): # Do not try to do anything than placing higher buy whether there is no sell orders return else: - closest_opposite_order = opposite_orders[0] if not self.check_partial_fill(closest_opposite_order): """ Detect partially filled order on the opposite side and reserve appropriate amount to place closer order From 681ffae78166b6748657af8f845a0a3277f33a94 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 11 Sep 2018 22:37:33 +0500 Subject: [PATCH 0743/1846] Add note describing one rare condition --- dexbot/strategies/staggered_orders.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index a701b07a1..e9497849f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -415,6 +415,14 @@ def allocate_asset(self, asset, asset_balance): self.actual_spread = (closest_opposite_price / closest_own_price) - 1 if self.actual_spread >= self.target_spread + self.increment: + """ Note: because we're using operations batching, there is possible a situation when we will have + both free balances and `self.actual_spread >= self.target_spread + self.increment`. In such case + there will be TWO orders placed, one buy and one sell despite only one would be enough to reach + target spread. Sure, we can add a workaround for that by overriding `closest_opposite_price` for + second call of allocate_asset(). We are not doing this because we're not doing assumption on + which side order (buy or sell) should be placed first. So, when placing two closer orders from + both sides, spread will be no less than `target_spread - increment`, thus not making any loss. + """ if opposite_balance <= opposite_threshold and self.bootstrapping and opposite_orders: """ During the bootstrap we're fist placing orders of some amounts, than we are reaching target spread and then turning bootstrap flag off and starting to allocate remaining balance by From f54fa91d343a15ab4b649da5298fa88627d1c590 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 13 Sep 2018 00:11:31 +0500 Subject: [PATCH 0744/1846] Refactor keeping track of balance changes across maintenance We need to keep several balance "snapshots" across maintenance runs to be able to use triggers based on these changes. --- dexbot/strategies/staggered_orders.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index e9497849f..4a418801f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -120,6 +120,9 @@ def __init__(self, *args, **kwargs): self.ticker = None self.quote_asset_threshold = 0 self.base_asset_threshold = 0 + # Initial balance history elements should not be equal to avoid immediate bootstrap turn off + self.quote_balance_history = [1, 2, 3] + self.base_balance_history = [1, 2, 3] # Order expiration time self.expiration = 60 * 60 * 24 * 365 * 5 @@ -183,11 +186,6 @@ def maintain_strategy(self, *args, **kwargs): # Calculate balances self.refresh_balances() - # Save current balances for further checks. - # Save exactly key value instead of full key because it may be modified later on. - previous_base_balance = self.base_balance['amount'] - previous_quote_balance = self.quote_balance['amount'] - # Calculate asset thresholds self.quote_asset_threshold = self.quote_total_balance / 20000 self.base_asset_threshold = self.base_total_balance / 20000 @@ -235,19 +233,27 @@ def maintain_strategy(self, *args, **kwargs): self.execute() self.bitshares.bundle = False + # Maintain the history of free balances after maintenance runs. + # Save exactly key values instead of full key because it may be modified later on. + self.refresh_balances() + self.base_balance_history.append(self.base_balance['amount']) + self.quote_balance_history.append(self.quote_balance['amount']) + if len(self.base_balance_history) > 3: + del self.base_balance_history[0] + del self.quote_balance_history[0] + # Greatly increase check interval to lower CPU load whether there is no funds to allocate or we cannot # allocate funds for some reason - self.refresh_balances() if (self.current_check_interval == self.min_check_interval and - previous_base_balance == self.base_balance['amount'] and - previous_quote_balance == self.quote_balance['amount']): + self.base_balance_history[1] == self.base_balance_history[2] and + self.quote_balance_history[1] == self.quote_balance_history[2]): # Balance didn't changed, so we can reduce maintenance frequency self.log.debug('Raising check interval up to {} seconds to reduce CPU usage'.format( self.max_check_interval)) self.current_check_interval = self.max_check_interval elif (self.current_check_interval == self.max_check_interval and - (previous_base_balance != self.base_balance['amount'] or - previous_quote_balance != self.quote_balance['amount'])): + (self.base_balance_history[1] != self.base_balance_history[2] or + self.quote_balance_history[1] != self.quote_balance_history[2])): # Balance changed, increase maintenance frequency to allocate more quickly self.log.debug('Reducing check interval to {} seconds because of changed ' 'balances'.format(self.min_check_interval)) From 601b23f6052f44aa9fdd96bb5c852fb761a17e75 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 13 Sep 2018 00:14:19 +0500 Subject: [PATCH 0745/1846] Implement additional trigger to turn bootstrap off Whether we are in bootstrap mode, balance allocations should happen at every maintenance. Whether they are not happen and balances keeping unchanged, we may assume that we actually not bootstrapping but just working after bot restart. Thus, we may safely turn bootstrap off. --- dexbot/strategies/staggered_orders.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 4a418801f..fa3d9c0bb 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -456,6 +456,14 @@ def allocate_asset(self, asset, asset_balance): self.log.debug('Turning bootstrapping off: actual_spread > target_spread, and not having ' 'opposite-side balance') self.bootstrapping = False + elif (self.bootstrapping and + self.base_balance_history[2] == self.base_balance_history[0] and + self.quote_balance_history[2] == self.quote_balance_history[0]): + # Turn off bootstrap mode whether we're didn't allocated assets during previos 3 maintenances + self.log.debug('Turning bootstrapping off: actual_spread > target_spread, we have free ' + 'balances and cannot allocate them normally 3 times in a row') + self.bootstrapping = False + # Place order closer to the center price self.log.debug('Placing closer {} order; actual spread: {:.4%}, target + increment: {:.4%}'.format( order_type, self.actual_spread, self.target_spread + self.increment)) From e0d9c3df2f7cdfb2481dd8645c8166e324379ba8 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 08:04:22 +0300 Subject: [PATCH 0746/1846] Refactor variables in get_market_center_price() --- dexbot/strategies/base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 002de14cc..d55ba207a 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -570,23 +570,23 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors :return: Market center price as float """ - highest_buy_order = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount) - lowest_sell_order = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount) + buy_price = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount) + sell_price = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount) - if highest_buy_order is None or highest_buy_order == 0.0: + if buy_price is None or buy_price == 0.0: if not suppress_errors: self.log.critical("Cannot estimate center price, there is no highest bid.") self.disabled = True return None - if lowest_sell_order is None or lowest_sell_order == 0.0: + if sell_price is None or sell_price == 0.0: if not suppress_errors: self.log.critical("Cannot estimate center price, there is no lowest ask.") self.disabled = True return None # Calculate and return market center price - return highest_buy_order * math.sqrt(lowest_sell_order / highest_buy_order) + return buy_price * math.sqrt(sell_price / buy_price) def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0): """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with From 6d0e9084fb24a8b7581c22e356a2b6c7c44f3e47 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 08:05:11 +0300 Subject: [PATCH 0747/1846] Remove todo comments --- dexbot/strategies/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index d55ba207a..950e11cd1 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -635,7 +635,7 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, quote_amount = 0 base_amount = 0 - # Todo: Work in progress, THIS IS EXPERIEMENTAL WAY TO CALCULATE THIS. Revert if incorrect. + for order in market_buy_orders: if base: # BASE amount was given @@ -697,7 +697,6 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, quote_amount = 0 base_amount = 0 - # Todo: Work in progress, THIS IS EXPERIEMENTAL WAY TO CALCULATE THIS. Revert if incorrect. for order in market_sell_orders: if quote: # QUOTE amount was given From fea24d6edc18dea3ad055fbf942a4f603391f312 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 10:31:51 +0300 Subject: [PATCH 0748/1846] Remove base for restore_order() --- dexbot/strategies/base.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 950e11cd1..a36a5041a 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1025,15 +1025,6 @@ def place_market_sell_order(self, amount, price, return_none=False, *args, **kwa return sell_order - def restore_order(self, order): - """ If an order is partially or completely filled, this will make a new order of original size and price. - - :param order: - :return: - """ - # Todo: Insert logic here - # Todo: Is this something that is commonly used and thus needed? - def retry_action(self, action, *args, **kwargs): """ Perform an action, and if certain suspected-to-be-spurious grapheme bugs occur, instead of bubbling the exception, it is quietly logged (level WARN), and try again From 9a1dfdbf667247f6baff43fff54909a0abf71b12 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 10:32:35 +0300 Subject: [PATCH 0749/1846] Change get_market_buy_price() --- dexbot/strategies/base.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index a36a5041a..de4b20b53 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -588,30 +588,14 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors # Calculate and return market center price return buy_price * math.sqrt(sell_price / buy_price) - def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0): + def get_market_buy_price(self, quote_amount=0, base_amount=0): """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with moving average or weighted moving average :param float | quote_amount: :param float | base_amount: - :param int | moving_average: Count of orders to be taken in to the calculations - :param int | weighted_moving_average: Count of orders to be taken in to the calculations :return: """ - """ - Buy orders: - 10 CODACOIN for 20 TEST - 15 CODACOIN for 30 TEST - 20 CODACOIN for 40 TEST - - (price + price + price) / moving_average - moving average = (2 + 3 + 4) / 3 = 3 - - ((amount * price) + (amount * price) + (amount * price)) / amount_total - weighted moving average = ((10 * 2) + (15 * 3) + (20 * 4)) / 45 = 3,222222 - - """ - # Todo: Work in progress # Like get_market_sell_price(), but defaulting to base_amount if both base and quote are specified. # In case amount is not given, return price of the lowest sell order on the market if quote_amount == 0 and base_amount == 0: @@ -635,6 +619,7 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, quote_amount = 0 base_amount = 0 + missing_amount = target_amount for order in market_buy_orders: if base: @@ -642,11 +627,21 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, if base_amount < target_amount: quote_amount += order['quote']['amount'] base_amount += order['base']['amount'] + missing_amount -= order['base']['amount'] + elif base_amount > missing_amount: + base_amount += missing_amount + quote_amount += missing_amount / order['price'] + break elif not base: # QUOTE amount was given if quote_amount < target_amount: quote_amount += order['quote']['amount'] base_amount += order['base']['amount'] + missing_amount -= order['quote']['amount'] + elif quote_amount > missing_amount: + base_amount += missing_amount * order['price'] + quote_amount += missing_amount + break return base_amount / quote_amount From 1f981f55f669c4d6e07cbb05c60a3156e58671e2 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 10:33:08 +0300 Subject: [PATCH 0750/1846] Change get_market_sell_price() --- dexbot/strategies/base.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index de4b20b53..96f49e482 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -656,7 +656,7 @@ def get_market_orders(self, depth=1): """ return self.market.orderbook(depth) - def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0): + def get_market_sell_price(self, quote_amount=0, base_amount=00): """ Returns the BASE/QUOTE price for which [quote_amount] worth of QUOTE could be bought, enhanced with moving average or weighted moving average. @@ -664,11 +664,8 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, :param float | quote_amount: :param float | base_amount: - :param float | moving_average: - :param float | weighted_moving_average: :return: """ - # Todo: Work in progress # In case amount is not given, return price of the lowest sell order on the market if quote_amount == 0 and base_amount == 0: return self.ticker.get('lowestAsk') @@ -691,6 +688,7 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, quote_amount = 0 base_amount = 0 + missing_amount = target_amount for order in market_sell_orders: if quote: @@ -698,11 +696,22 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, if quote_amount < target_amount: quote_amount += order['quote']['amount'] base_amount += order['base']['amount'] + missing_amount -= order['quote']['amount'] + elif quote_amount > missing_amount: + base_amount += missing_amount * order['price'] + quote_amount += missing_amount + break + elif not quote: # BASE amount was given if base_amount < target_amount: quote_amount += order['quote']['amount'] base_amount += order['base']['amount'] + missing_amount -= order['base']['amount'] + elif base_amount > missing_amount: + base_amount += missing_amount + quote_amount += missing_amount / order['price'] + break return base_amount / quote_amount From 982fb5a8da9d9d74df75d152f99cfaeb98b76884 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 10:34:52 +0300 Subject: [PATCH 0751/1846] Change fee asset for get_x_fee functions to BTS --- dexbot/strategies/base.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 96f49e482..97ce9c14f 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -743,8 +743,7 @@ def get_order_cancellation_fee(self, fee_asset): limit_order_cancel = fees['limit_order_cancel'] # Convert fee - # Todo: Change 'TEST' to 'BTS' - return self.convert_asset(limit_order_cancel['fee'], 'TEST', fee_asset) + return self.convert_asset(limit_order_cancel['fee'], 'BTS', fee_asset) def get_order_creation_fee(self, fee_asset): """ Returns the cost of creating an order in the asset specified @@ -757,8 +756,7 @@ def get_order_creation_fee(self, fee_asset): limit_order_create = fees['limit_order_create'] # Convert fee - # Todo: Change 'TEST' to 'BTS' - return self.convert_asset(limit_order_create['fee'], 'TEST', fee_asset) + return self.convert_asset(limit_order_create['fee'], 'BTS', fee_asset) def filter_buy_orders(self, orders, sort=None): """ Return own buy orders from list of orders. Can be used to pick buy orders from a list From 30ea21cd2f22a7d3db51ff93b25e3bc1c72ce745 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 10:35:12 +0300 Subject: [PATCH 0752/1846] Change comments on functions --- dexbot/strategies/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 97ce9c14f..9b72ce34c 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -802,7 +802,7 @@ def filter_sell_orders(self, orders, sort=None): return sell_orders def get_own_buy_orders(self, orders=None): - """ Get own buy orders from current market + """ Get own buy orders from current market, or from a set of orders passed for this function. :return: List of buy orders """ @@ -1212,7 +1212,6 @@ def get_updated_limit_order(limit_order): @staticmethod def purge_all_local_worker_data(worker_name): - # Todo: Confirm this being correct """ Removes worker's data and orders from local sqlite database :param worker_name: Name of the worker to be removed From 7efc45a6b5e693a43011145dcd2782cfa415a7e9 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 13:36:22 +0300 Subject: [PATCH 0753/1846] Change get_own_spread() by removing depth --- dexbot/strategies/base.py | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 9b72ce34c..6567237b5 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -823,27 +823,18 @@ def get_own_sell_orders(self, orders=None): return self.filter_sell_orders(orders) - def get_own_spread(self, highest_own_buy_price=None, lowest_own_sell_price=None, depth=0, refresh=False): + def get_own_spread(self): """ Returns the difference between own closest opposite orders. - :param float | highest_own_buy_price: - :param float | lowest_own_sell_price: - :param float | depth: Use most resent data from Bitshares - :param bool | refresh: :return: float or None: Own spread """ - # Todo: Add depth - if refresh: - try: - # Try fetching own orders - highest_own_buy_price = self.get_highest_market_buy_order().get('price') - lowest_own_sell_price = self.get_lowest_own_sell_order().get('price') - except AttributeError: - return None - else: - # If orders are given, use them instead newest data from the blockchain - highest_own_buy_price = highest_own_buy_price - lowest_own_sell_price = lowest_own_sell_price + + try: + # Try fetching own orders + highest_own_buy_price = self.get_highest_market_buy_order().get('price') + lowest_own_sell_price = self.get_lowest_own_sell_order().get('price') + except AttributeError: + return None # Calculate actual spread actual_spread = lowest_own_sell_price / highest_own_buy_price - 1 @@ -884,7 +875,6 @@ def enhance_center_price(self, reference=None, manual_offset=False, balance_base # Todo: Remove this after market center price is done def is_current_market(self, base_asset_id, quote_asset_id): - # Todo: Is this useful? """ Returns True if given asset id's are of the current market :return: bool: True = Current market, False = Not current market @@ -1061,12 +1051,11 @@ def retry_action(self, action, *args, **kwargs): raise def write_order_log(self, worker_name, order): - """ F + """ Write order log to csv file :param string | worker_name: Name of the worker :param object | order: Order that was fulfilled """ - # Todo: Add documentation operation_type = 'TRADE' if order['base']['symbol'] == self.market['base']['symbol']: From 4f9108b7ba0fa25cd3f8ccd9f011cd567eab107e Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 14:44:18 +0300 Subject: [PATCH 0754/1846] Add doc for WorkerController strategies() --- dexbot/controllers/worker_controller.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index 5b8dbc4a2..4d79e3211 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -25,6 +25,14 @@ def __init__(self, view, bitshares_instance, mode): @property def strategies(self): + """ Defines strategies that are configurable from the GUI. + + key: Strategy location in the project + name: The name that is shown in the GUI for user + form_module: If there is custom form module created with QTDesigner + + :return: List of strategies + """ strategies = collections.OrderedDict() strategies['dexbot.strategies.relative_orders'] = { 'name': 'Relative Orders', From 18cd2a0cfaeaa2d8350259f3c38dc0ef39c4a380 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 14:53:33 +0300 Subject: [PATCH 0755/1846] Refactor base.py variable names and ticker --- dexbot/strategies/base.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 6567237b5..e2205666e 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -204,7 +204,7 @@ def __init__(self, self.fee_asset = Asset('1.3.0') # Ticker - self.ticker = self.market.ticker() + self.ticker = self.market.ticker # Settings for bitshares instance self.bitshares.bundle = bool(self.worker.get("bundle", False)) @@ -351,7 +351,7 @@ def calculate_worker_value(self, unit_of_measure): quote_total += balance['amount'] # Calculate value of the orders in unit of measure - orders = self.current_market_own_orders + orders = self.get_own_orders for order in orders: if order['base']['symbol'] == self.quote_asset: # Pick sell orders order's BASE amount, which is same as worker's QUOTE, to worker's BASE @@ -399,7 +399,7 @@ def cancel_orders(self, orders, batch_only=False): self._cancel_orders(order) return True - def count_asset(self, order_ids=None, return_asset=False, refresh=True): + def count_asset(self, order_ids=None, return_asset=False): """ Returns the combined amount of the given order ids and the account balance The amounts are returned in quote and base assets of the market @@ -422,9 +422,9 @@ def count_asset(self, order_ids=None, return_asset=False, refresh=True): if order_ids is None: # Get all orders from Blockchain - order_ids = [order['id'] for order in self.current_market_own_orders] + order_ids = [order['id'] for order in self.get_own_orders] if order_ids: - orders_balance = self.orders_balance(order_ids) + orders_balance = self.get_allocated_assets(order_ids) quote += orders_balance['quote'] base += orders_balance['base'] @@ -599,7 +599,7 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0): # Like get_market_sell_price(), but defaulting to base_amount if both base and quote are specified. # In case amount is not given, return price of the lowest sell order on the market if quote_amount == 0 and base_amount == 0: - return self.ticker.get('highestBid') + return self.ticker().get('highestBid') asset_amount = base_amount @@ -668,7 +668,7 @@ def get_market_sell_price(self, quote_amount=0, base_amount=00): """ # In case amount is not given, return price of the lowest sell order on the market if quote_amount == 0 and base_amount == 0: - return self.ticker.get('lowestAsk') + return self.ticker().get('lowestAsk') asset_amount = quote_amount @@ -808,7 +808,7 @@ def get_own_buy_orders(self, orders=None): """ if not orders: # List of orders was not given so fetch everything from the market - orders = self.current_market_own_orders + orders = self.get_own_orders return self.filter_buy_orders(orders) @@ -819,7 +819,7 @@ def get_own_sell_orders(self, orders=None): """ if not orders: # List of orders was not given so fetch everything from the market - orders = self.current_market_own_orders + orders = self.get_own_orders return self.filter_sell_orders(orders) @@ -1125,7 +1125,7 @@ def all_own_orders(self, refresh=True): return orders @property - def current_market_own_orders(self, refresh=False): + def get_own_orders(self): """ Return the account's open orders in the current market :return: List of Order objects @@ -1133,8 +1133,7 @@ def current_market_own_orders(self, refresh=False): orders = [] # Refresh account data - if refresh: - self.account.refresh() + self.account.refresh() for order in self.account.openorders: if self.worker["market"] == order.market and self.account.openorders: @@ -1192,8 +1191,8 @@ def get_updated_limit_order(limit_order): Todo: If get_updated_order is removed, this can be removed as well. """ order = copy.deepcopy(limit_order) - price = order['sell_price']['base']['amount'] / order['sell_price']['quote']['amount'] - base_amount = order['for_sale'] + price = float(order['sell_price']['base']['amount']) / float(order['sell_price']['quote']['amount']) + base_amount = float(order['for_sale']) quote_amount = base_amount / price order['sell_price']['base']['amount'] = base_amount order['sell_price']['quote']['amount'] = quote_amount From ce01996fb110a95c44e3c011c573fe46b85b95cc Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 15:13:48 +0300 Subject: [PATCH 0756/1846] Add strategy_template.py --- dexbot/strategies/strategy_template.py | 241 +++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 dexbot/strategies/strategy_template.py diff --git a/dexbot/strategies/strategy_template.py b/dexbot/strategies/strategy_template.py new file mode 100644 index 000000000..44ca214ec --- /dev/null +++ b/dexbot/strategies/strategy_template.py @@ -0,0 +1,241 @@ +# Python imports +import math + +# Project imports +from dexbot.strategies.base import StrategyBase, ConfigElement +from dexbot.qt_queue.idle_queue import idle_add + +# Third party imports +from bitshares.market import Market + +STRATEGY_NAME = 'Strategy Template' + + +class Strategy(StrategyBase): + """ + + Replace with the name of the strategy. + + This is a template strategy which can be used to create custom strategies easier. The base for the strategy is + ready. It is recommended comment the strategy and functions to help other developers to make changes. + + Adding strategy to GUI + In dexbot.controller.strategy_controller add new strategy inside strategies() as show below: + + strategies['dexbot.strategies.strategy_template'] = { + 'name': '', + 'form_module': '' + } + + key: Strategy location in the project + name: The name that is shown in the GUI for user + form_module: If there is custom form module created with QTDesigner + + Adding strategy to CLI + In dexbot.cli_conf add strategy in to the STRATEGIES list + + {'tag': 'strategy_temp', + 'class': 'dexbot.strategies.strategy_template', + 'name': 'Template Strategy'}, + + NOTE: Change this comment section to describe the strategy. + """ + + @classmethod + def configure(cls, return_base_config=True): + """ This function is used to auto generate fields for GUI + + :param return_base_config: If base config is used in addition to this configuration. + :return: List of ConfigElement(s) + """ + + """ As a demonstration this template has two fields in the worker configuration. Upper and lower bound. + Documentation of ConfigElements can be found from base.py. + """ + return StrategyBase.configure(return_base_config) + [ + ConfigElement('upper_bound', 'float', 1, 'Max buy price', + 'Maximum price to pay in BASE.', + (0, None, 8, '')), + ConfigElement('lower_bound', 'float', 1, 'Min buy price', + 'Minimum price to pay in BASE.', + (0, None, 8, '')) + ] + + def __init__(self, *args, **kwargs): + # Initializes StrategyBase class + super().__init__(*args, **kwargs) + + """ Using self.log.info() you can print text on the GUI to inform user on what is the bot currently doing. This + is also written in the dexbot.log file. + """ + self.log.info("Initializing {}...".format(STRATEGY_NAME)) + + # Tick counter + self.counter = 0 + + # Define Callbacks + self.onMarketUpdate += self.maintain_strategy + self.onAccount += self.maintain_strategy + self.ontick += self.tick + + self.error_ontick = self.error + self.error_onMarketUpdate = self.error + self.error_onAccount = self.error + """ Define what strategy does on the following events + - Bitshares account has been modified = self.onAccount + - Market has been updated = self.onMarketUpdate + + These events are tied to methods which decide how the loop goes, unless the strategy is static, which + means that it will only do one thing and never do + """ + + # Get view + self.view = kwargs.get('view') + + """ Worker parameters + + There values are taken from the worker's config file. + Name of the worker is passed in the **kwargs. + """ + self.worker_name = kwargs.get('name') + + self.upper_bound = self.worker.get('upper_bound') + self.lower_bound = self.worker.get('lower_bound') + + """ Strategy variables + + These variables are for the strategy only and should be initialized here if wanted into self's scope. + """ + self.market_center_price = 0 + + if self.view: + self.update_gui_slider() + + def maintain_strategy(self): + """ Strategy main loop + + This method contains the strategy's logic. Keeping this function as simple as possible is recommended. + + Note: All orders are "buy" orders, since they are flipped to be easier to handle. Keep them separated to + avoid confusion on problems. + """ + # Todo: Clean this up once ready to release. + asset = 'TEST' + worker_value = self.calculate_worker_value(asset) + print('worker_value: {} as {}'.format(worker_value, asset)) + + asset = 'CODACOIN' + worker_value = self.calculate_worker_value(asset) + print('worker_value: {} as {}\n'.format(worker_value, asset)) + + print('BASE asset for this strategy is {}'.format(self.base_asset)) + print('QUOTE asset for this strategy is {}'.format(self.quote_asset)) + print('Market is (QUOTE/BASE) ({})'.format(self.worker.get('market'))) + + market_orders = self.get_market_orders(10) + + print('\nMarket buy orders') + for order in market_orders['bids']: + print(order) + + print('\nMarket sell orders') + for order in market_orders['asks']: + print(order) + + all_orders = self.get_own_orders + print('\nUser\'s orders') + for order in all_orders: + print(order) + + print('\nGet own BUY orders') + buy_orders = self.get_own_buy_orders() + for order in buy_orders: + print(order) + + print('\nHighest own buy order') + highest_own_buy_order = self.get_highest_own_buy(buy_orders) + print(highest_own_buy_order) + + print('\nGet own SELL orders') + sell_orders = self.get_own_sell_orders() + for order in sell_orders: + print(order) + + print('\nLowest own sell order') + lowest_own_sell_order = self.get_lowest_own_sell_order(sell_orders) + print(lowest_own_sell_order) + + print('Testing get_market_sell_price()') + quote_amount = 200 + base_amount = 1200 + sell_price = self.get_market_sell_price(quote_amount=quote_amount) + print('Sell price for {} CODACOIN {}'.format(quote_amount, sell_price)) + + sell_price = self.get_market_sell_price(base_amount=base_amount) + print('Sell price for {} TEST {}\n'.format(base_amount, sell_price)) + + print('Testing get_market_buy_price()') + buy_price = self.get_market_buy_price(quote_amount=quote_amount) + print('Buy price for {} CODACOIN is {}'.format(quote_amount, buy_price)) + + buy_price = self.get_market_buy_price(base_amount=base_amount) + print('Buy price for {} TEST is {}'.format(base_amount, buy_price)) + + self.market_center_price = self.get_market_center_price() + + """ Placing an order to the market has been made simple. Placing a buy order for example requires two values: + Amount (as QUOTE asset) and price (which is BASE amount divided by QUOTE amount) + + "Placing to buy 100 ASSET_A with price of 10 ASSET_A / ASSET_B" would be place_market_buy_order(100, 10). + This would then cost 1000 USD to fulfil. + + Further documentation can be found from the function's documentation. + """ + + # Place BUY order to the market + # self.place_market_buy_order(highest_own_buy_order['quote']['amount'], highest_own_buy_order['price']) + # self.place_market_buy_order(100, 10) + + # Place SELL order to the market + # self.place_market_sell_order(lowest_own_sell_order['quote']['amount'], lowest_own_sell_order['price']) + + def check_orders(self, *args, **kwargs): + """ """ + pass + + def error(self, *args, **kwargs): + """ Defines what happens when error occurs """ + self.disabled = False + + def pause(self): + """ Override pause() in StrategyBase """ + pass + + def tick(self, d): + """ Ticks come in on every block """ + if not (self.counter or 0) % 3: + self.maintain_strategy() + self.counter += 1 + + def update_gui_slider(self): + """ Updates GUI slider on the workers list """ + # Todo: Need's fixing? + latest_price = self.ticker().get('latest', {}).get('price', None) + if not latest_price: + return + + order_ids = None + orders = self.get_own_orders + + if orders: + order_ids = [order['id'] for order in orders if 'id' in order] + + total_balance = self.count_asset(order_ids) + 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 From ad7f18db7af459829b2bc74610922683d4a92f5c Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 13 Sep 2018 00:17:16 +0500 Subject: [PATCH 0757/1846] Allow partial orders when not bootstrapping When we have imbalanced sides, and partially filled orders on both sides, and then one order gets fully filled, we may come into situation where we cannot place any new orders because available balance may be not enough nor for full-sized order, neighter for limited by opposite side. So, as a workaround, allow placing of partial orders. This will not introduce any harm because spread will be `spread - increment`. --- 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 fa3d9c0bb..cde41a524 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -486,7 +486,7 @@ def allocate_asset(self, asset, asset_balance): self.log.debug('Limiting {} order by opposite order: {} {}'.format( order_type, opposite_asset_limit, symbol)) self.place_closer_order(asset, closest_own_order, own_asset_limit=own_asset_limit, - opposite_asset_limit=opposite_asset_limit, allow_partial=False) + opposite_asset_limit=opposite_asset_limit, allow_partial=True) elif not opposite_orders: # Do not try to do anything than placing higher buy whether there is no sell orders return From bdf7ae66bf5c01fb66074da3f1ddeb46c60c861b Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 14 Sep 2018 10:04:55 +0500 Subject: [PATCH 0758/1846] Update comment --- 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 cde41a524..7f7473c05 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -680,7 +680,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): maximum size. If it is, move on to next. If not, cancel it and replace with maximum size order. Maximum order size will be a - size of higher order. Then return. + size of closer-to-center order. Then return. If furthest is reached, increase it to maximum size. Maximum size is (example for buy orders): From 9da827c96cd525f05ea094dc8fbf12c640f569b4 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 09:34:27 +0300 Subject: [PATCH 0759/1846] Update cli_conf.py to use new StrategyBase --- dexbot/cli_conf.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index e2ca4c9b1..e3260b480 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -7,7 +7,7 @@ Requires the 'whiptail' tool for text-based configuration (so UNIX only) if not available, falls back to a line-based configurator ("NoWhiptail") -Note there is some common cross-UI configuration stuff: look in basestrategy.py +Note there is some common cross-UI configuration stuff: look in base.py It's expected GUI/web interfaces will be re-implementing code in this file, but they should understand the common code so worker strategy writers can define their configuration once for each strategy class. @@ -22,7 +22,7 @@ import subprocess from dexbot.whiptail import get_whiptail -from dexbot.basestrategy import BaseStrategy +from dexbot.strategies.base import StrategyBase import dexbot.helper STRATEGIES = [ @@ -217,14 +217,14 @@ def configure_dexbot(config, ctx): worker_name = whiptail.menu("Select worker to edit", [(i, i) for i in workers]) config['workers'][worker_name] = configure_worker(whiptail, config['workers'][worker_name]) - strategy = BaseStrategy(worker_name, bitshares_instance=bitshares_instance, config=config) - strategy.purge() + strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) + strategy.clear_all_worker_data() elif action == 'DEL': worker_name = whiptail.menu("Select worker to delete", [(i, i) for i in workers]) del config['workers'][worker_name] - strategy = BaseStrategy(worker_name, bitshares_instance=bitshares_instance, config=config) - strategy.purge() + strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) + strategy.clear_all_worker_data() elif action == 'NEW': txt = whiptail.prompt("Your name for the new worker") config['workers'][txt] = configure_worker(whiptail, {}) From 6de9454126a0aa4b3edfba3489ce991819bd76ff Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 09:35:08 +0300 Subject: [PATCH 0760/1846] Update worker.py to use new StrategyBase --- dexbot/worker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/worker.py b/dexbot/worker.py index 15f2d011c..6af9f742f 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -6,7 +6,7 @@ import copy import dexbot.errors as errors -from dexbot.basestrategy import BaseStrategy +from dexbot.strategies.base import StrategyBase from bitshares import BitShares from bitshares.notify import Notify @@ -225,12 +225,12 @@ def remove_market(self, worker_name): @staticmethod def remove_offline_worker(config, worker_name, bitshares_instance): # Initialize the base strategy to get control over the data - strategy = BaseStrategy(worker_name, config, bitshares_instance=bitshares_instance) + strategy = StrategyBase(worker_name, config, bitshares_instance=bitshares_instance) strategy.purge() @staticmethod def remove_offline_worker_data(worker_name): - BaseStrategy.purge_worker_data(worker_name) + StrategyBase.purge_all_local_worker_data(worker_name) def do_next_tick(self, job): """ Add a callable to be executed on the next tick """ From 0310770ab25c42d7a84c59e0563b9b5c948750ca Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 09:35:33 +0300 Subject: [PATCH 0761/1846] Refactor Relative Orders to work with StrategyBase --- dexbot/strategies/relative_orders.py | 78 +++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 7 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index a9f638a90..8e59b0cf5 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -1,17 +1,17 @@ import math from datetime import datetime, timedelta -from dexbot.basestrategy import BaseStrategy, ConfigElement +from dexbot.strategies.base import StrategyBase, ConfigElement from dexbot.qt_queue.idle_queue import idle_add -class Strategy(BaseStrategy): +class Strategy(StrategyBase): """ Relative Orders strategy """ @classmethod def configure(cls, return_base_config=True): - return BaseStrategy.configure(return_base_config) + [ + return StrategyBase.configure(return_base_config) + [ ConfigElement('amount', 'float', 1, 'Amount', 'Fixed order size, expressed in quote asset, unless "relative order size" selected', (0, None, 8, '')), @@ -166,7 +166,7 @@ def update_orders(self): self.calculate_order_prices() # Cancel the orders before redoing them - self.cancel_all() + self.cancel_all_orders() self.clear_orders() order_ids = [] @@ -177,7 +177,7 @@ def update_orders(self): # Buy Side if amount_base: - buy_order = self.market_buy(amount_base, self.buy_price, True) + buy_order = self.place_market_buy_order(amount_base, self.buy_price, True) if buy_order: self.save_order(buy_order) order_ids.append(buy_order['id']) @@ -185,7 +185,7 @@ def update_orders(self): # Sell Side if amount_quote: - sell_order = self.market_sell(amount_quote, self.sell_price, True) + sell_order = self.place_market_sell_order(amount_quote, self.sell_price, True) if sell_order: self.save_order(sell_order) order_ids.append(sell_order['id']) @@ -199,6 +199,70 @@ def update_orders(self): if len(order_ids) < expected_num_orders and not self.disabled: self.update_orders() + 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: + 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: + if not suppress_errors: + self.log.critical( + "Cannot estimate center price, there is no lowest ask." + ) + self.disabled = True + return None + + center_price = highest_bid['price'] * math.sqrt(lowest_ask['price'] / highest_bid['price']) + return center_price + + def calculate_center_price(self, center_price=None, asset_offset=False, spread=None, + order_ids=None, manual_offset=0, suppress_errors=False): + """ Calculate center price which shifts based on available funds + """ + if center_price is None: + # No center price was given so we simply calculate the center price + calculated_center_price = self._calculate_center_price(suppress_errors) + else: + # 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 center_price: + calculated_center_price = center_price + + if asset_offset: + total_balance = self.count_asset(order_ids) + total = (total_balance['quote'] * calculated_center_price) + total_balance['base'] + + if not total: # Prevent division by zero + balance = 0 + else: + # Returns a value between -1 and 1 + balance = (total_balance['base'] / total) * 2 - 1 + + if balance < 0: + # With less of base asset center price should be offset downward + calculated_center_price = calculated_center_price / math.sqrt(1 + spread * (balance * -1)) + elif balance > 0: + # With more of base asset center price will be offset upwards + calculated_center_price = calculated_center_price * math.sqrt(1 + spread * balance) + else: + calculated_center_price = calculated_center_price + + # Calculate final_offset_price if manual center price offset is given + if manual_offset: + calculated_center_price = calculated_center_price + (calculated_center_price * manual_offset) + + return calculated_center_price + def check_orders(self, *args, **kwargs): """ Tests if the orders need updating """ @@ -287,7 +351,7 @@ def update_gui_slider(self): if orders: order_ids = orders.keys() - total_balance = self.total_balance(order_ids) + total_balance = self.count_asset(order_ids) total = (total_balance['quote'] * latest_price) + total_balance['base'] if not total: # Prevent division by zero From 90e1c447c972feea82e01d0054b6d4ceb6d9b9e3 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 09:39:24 +0300 Subject: [PATCH 0762/1846] Change basestrategy.rst to strategybase.rst --- docs/{basestrategy.rst => strategybase.rst} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename docs/{basestrategy.rst => strategybase.rst} (50%) diff --git a/docs/basestrategy.rst b/docs/strategybase.rst similarity index 50% rename from docs/basestrategy.rst rename to docs/strategybase.rst index b3cb61578..910239d64 100644 --- a/docs/basestrategy.rst +++ b/docs/strategybase.rst @@ -1,13 +1,13 @@ ************* -Base Strategy +Strategy Base ************* All strategies should inherit -:class:`dexbot.basestrategy.BaseStrategy` which simplifies and +:class:`dexbot.strategies.StrategyBase` which simplifies and unifies the development of new strategies. API --- -.. autoclass:: dexbot.basestrategy.BaseStrategy +.. autoclass:: dexbot.strategies.StrategyBase :members: From 5d70892126e4c2766add3b7cef6af0c59b6a0d61 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 09:40:18 +0300 Subject: [PATCH 0763/1846] Remove enchance_center_price() --- dexbot/strategies/base.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index e2205666e..fcc7b352f 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -861,19 +861,6 @@ def get_updated_order(self, order_id): order = self.get_updated_limit_order(order) return Order(order, bitshares_instance=self.bitshares) - def enhance_center_price(self, reference=None, manual_offset=False, balance_based_offset=False, - moving_average=0, weighted_average=0): - """ Returns the passed reference price shifted up or down based on arguments. - - :param float | reference: Center price to enhance - :param bool | manual_offset: - :param bool | balance_based_offset: - :param int or float | moving_average: - :param int or float | weighted_average: - :return: - """ - # Todo: Remove this after market center price is done - def is_current_market(self, base_asset_id, quote_asset_id): """ Returns True if given asset id's are of the current market From 3a0df0ce03b69374d80b8cf862097d79fac6d5d5 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 09:41:22 +0300 Subject: [PATCH 0764/1846] Refactor pause_worker() to pause() --- dexbot/strategies/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index fcc7b352f..c6556198e 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -879,7 +879,7 @@ def is_current_market(self, base_asset_id, quote_asset_id): return False - def pause_worker(self): + def pause(self): """ Pause the worker Note: By default pause cancels orders, but this can be overridden by strategy From a2acbfd870aff55c4010a8570d5acc3c951bfab7 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 09:51:27 +0300 Subject: [PATCH 0765/1846] Update echo.py to use StrategyBase --- dexbot/strategies/echo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/echo.py b/dexbot/strategies/echo.py index c7a732aaa..264f1027c 100644 --- a/dexbot/strategies/echo.py +++ b/dexbot/strategies/echo.py @@ -1,7 +1,7 @@ -from dexbot.basestrategy import BaseStrategy +from dexbot.strategies.base import StrategyBase -class Strategy(BaseStrategy): +class Strategy(StrategyBase): """ Echo strategy Strategy that logs all events within the blockchain """ From 2bd72c2fb415482deae99a29e9ce6329a32a4a36 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 13:16:58 +0300 Subject: [PATCH 0766/1846] Remove get_external_price() --- dexbot/strategies/base.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index c6556198e..09279fc4d 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -468,14 +468,6 @@ def get_allocated_assets(self, order_ids=None, return_asset=False): return {'quote': quote, 'base': base} - def get_external_price(self, source): - """ Returns the center price of market including own orders. - - :param source: - :return: - """ - # Todo: Insert logic here - def get_market_fee(self): """ Returns the fee percentage for buying specified asset From f5eb0608d6bcc94e4585e2ac0dc7751ac4a61319 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 13:17:32 +0300 Subject: [PATCH 0767/1846] Change comments on function descriptions --- dexbot/strategies/base.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 09279fc4d..e51704cb8 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -53,13 +53,11 @@ class StrategyBase(Storage, StateMachine, Events): - Buy orders reserve BASE - Sell orders reserve QUOTE - Todo: This is copy / paste from old, update this if needed! Strategy inherits: * :class:`dexbot.storage.Storage` : Stores data to sqlite database * :class:`dexbot.statemachine.StateMachine` * ``Events`` - Todo: This is copy / paste from old, update this if needed! Available attributes: * ``worker.bitshares``: instance of ´`bitshares.BitShares()`` * ``worker.add_state``: Add a specific state @@ -124,7 +122,6 @@ def configure(cls, return_base_config=True): r'[A-Z\.]+') ] - # Todo: Is there any case / strategy where the base config would NOT be needed, making this unnecessary? if return_base_config: return base_config return [] @@ -232,7 +229,6 @@ def __init__(self, def _callbackPlaceFillOrders(self, d): """ This method distinguishes notifications caused by Matched orders from those caused by placed orders - Todo: can this be renamed to _instantFill()? """ if isinstance(d, FilledOrder): self.onOrderMatched(d) @@ -285,7 +281,6 @@ def account_total_value(self, return_asset): # Orders balance calculation for order in self.all_own_orders: - # Todo: What is the purpose of this? updated_order = self.get_updated_order(order['id']) if not order: @@ -302,13 +297,12 @@ def account_total_value(self, return_asset): return total_value def balance(self, asset, fee_reservation=0): - """ Return the balance of your worker's account for a specific asset + """ Return the balance of your worker's account in a specific asset. - :param string | asset: - :param bool | fee_reservation: + :param string | asset: In what asset the balance is wanted to be returned + :param float | fee_reservation: How much is saved in reserve for the fees :return: Balance of specific asset """ - # Todo: Check that fee reservation was as intended, having it true / false made no sense balance = self._account.balance(asset) if fee_reservation > 0: @@ -379,11 +373,11 @@ def cancel_all_orders(self): self.log.info("Orders canceled") def cancel_orders(self, orders, batch_only=False): - """ Cancel specific order or orders + """ Cancel specific order(s) :param list | orders: List of orders to cancel :param bool | batch_only: Try cancel orders only in batch mode without one-by-one fallback - :return: Todo: Add documentation + :return: """ if not isinstance(orders, (list, set, tuple)): orders = [orders] @@ -820,7 +814,6 @@ def get_own_spread(self): :return: float or None: Own spread """ - try: # Try fetching own orders highest_own_buy_price = self.get_highest_market_buy_order().get('price') From 03e48526a2b9de7391f5695d613a2c4e0746cf1a Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 13:17:56 +0300 Subject: [PATCH 0768/1846] Fix problem where old cancel method was called --- dexbot/strategies/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index e51704cb8..c9711eaff 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -368,7 +368,7 @@ def cancel_all_orders(self): self.log.info('Canceling all orders') if self.all_own_orders: - self.cancel(self.all_own_orders) + self.cancel_orders(self.all_own_orders) self.log.info("Orders canceled") @@ -870,7 +870,7 @@ def pause(self): Note: By default pause cancels orders, but this can be overridden by strategy """ # Cancel all orders from the market - self.cancel_all() + self.cancel_all_orders() # Removes worker's orders from local database self.clear_orders() @@ -882,7 +882,7 @@ def clear_all_worker_data(self): self.clear_orders() # Cancel all orders from the market - self.cancel_all() + self.cancel_all_orders() # Finally clear all worker data from the database self.clear() From 05841583d04763f5092e322c3d60fcfee20ff934 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 13:18:23 +0300 Subject: [PATCH 0769/1846] Remove code that was used for testing --- dexbot/strategies/strategy_template.py | 85 +++----------------------- 1 file changed, 9 insertions(+), 76 deletions(-) diff --git a/dexbot/strategies/strategy_template.py b/dexbot/strategies/strategy_template.py index 44ca214ec..71ae28d29 100644 --- a/dexbot/strategies/strategy_template.py +++ b/dexbot/strategies/strategy_template.py @@ -20,7 +20,7 @@ class Strategy(StrategyBase): ready. It is recommended comment the strategy and functions to help other developers to make changes. Adding strategy to GUI - In dexbot.controller.strategy_controller add new strategy inside strategies() as show below: + In dexbot.controller.worker_controller add new strategy inside strategies() as show below: strategies['dexbot.strategies.strategy_template'] = { 'name': '', @@ -111,6 +111,8 @@ def __init__(self, *args, **kwargs): if self.view: self.update_gui_slider() + self.log.info("{} initialized.".format(STRATEGY_NAME)) + def maintain_strategy(self): """ Strategy main loop @@ -118,86 +120,18 @@ def maintain_strategy(self): Note: All orders are "buy" orders, since they are flipped to be easier to handle. Keep them separated to avoid confusion on problems. - """ - # Todo: Clean this up once ready to release. - asset = 'TEST' - worker_value = self.calculate_worker_value(asset) - print('worker_value: {} as {}'.format(worker_value, asset)) - - asset = 'CODACOIN' - worker_value = self.calculate_worker_value(asset) - print('worker_value: {} as {}\n'.format(worker_value, asset)) - - print('BASE asset for this strategy is {}'.format(self.base_asset)) - print('QUOTE asset for this strategy is {}'.format(self.quote_asset)) - print('Market is (QUOTE/BASE) ({})'.format(self.worker.get('market'))) - - market_orders = self.get_market_orders(10) - - print('\nMarket buy orders') - for order in market_orders['bids']: - print(order) - - print('\nMarket sell orders') - for order in market_orders['asks']: - print(order) - - all_orders = self.get_own_orders - print('\nUser\'s orders') - for order in all_orders: - print(order) - - print('\nGet own BUY orders') - buy_orders = self.get_own_buy_orders() - for order in buy_orders: - print(order) - - print('\nHighest own buy order') - highest_own_buy_order = self.get_highest_own_buy(buy_orders) - print(highest_own_buy_order) - - print('\nGet own SELL orders') - sell_orders = self.get_own_sell_orders() - for order in sell_orders: - print(order) - - print('\nLowest own sell order') - lowest_own_sell_order = self.get_lowest_own_sell_order(sell_orders) - print(lowest_own_sell_order) - print('Testing get_market_sell_price()') - quote_amount = 200 - base_amount = 1200 - sell_price = self.get_market_sell_price(quote_amount=quote_amount) - print('Sell price for {} CODACOIN {}'.format(quote_amount, sell_price)) - - sell_price = self.get_market_sell_price(base_amount=base_amount) - print('Sell price for {} TEST {}\n'.format(base_amount, sell_price)) - - print('Testing get_market_buy_price()') - buy_price = self.get_market_buy_price(quote_amount=quote_amount) - print('Buy price for {} CODACOIN is {}'.format(quote_amount, buy_price)) - - buy_price = self.get_market_buy_price(base_amount=base_amount) - print('Buy price for {} TEST is {}'.format(base_amount, buy_price)) - - self.market_center_price = self.get_market_center_price() - - """ Placing an order to the market has been made simple. Placing a buy order for example requires two values: + Placing an order to the market has been made simple. Placing a buy order for example requires two values: Amount (as QUOTE asset) and price (which is BASE amount divided by QUOTE amount) - + "Placing to buy 100 ASSET_A with price of 10 ASSET_A / ASSET_B" would be place_market_buy_order(100, 10). This would then cost 1000 USD to fulfil. - - Further documentation can be found from the function's documentation. - """ - # Place BUY order to the market - # self.place_market_buy_order(highest_own_buy_order['quote']['amount'], highest_own_buy_order['price']) - # self.place_market_buy_order(100, 10) + Further documentation can be found from the function's documentation. - # Place SELL order to the market - # self.place_market_sell_order(lowest_own_sell_order['quote']['amount'], lowest_own_sell_order['price']) + """ + # Start writing strategy logic from here. + self.log.info("Starting {}".format(STRATEGY_NAME)) def check_orders(self, *args, **kwargs): """ """ @@ -219,7 +153,6 @@ def tick(self, d): def update_gui_slider(self): """ Updates GUI slider on the workers list """ - # Todo: Need's fixing? latest_price = self.ticker().get('latest', {}).get('price', None) if not latest_price: return From e089dcaf5f3728d68dc2d5a8d9bf3d0fa5e7fcfe Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 13:42:41 +0300 Subject: [PATCH 0770/1846] Change example fields to be more clear --- dexbot/strategies/strategy_template.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/strategy_template.py b/dexbot/strategies/strategy_template.py index 71ae28d29..211223e53 100644 --- a/dexbot/strategies/strategy_template.py +++ b/dexbot/strategies/strategy_template.py @@ -53,12 +53,12 @@ def configure(cls, return_base_config=True): Documentation of ConfigElements can be found from base.py. """ return StrategyBase.configure(return_base_config) + [ - ConfigElement('upper_bound', 'float', 1, 'Max buy price', - 'Maximum price to pay in BASE.', - (0, None, 8, '')), - ConfigElement('lower_bound', 'float', 1, 'Min buy price', - 'Minimum price to pay in BASE.', - (0, None, 8, '')) + ConfigElement('lower_bound', 'float', 1, 'Lower bound', + 'The bottom price in the range', + (0, 10000000, 8, '')), + ConfigElement('upper_bound', 'float', 10, 'Upper bound', + 'The top price in the range', + (0, 10000000, 8, '')), ] def __init__(self, *args, **kwargs): From a1928d93e1d50e3198f157afa802965a715d84b8 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 13:43:16 +0300 Subject: [PATCH 0771/1846] Add support for the old BaseStrategy --- dexbot/strategies/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index c9711eaff..e7e7abf02 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -5,6 +5,7 @@ import math import time +from dexbot.basestrategy import BaseStrategy # Todo: Once the old BaseStrategy deprecates, remove it. from dexbot.config import Config from dexbot.storage import Storage from dexbot.statemachine import StateMachine @@ -44,7 +45,7 @@ ConfigElement = collections.namedtuple('ConfigElement', 'key type default title description extra') -class StrategyBase(Storage, StateMachine, Events): +class StrategyBase(BaseStrategy, Storage, StateMachine, Events): """ A strategy based on this class is intended to work in one market. This class contains most common methods needed by the strategy. From 49f46b8cc07bd149317110b5791bb45575ec2e89 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 16:05:55 +0300 Subject: [PATCH 0772/1846] Refactor code to fix warnings and PEP errors --- dexbot/strategies/staggered_orders.py | 147 ++++++++++++++++---------- 1 file changed, 94 insertions(+), 53 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 7f7473c05..77886760e 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -115,8 +115,8 @@ def __init__(self, *args, **kwargs): self.actual_spread = self.target_spread + 1 self.quote_total_balance = 0 self.base_total_balance = 0 - self.quote_balance = 0 - self.base_balance = 0 + self.quote_balance = None + self.base_balance = None self.ticker = None self.quote_asset_threshold = 0 self.base_asset_threshold = 0 @@ -245,15 +245,15 @@ def maintain_strategy(self, *args, **kwargs): # Greatly increase check interval to lower CPU load whether there is no funds to allocate or we cannot # allocate funds for some reason if (self.current_check_interval == self.min_check_interval and - self.base_balance_history[1] == self.base_balance_history[2] and - self.quote_balance_history[1] == self.quote_balance_history[2]): + self.base_balance_history[1] == self.base_balance_history[2] and + self.quote_balance_history[1] == self.quote_balance_history[2]): # Balance didn't changed, so we can reduce maintenance frequency self.log.debug('Raising check interval up to {} seconds to reduce CPU usage'.format( self.max_check_interval)) self.current_check_interval = self.max_check_interval elif (self.current_check_interval == self.max_check_interval and - (self.base_balance_history[1] != self.base_balance_history[2] or - self.quote_balance_history[1] != self.quote_balance_history[2])): + (self.base_balance_history[1] != self.base_balance_history[2] or + self.quote_balance_history[1] != self.quote_balance_history[2])): # Balance changed, increase maintenance frequency to allocate more quickly self.log.debug('Reducing check interval to {} seconds because of changed ' 'balances'.format(self.min_check_interval)) @@ -377,10 +377,21 @@ def remove_outside_orders(self, sell_orders, buy_orders): def allocate_asset(self, asset, asset_balance): """ Allocates available asset balance as buy or sell orders. + :param str | asset: 'base' or 'quote' :param Amount | asset_balance: Amount of the asset available to use """ self.log.debug('Need to allocate {}: {}'.format(asset, asset_balance)) + closest_opposite_order = None + opposite_asset_limit = None + opposite_orders = [] + opposite_balance = None + opposite_threshold = 0.0 + order_type = '' + own_asset_limit = None + own_orders = [] + own_threshold = 0 + symbol = '' if asset == 'base': order_type = 'buy' @@ -457,30 +468,30 @@ def allocate_asset(self, asset, asset_balance): 'opposite-side balance') self.bootstrapping = False elif (self.bootstrapping and - self.base_balance_history[2] == self.base_balance_history[0] and - self.quote_balance_history[2] == self.quote_balance_history[0]): - # Turn off bootstrap mode whether we're didn't allocated assets during previos 3 maintenances + self.base_balance_history[2] == self.base_balance_history[0] and + self.quote_balance_history[2] == self.quote_balance_history[0]): + # Turn off bootstrap mode whether we're didn't allocated assets during previous 3 maintenance self.log.debug('Turning bootstrapping off: actual_spread > target_spread, we have free ' 'balances and cannot allocate them normally 3 times in a row') self.bootstrapping = False # Place order closer to the center price - self.log.debug('Placing closer {} order; actual spread: {:.4%}, target + increment: {:.4%}'.format( - order_type, self.actual_spread, self.target_spread + self.increment)) + self.log.debug('Placing closer {} order; actual spread: {:.4%}, target + increment: {:.4%}' + .format(order_type, self.actual_spread, self.target_spread + self.increment)) if self.bootstrapping: self.place_closer_order(asset, closest_own_order) else: # Place order limited by size of the opposite-side order if (self.mode == 'mountain' or - (self.mode == 'buy_slope' and asset == 'base') or - (self.mode == 'sell_slope' and asset == 'quote')): - opposite_asset_limit = None - own_asset_limit = closest_opposite_order['quote']['amount'] - self.log.debug('Limiting {} order by opposite order: {} {}'.format( - order_type, own_asset_limit, symbol)) + (self.mode == 'buy_slope' and asset == 'base') or + (self.mode == 'sell_slope' and asset == 'quote')): + opposite_asset_limit = None + own_asset_limit = closest_opposite_order['quote']['amount'] + self.log.debug('Limiting {} order by opposite order: {} {}' + .format(order_type, own_asset_limit, symbol)) elif (self.mode == 'valley' or - (self.mode == 'buy_slope' and asset == 'quote') or - (self.mode == 'sell_slope' and asset == 'base')): + (self.mode == 'buy_slope' and asset == 'quote') or + (self.mode == 'sell_slope' and asset == 'base')): opposite_asset_limit = closest_opposite_order['base']['amount'] own_asset_limit = None self.log.debug('Limiting {} order by opposite order: {} {}'.format( @@ -492,9 +503,10 @@ def allocate_asset(self, asset, asset_balance): return else: if not self.check_partial_fill(closest_opposite_order): - """ Detect partially filled order on the opposite side and reserve appropriate amount to place - closer order + """ Detect partially filled order on the opposite side and + reserve appropriate amount to place closer order """ + funds_to_reserve = 0 closer_own_order = self.place_closer_order(asset, closest_own_order, place_order=False) if asset == 'base': funds_to_reserve = closer_own_order['amount'] * closer_own_order['price'] @@ -504,8 +516,10 @@ def allocate_asset(self, asset, asset_balance): '{:.8f} {}'.format(order_type, funds_to_reserve, symbol)) asset_balance -= funds_to_reserve if asset_balance > own_threshold: - if ((asset == 'base' and furthest_own_order_price / (1 + self.increment) < self.lower_bound) or - (asset == 'quote' and furthest_own_order_price * (1 + self.increment) > self.upper_bound)): + if ((asset == 'base' and furthest_own_order_price / + (1 + self.increment) < self.lower_bound) or + (asset == 'quote' and furthest_own_order_price * + (1 + self.increment) > self.upper_bound)): # Lower/upper bound has been reached and now will start allocating rest of the balance. self.bootstrapping = False self.log.debug('Increasing sizes of {} orders'.format(order_type)) @@ -551,8 +565,8 @@ def increase_order_sizes(self, asset, asset_balance, orders): started from the furthest order. Neutral: - Try to flatten everything by increasing order sizes to neutral. When everything is correct, maximize closest - orders and then increase other orders to match that. + Try to flatten everything by increasing order sizes to neutral. When everything is correct, maximize + closest orders and then increase other orders to match that. Valley: Maximize order sizes as far as possible from center first. When all orders are max, the new increase round @@ -571,10 +585,13 @@ def increase_order_sizes(self, asset, asset_balance, orders): :param list | orders: List of buy or sell orders :return None """ + total_balance = 0 + order_type = '' + # Mountain mode: if (self.mode == 'mountain' or - (self.mode == 'buy_slope' and asset == 'quote') or - (self.mode == 'sell_slope' and asset == 'base')): + (self.mode == 'buy_slope' and asset == 'quote') or + (self.mode == 'sell_slope' and asset == 'base')): """ Starting from the furthest order. For each order, see if it is approximately maximum size. If it is, move on to next. @@ -625,7 +642,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): further_bound = further_order['base']['amount'] * (1 + self.increment) if (further_bound > order_amount * (1 + self.increment / 10) < closer_bound and - further_bound - order_amount >= order_amount * self.increment / 2): + further_bound - order_amount >= order_amount * self.increment / 2): # Calculate new order size and place the order to the market new_order_amount = further_bound @@ -651,8 +668,10 @@ def increase_order_sizes(self, asset, asset_balance, orders): # Limit sell order to available balance if asset_balance < new_order_amount - order_amount: new_order_amount = order_amount + asset_balance['amount'] - self.log.info('Limiting new {} order to avail asset balance: {:.8f} {}'.format( - order_type, new_order_amount, asset_balance['symbol'])) + self.log.info('Limiting new {} order to avail asset balance: {:.8f} {}' + .format(order_type, new_order_amount, asset_balance['symbol'])) + quote_amount = 0 + price = 0 if asset == 'quote': price = (order['price'] ** -1) @@ -661,20 +680,18 @@ def increase_order_sizes(self, asset, asset_balance, orders): price = order['price'] quote_amount = new_order_amount / price - self.log.debug('Cancelling {} order in increase_order_sizes(); ' - 'mode: {}, amount: {}, price: {:.8f}'.format(order_type, self.mode, order_amount, - price)) + self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}, amount: {}, price: {:.8f}' + .format(order_type, self.mode, order_amount, price)) self.cancel(order) if asset == 'quote': self.market_sell(quote_amount, price) elif asset == 'base': self.market_buy(quote_amount, price) - # Only one increase at a time. This prevents running more than one increment round - # simultaneously + # Only one increase at a time. This prevents running more than one increment round simultaneously return elif (self.mode == 'valley' or - (self.mode == 'buy_slope' and asset == 'base') or - (self.mode == 'sell_slope' and asset == 'quote')): + (self.mode == 'buy_slope' and asset == 'base') or + (self.mode == 'sell_slope' and asset == 'quote')): """ Starting from the furthest order, for each order, see if it is approximately maximum size. @@ -721,7 +738,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): may have an actual difference like 30% from closer and 70% from further. """ if (order_amount * (1 + self.increment / 10) < closer_order_bound and - closer_order_bound - order_amount >= order_amount * self.increment / 2): + closer_order_bound - order_amount >= order_amount * self.increment / 2): amount_base = closer_order_bound @@ -731,6 +748,8 @@ def increase_order_sizes(self, asset, asset_balance, orders): self.log.info('Limiting new order to avail asset balance: {:.8f} {}' .format(amount_base, asset_balance['symbol'])) + price = 0 + if asset == 'quote': price = (order['price'] ** -1) elif asset == 'base': @@ -771,7 +790,8 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False opposite_asset_limit=None): """ Place order closer to the center - :param order: Previously closest order + :param asset: + :param order: Previously closest order :param bool | place_order: True = Places order to the market, False = returns amount and price :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance :param float | own_asset_limit: order should be limited in size by amount of order's "base" @@ -784,6 +804,11 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False return None # Define asset-dependent variables + balance = 0 + order_type = '' + quote_amount = 0 + symbol = '' + if asset == 'base': order_type = 'buy' balance = self.base_balance['amount'] @@ -797,28 +822,30 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False if asset == 'base': price = order['price'] * (1 + self.increment) if not self.is_instant_fill_enabled and price > float(self.ticker['lowestAsk']): - self.log.info('Refusing to place an order which crosses lowestAsk') + self.log.info('Refusing to place an order which crosses lowest ask') return None elif asset == 'quote': price = (order['price'] ** -1) / (1 + self.increment) if not self.is_instant_fill_enabled and price < float(self.ticker['highestBid']): - self.log.info('Refusing to place an order which crosses highestBid') + self.log.info('Refusing to place an order which crosses highest bid') return None # For next steps we do not need inverted price for sell orders price = order['price'] * (1 + self.increment) # Calculate new order amounts depending on mode + opposite_asset_amount = 0 + own_asset_amount = 0 if (self.mode == 'mountain' or - (self.mode == 'buy_slope' and asset == 'quote') or - (self.mode == 'sell_slope' and asset == 'base')): - opposite_asset_amount = order['quote']['amount'] - own_asset_amount = opposite_asset_amount * price + (self.mode == 'buy_slope' and asset == 'quote') or + (self.mode == 'sell_slope' and asset == 'base')): + opposite_asset_amount = order['quote']['amount'] + own_asset_amount = opposite_asset_amount * price elif (self.mode == 'valley' or - (self.mode == 'buy_slope' and asset == 'base') or - (self.mode == 'sell_slope' and asset == 'quote')): - own_asset_amount = order['base']['amount'] - opposite_asset_amount = own_asset_amount / price + (self.mode == 'buy_slope' and asset == 'base') or + (self.mode == 'sell_slope' and asset == 'quote')): + own_asset_amount = order['base']['amount'] + opposite_asset_amount = own_asset_amount / price # Apply limits. Limit order only whether passed limit is less than expected order size if own_asset_limit and own_asset_limit < own_asset_amount: @@ -828,6 +855,7 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False opposite_asset_amount = opposite_asset_limit own_asset_amount = opposite_asset_amount * price + limiter = 0 if asset == 'base': # Define amounts in terms of BASE and QUOTE base_amount = own_asset_amount @@ -846,8 +874,8 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False .format(order_type, limiter, balance)) place_order = False elif allow_partial: - self.log.debug('Limiting {} order amount to available asset balance: {} {}'.format( - order_type, balance, symbol)) + self.log.debug('Limiting {} order amount to available asset balance: {} {}' + .format(order_type, balance, symbol)) if asset == 'base': quote_amount = balance / price elif asset == 'quote': @@ -863,12 +891,17 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False def place_further_order(self, asset, order, place_order=True, allow_partial=False): """ Place order further from specified order + :param asset: :param order: furthest buy or sell order :param bool | place_order: True = Places order to the market, False = returns amount and price :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance """ # Define asset-dependent variables + balance = 0 + order_type = '' + symbol = '' + if asset == 'base': order_type = 'buy' balance = self.base_balance['amount'] @@ -881,6 +914,8 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals price = order['price'] / (1 + self.increment) # Calculate new order amounts depending on mode + opposite_asset_amount = 0 + own_asset_amount = 0 if self.mode == 'mountain' or self.mode == 'buy_slope': opposite_asset_amount = order['quote']['amount'] own_asset_amount = opposite_asset_amount * price @@ -888,6 +923,8 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals own_asset_amount = order['base']['amount'] opposite_asset_amount = own_asset_amount / price + limiter = 0 + quote_amount = 0 if asset == 'base': base_amount = own_asset_amount quote_amount = opposite_asset_amount @@ -905,8 +942,8 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals .format(order_type, limiter, balance)) place_order = False elif allow_partial: - self.log.debug('Limiting {} order amount to available asset balance: {} {}'.format( - order_type, balance, symbol)) + self.log.debug('Limiting {} order amount to available asset balance: {} {}' + .format(order_type, balance, symbol)) if asset == 'base': quote_amount = balance / price elif asset == 'quote': @@ -939,6 +976,8 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente .format(market_center_price, price, self.upper_bound)) return + amount_quote = 0 + previous_price = 0 if self.mode == 'mountain' or self.mode == 'buy_slope': previous_price = price orders_sum = 0 @@ -1025,6 +1064,8 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p .format(market_center_price, price, self.lower_bound)) return + amount_quote = 0 + previous_price = 0 if self.mode == 'mountain' or self.mode == 'sell_slope': previous_price = price orders_sum = 0 From 840beb3bb721163d28471bc9083be302c593e8a3 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 16:14:29 +0300 Subject: [PATCH 0773/1846] Change dexbot version number to 0.6.3 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 1000f6b7a..e4a35be02 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.6.2' +VERSION = '0.6.3' AUTHOR = 'Codaone Oy' __version__ = VERSION From 85fb389a87fa42a83d0ca678e670a24a78a43a28 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 16:35:54 +0300 Subject: [PATCH 0774/1846] Change dexbot version number to 0.6.4 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index e4a35be02..64508fe79 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.6.3' +VERSION = '0.6.4' AUTHOR = 'Codaone Oy' __version__ = VERSION From 78b1130e7879b6e254f4b03b7b63d44a599818e3 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 19 Sep 2018 07:43:58 +0300 Subject: [PATCH 0775/1846] Add settings windows ui file --- dexbot/views/ui/global_settings_window.ui | 222 ++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 dexbot/views/ui/global_settings_window.ui diff --git a/dexbot/views/ui/global_settings_window.ui b/dexbot/views/ui/global_settings_window.ui new file mode 100644 index 000000000..a1863146f --- /dev/null +++ b/dexbot/views/ui/global_settings_window.ui @@ -0,0 +1,222 @@ + + + global_settings + + + + 0 + 0 + 600 + 400 + + + + + 0 + 0 + + + + + 600 + 400 + + + + + 600 + 600 + + + + TabWidget + + + 0 + + + + Nodes + + + + + 9 + 55 + 580 + 192 + + + + + 580 + 0 + + + + + 580 + 16777215 + + + + true + + + QAbstractItemView::DragDrop + + + false + + + true + + + false + + + false + + + + # + + + + + Node + + + + + 1 + + + wss://testnet.nodes.bitshares.ws + + + ItemIsSelectable|ItemIsEditable|ItemIsDragEnabled|ItemIsUserCheckable|ItemIsEnabled + + + + + 2 + + + wss://node.testnet.bitshares.eu + + + + + 3 + + + wss://testnet.dex.trading + + + + + 4 + + + wss://testnet.bitshares.apasia.tech/ws + + + + + + + 15 + 331 + 176 + 27 + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Save + + + false + + + + + + 150 + 10 + 436 + 38 + + + + + + + + 150 + 20 + + + + border: 0px; background-color: #3a5162; width: 100px; height: 20px; border-radius: 5px; color: #fff; + + + Move down + + + + + + + + 150 + 20 + + + + border: 0px; background-color: #3a5162; width: 100px; height: 20px; border-radius: 5px; color: #fff; + + + Move up + + + + + + + + 150 + 20 + + + + border: 0px; background-color: #623a3a; width: 100px; height: 20px; border-radius: 5px; color: #fff; + + + Remove + + + + + + + + 150 + 20 + + + + border: 0px; background-color: #3a623a; width: 100px; height: 20px; border-radius: 5px; color: #fff; + + + Add + + + + + + + + + + From 906909510679fc900cf6ad948f8136dd344170a6 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 19 Sep 2018 07:45:50 +0300 Subject: [PATCH 0776/1846] Change comment on get_market_spread() --- dexbot/strategies/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index e7e7abf02..e4f897513 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -703,8 +703,7 @@ def get_market_sell_price(self, quote_amount=0, base_amount=00): return base_amount / quote_amount def get_market_spread(self, quote_amount=0, base_amount=0): - """ Returns the market spread %, including own orders, from specified depth, enhanced with moving average or - weighted moving average. + """ Returns the market spread %, including own orders, from specified depth. :param float | quote_amount: :param float | base_amount: From 93c9caf1e772ab8ced12cdc5f08e4f83948ecd36 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 19 Sep 2018 12:12:54 +0300 Subject: [PATCH 0777/1846] Add new fields to relative orders widget --- .../views/ui/forms/relative_orders_widget.ui | 443 +++++++++++++++++- 1 file changed, 420 insertions(+), 23 deletions(-) diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index ce3ec4e66..bd658cc8f 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 449 - 465 + 570 @@ -420,7 +420,317 @@ - + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Market depth + + + true + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + From which depth will market spread be measured? (QUOTE amount) + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 9 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 8 + + + 1000000000.000000000000000 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + + + + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Dynamic spread factor + + + true + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + How many percent will own spread be compared to market spread? + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + % + + + 0.010000000000000 + + + 1000.000000000000000 + + + 1.000000000000000 + + + + @@ -477,7 +787,7 @@ - Center Price + Center price center_price_input @@ -515,7 +825,7 @@ - + @@ -613,7 +923,7 @@ - + @@ -693,7 +1003,7 @@ - + @@ -712,7 +1022,7 @@ - + @@ -792,14 +1102,14 @@ - + Center price offset based on asset balances - + @@ -894,7 +1204,7 @@ - + @@ -1017,7 +1327,7 @@ QSlider::handle:horizontal { - + @@ -1097,14 +1407,14 @@ QSlider::handle:horizontal { - + Reset orders on partial fill - + @@ -1199,7 +1509,7 @@ QSlider::handle:horizontal { - + @@ -1230,7 +1540,7 @@ QSlider::handle:horizontal { - + @@ -1310,14 +1620,14 @@ QSlider::handle:horizontal { - + - Resert orders on center price change + Reset orders on center price change - + @@ -1412,7 +1722,7 @@ QSlider::handle:horizontal { - + @@ -1443,7 +1753,7 @@ QSlider::handle:horizontal { - + @@ -1523,14 +1833,14 @@ QSlider::handle:horizontal { - + Custom expiration - + @@ -1625,7 +1935,7 @@ QSlider::handle:horizontal { - + @@ -1659,6 +1969,93 @@ QSlider::handle:horizontal { + + + + Dynamic spread + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 101 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Enable dynamic spread which overrides the spread field + + + ? + + + 5 + + + + + + From 53fd3404a1b9663baa9cc7a225bc502671ce0d46 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 19 Sep 2018 12:13:24 +0300 Subject: [PATCH 0778/1846] Add new fields to strategy controller --- dexbot/controllers/strategy_controller.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 396dec803..0383affec 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -107,6 +107,7 @@ def __init__(self, view, configure, worker_controller, worker_data): # Event connecting widget.relative_order_size_input.clicked.connect(self.onchange_relative_order_size_input) + widget.dynamic_spread_input.clicked.connect(self.onchange_dynamic_spread_input) widget.center_price_dynamic_input.clicked.connect(self.onchange_center_price_dynamic_input) widget.manual_offset_input.valueChanged.connect(self.onchange_manual_offset_input) widget.reset_on_partial_fill_input.clicked.connect(self.onchange_reset_on_partial_fill_input) @@ -116,6 +117,7 @@ def __init__(self, view, configure, worker_controller, worker_data): # Trigger the onchange events once self.onchange_relative_order_size_input(widget.relative_order_size_input.isChecked()) self.onchange_center_price_dynamic_input(widget.center_price_dynamic_input.isChecked()) + self.onchange_dynamic_spread_input(widget.dynamic_spread_input.isChecked()) self.onchange_reset_on_partial_fill_input(widget.reset_on_partial_fill_input.isChecked()) self.onchange_reset_on_price_change_input(widget.reset_on_price_change_input.isChecked()) self.onchange_custom_expiration_input(widget.custom_expiration_input.isChecked()) @@ -133,6 +135,18 @@ def onchange_manual_offset_input(self): text = "{}%".format(value) self.view.strategy_widget.manual_offset_amount_label.setText(text) + def onchange_dynamic_spread_input(self, checked): + if checked: + self.view.strategy_widget.market_depth_amount_input.setDisabled(False) + self.view.strategy_widget.dynamic_spread_factor_input.setDisabled(False) + # Disable the spread field if dynamic spread in use + self.view.strategy_widget.spread_input.setDisabled(True) + else: + self.view.strategy_widget.market_depth_amount_input.setDisabled(True) + self.view.strategy_widget.dynamic_spread_factor_input.setDisabled(True) + # Enable spread field if dynamic not in use + self.view.strategy_widget.spread_input.setDisabled(False) + def onchange_relative_order_size_input(self, checked): if checked: self.order_size_input_to_relative() @@ -173,9 +187,9 @@ def onchange_asset_labels(self): quote_symbol = self.worker_controller.view.quote_asset_input.text() if quote_symbol: - self.set_amount_asset_label(quote_symbol) + self.set_quote_asset_label(quote_symbol) else: - self.set_amount_asset_label('') + self.set_quote_asset_label('') if base_symbol and quote_symbol: text = '{} / {}'.format(base_symbol, quote_symbol) @@ -197,8 +211,9 @@ def order_size_input_to_static(self): def set_center_price_market_label(self, text): self.view.strategy_widget.center_price_market_label.setText(text) - def set_amount_asset_label(self, text): + def set_quote_asset_label(self, text): self.view.strategy_widget.amount_input_asset_label.setText(text) + self.view.strategy_widget.market_depth_amount_input_asset_label.setText(text) def validation_errors(self): error_texts = [] From 6e47ad916948d4e660b8f105be3d1fdcb7778d66 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 19 Sep 2018 12:14:17 +0300 Subject: [PATCH 0779/1846] Add new spread fields to strategy --- dexbot/strategies/relative_orders.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 8e59b0cf5..9bf6ee69e 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -19,6 +19,14 @@ def configure(cls, return_base_config=True): 'Amount is expressed as a percentage of the account balance of quote/base asset', None), ConfigElement('spread', 'float', 5, 'Spread', 'The percentage difference between buy and sell', (0, 100, 2, '%')), + ConfigElement('dynamic_spread', 'bool', False, 'Dynamic spread', + 'Enable dynamic spread which overrides the spread field', None), + ConfigElement('market_depth_amount', 'float', 0, 'Market depth', + 'From which depth will market spread be measured? (QUOTE amount)' + , (0.00000001, 1000000000, 8, '')), + ConfigElement('dynamic_spread_factor', 'float', 1, 'Dynamic spread factor', + 'How many percent will own spread be compared to market spread?' + , (0.01, 1000, 2, '%')), ConfigElement('center_price', 'float', 0, 'Center price', 'Fixed center price expressed in base asset: base/quote', (0, None, 8, '')), ConfigElement('center_price_dynamic', 'bool', True, 'Update center price from closest market orders', @@ -69,7 +77,12 @@ def __init__(self, *args, **kwargs): self.is_asset_offset = self.worker.get('center_price_offset', False) self.manual_offset = self.worker.get('manual_offset', 0) / 100 self.order_size = float(self.worker.get('amount', 1)) + + # Spread options self.spread = self.worker.get('spread') / 100 + self.dynamic_spread = self.worker.get('dynamic_spread', False) + self.market_depth_amount = self.worker.get('market_depth_amount', 0) + self.dynamic_spread_factor = self.worker.get('dynamic_spread_factor', 1) / 100 self.is_reset_on_partial_fill = self.worker.get('reset_on_partial_fill', True) self.partial_fill_threshold = self.worker.get('partial_fill_threshold', 30) / 100 self.is_reset_on_price_change = self.worker.get('reset_on_price_change', False) From 7933bc259616e4f3b1d8a68caeb1db7bf01a305e Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 19 Sep 2018 13:54:53 +0300 Subject: [PATCH 0780/1846] Change comment styling --- 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 9bf6ee69e..969d69a44 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -109,8 +109,8 @@ def __init__(self, *args, **kwargs): self.disabled = True return - # Check old orders from previous run (from force-interruption) only whether we are not using "Reset orders on - # center price change" option + # Check old orders from previous run (from force-interruption) only whether we are not using + # "Reset orders on center price change" option if self.is_reset_on_price_change: self.log.info('"Reset orders on center price change" is active, placing fresh orders') self.update_orders() From d0d27fa66844a98366dc0cb4b13d05be7112741e Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 19 Sep 2018 13:55:22 +0300 Subject: [PATCH 0781/1846] Change spread calculation in relative orders --- dexbot/strategies/relative_orders.py | 32 ++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 969d69a44..7723eb5c4 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -152,11 +152,18 @@ def amount_base(self): return self.order_size def calculate_order_prices(self): + + # Calculate spread if dynamic spread option in use, this calculation doesn't include own orders on the market + if self.dynamic_spread: + spread = self.get_market_spread(quote_amount=self.market_depth_amount) * self.dynamic_spread_factor + else: + spread = self.spread + if self.is_center_price_dynamic: self.center_price = self.calculate_center_price( None, self.is_asset_offset, - self.spread, + spread, self['order_ids'], self.manual_offset ) @@ -164,24 +171,24 @@ def calculate_order_prices(self): self.center_price = self.calculate_center_price( self.center_price, self.is_asset_offset, - self.spread, + spread, self['order_ids'], self.manual_offset ) - self.buy_price = self.center_price / math.sqrt(1 + self.spread) - self.sell_price = self.center_price * math.sqrt(1 + self.spread) + self.buy_price = self.center_price / math.sqrt(1 + spread) + self.sell_price = self.center_price * math.sqrt(1 + spread) def update_orders(self): self.log.debug('Starting to update orders') - # Recalculate buy and sell order prices - self.calculate_order_prices() - # Cancel the orders before redoing them self.cancel_all_orders() self.clear_orders() + # Recalculate buy and sell order prices + self.calculate_order_prices() + order_ids = [] expected_num_orders = 0 @@ -317,11 +324,18 @@ def check_orders(self, *args, **kwargs): # we're updating order it may be filled further so trade log entry will not # be correct - if self.is_reset_on_price_change and not self.is_center_price_dynamic: + # Check center price change when using market center price with reset option on change + if self.is_reset_on_price_change and self.is_center_price_dynamic: + # Calculate spread if dynamic spread option in use, this calculation includes own orders on the market + if self.dynamic_spread: + spread = self.get_market_spread(quote_amount=self.market_depth_amount) * self.dynamic_spread_factor + else: + spread = self.spread + center_price = self.calculate_center_price( None, self.is_asset_offset, - self.spread, + spread, self['order_ids'], self.manual_offset ) From d77b708da25f2f0b298fe257c1c7acbbc64b4a03 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Wed, 19 Sep 2018 21:13:46 +0300 Subject: [PATCH 0782/1846] WIP: neutral mode for staggered orders --- dexbot/strategies/staggered_orders.py | 131 +++++++++++++++++++++++++- 1 file changed, 127 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 77886760e..615a1dbf6 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -489,6 +489,9 @@ def allocate_asset(self, asset, asset_balance): own_asset_limit = closest_opposite_order['quote']['amount'] self.log.debug('Limiting {} order by opposite order: {} {}' .format(order_type, own_asset_limit, symbol)) + elif self.mode == 'neutral': + # todo: implement + pass elif (self.mode == 'valley' or (self.mode == 'buy_slope' and asset == 'quote') or (self.mode == 'sell_slope' and asset == 'base')): @@ -767,7 +770,89 @@ def increase_order_sizes(self, asset, asset_balance, orders): return elif self.mode == 'neutral': - pass + # todo: convert to neutral + if asset == 'quote': + total_balance = self.quote_total_balance + order_type = 'sell' + elif asset == 'base': + total_balance = self.base_total_balance + order_type = 'buy' + + # Get orders and amounts to be compared. Note: orders are sorted from low price to high + for order in orders: + order_index = orders.index(order) + order_amount = order['base']['amount'] + + # This check prevents choosing order with index lower than the list length + if order_index == 0: + # In case checking the first order, use the same order, but increased by 1 increment + # This allows our closest order amount exceed highest opposite-side order amount + closer_order = order + closer_bound = closer_order['base']['amount'] * (1 + self.increment) + else: + closer_order = orders[order_index - 1] + closer_bound = closer_order['base']['amount'] + + # This check prevents choosing order with index higher than the list length + if order_index + 1 < len(orders): + # Current order is a not furthest order + further_order = orders[order_index + 1] + is_least_order = False + else: + # Current order is furthest order + further_order = orders[order_index] + is_least_order = True + + further_bound = further_order['base']['amount'] * (1 + self.increment) + + if (further_bound > order_amount * (1 + self.increment / 10) < closer_bound and + further_bound - order_amount >= order_amount * self.increment / 2): + # Calculate new order size and place the order to the market + new_order_amount = further_bound + + if is_least_order: + new_orders_sum = 0 + amount = order_amount + for o in orders: + amount = amount * (1 + self.increment) + new_orders_sum += amount + # To reduce allocation rounds, increase furthest order more + new_order_amount = order_amount * (total_balance / new_orders_sum) \ + * (1 + self.increment * 0.75) + + if new_order_amount < closer_bound: + """ This is for situations when calculated new_order_amount is not big enough to + allocate all funds. Use partial-increment increase, so we'll got at least one full + increase round. Whether we will just use `new_order_amount = further_bound`, we will + get less than one full allocation round, thus leaving closest-to-center order not + increased. + """ + new_order_amount = closer_bound / (1 + self.increment * 0.2) + + # Limit sell order to available balance + if asset_balance < new_order_amount - order_amount: + new_order_amount = order_amount + asset_balance['amount'] + self.log.info('Limiting new {} order to avail asset balance: {:.8f} {}' + .format(order_type, new_order_amount, asset_balance['symbol'])) + quote_amount = 0 + price = 0 + + if asset == 'quote': + price = (order['price'] ** -1) + quote_amount = new_order_amount + elif asset == 'base': + price = order['price'] + quote_amount = new_order_amount / price + + self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}, amount: {}, price: {:.8f}' + .format(order_type, self.mode, order_amount, price)) + self.cancel(order) + if asset == 'quote': + self.market_sell(quote_amount, price) + elif asset == 'base': + self.market_buy(quote_amount, price) + # Only one increase at a time. This prevents running more than one increment round simultaneously + return return None def check_partial_fill(self, order): @@ -846,6 +931,8 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False (self.mode == 'sell_slope' and asset == 'quote')): own_asset_amount = order['base']['amount'] opposite_asset_amount = own_asset_amount / price + elif self.mode == 'neutral': + # todo: implement # Apply limits. Limit order only whether passed limit is less than expected order size if own_asset_limit and own_asset_limit < own_asset_amount: @@ -919,6 +1006,8 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals if self.mode == 'mountain' or self.mode == 'buy_slope': opposite_asset_amount = order['quote']['amount'] own_asset_amount = opposite_asset_amount * price + if self.mode == 'neutral': + # todo: implement elif self.mode == 'valley' or self.mode == 'buy_slope': own_asset_amount = order['base']['amount'] opposite_asset_amount = own_asset_amount / price @@ -994,6 +1083,22 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente price = previous_price amount_quote = previous_amount * (self.quote_total_balance / orders_sum) * (1 + self.increment * 0.75) + if self.mode == 'neutral': + previous_price = price + orders_sum = 0 + amount = quote_balance['amount'] * math.sqrt(1 + self.increment) + previous_amount = amount + + while price <= self.upper_bound: + orders_sum += previous_amount + previous_price = price + previous_amount = amount + price = price * math.sqrt(1 + self.increment) + amount = amount / math.sqrt(1 + self.increment) + + price = previous_price + amount_quote = previous_amount * (self.quote_total_balance / orders_sum) * (1 + self.increment * 0.75) + elif self.mode == 'valley' or self.mode == 'sell_slope': orders_count = 0 while price <= self.upper_bound: @@ -1082,6 +1187,27 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p amount_base = previous_amount * (self.base_total_balance / orders_sum) * (1 + self.increment * 0.75) price = previous_price amount_quote = amount_base / price + + elif self.mode == 'neutral': + previous_price = price + orders_sum = 0 + amount = base_balance['amount'] * sqrt(1 + self.increment) + previous_amount = amount + + while price >= self.lower_bound: + orders_sum += previous_amount + previous_price = price + previous_amount = amount + price = price / sqrt(1 + self.increment) + amount = amount / sqrt(1 + self.increment) + + amount_base = previous_amount * (self.base_total_balance / orders_sum) * (1 + self.increment * 0.75) + price = previous_price + amount_quote = amount_base / price + + precision = self.market['quote']['precision'] + amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) + elif self.mode == 'valley' or self.mode == 'buy_slope': orders_count = 0 while price >= self.lower_bound: @@ -1097,9 +1223,6 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p """ amount_quote = amount_quote / (1 + self.increment / 100) - precision = self.market['quote']['precision'] - amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) - if place_order: self.market_buy(amount_quote, price) else: From a9731c12ecc404222f494887308024bc0d945133 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 20 Sep 2018 14:45:40 +0300 Subject: [PATCH 0783/1846] Change how dynamic center price fields look --- .../views/ui/forms/relative_orders_widget.ui | 368 +++++++++++++----- 1 file changed, 273 insertions(+), 95 deletions(-) diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index bd658cc8f..23af59351 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 449 - 570 + 597 @@ -420,6 +420,93 @@ + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 101 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Enable dynamic spread which overrides the spread field + + + ? + + + 5 + + + + + + + + + + Dynamic spread + + + @@ -730,7 +817,7 @@ - + @@ -825,7 +912,7 @@ - + @@ -923,7 +1010,7 @@ - + @@ -966,7 +1053,7 @@ - 40 + 101 20 @@ -990,7 +1077,7 @@ WhatsThisCursor - Always calculate the middle from the closest market orders + Estimate the center from closest opposite orders or from a depth ? @@ -1003,7 +1090,7 @@ - + @@ -1012,7 +1099,7 @@ - Update center price from closest market orders + Measure center price from market orders true @@ -1022,6 +1109,184 @@ + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Measurement depth + + + true + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Cumulative quote amount from which depth center price will be measured + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 9 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 8 + + + 1000000000.000000000000000 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + + + + + + @@ -1969,93 +2234,6 @@ QSlider::handle:horizontal { - - - - Dynamic spread - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 101 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Enable dynamic spread which overrides the spread field - - - ? - - - 5 - - - - - - From 9f4c79287221b66e2123a3c37e6a2cec73dab833 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 20 Sep 2018 14:48:02 +0300 Subject: [PATCH 0784/1846] Change dynamic center price GUI logic --- dexbot/controllers/strategy_controller.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 0383affec..a4b6a3de1 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -156,11 +156,14 @@ def onchange_relative_order_size_input(self, checked): def onchange_center_price_dynamic_input(self, checked): if checked: self.view.strategy_widget.center_price_input.setDisabled(True) + self.view.strategy_widget.center_price_depth_input.setDisabled(False) self.view.strategy_widget.reset_on_price_change_input.setDisabled(False) + if self.view.strategy_widget.reset_on_price_change_input.isChecked(): self.view.strategy_widget.price_change_threshold_input.setDisabled(False) else: self.view.strategy_widget.center_price_input.setDisabled(False) + self.view.strategy_widget.center_price_depth_input.setDisabled(True) self.view.strategy_widget.reset_on_price_change_input.setDisabled(True) self.view.strategy_widget.price_change_threshold_input.setDisabled(True) @@ -213,6 +216,7 @@ def set_center_price_market_label(self, text): def set_quote_asset_label(self, text): self.view.strategy_widget.amount_input_asset_label.setText(text) + self.view.strategy_widget.center_price_depth_input_asset_label.setText(text) self.view.strategy_widget.market_depth_amount_input_asset_label.setText(text) def validation_errors(self): From c336d1bef5e3c2e1ee6e8f1fbebf757716c9e700 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 20 Sep 2018 15:00:21 +0300 Subject: [PATCH 0785/1846] Change comments --- 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 7723eb5c4..3c11ca7ed 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -168,6 +168,7 @@ def calculate_order_prices(self): self.manual_offset ) else: + # User has given center price to use self.center_price = self.calculate_center_price( self.center_price, self.is_asset_offset, @@ -249,8 +250,7 @@ def calculate_center_price(self, center_price=None, asset_offset=False, spread=N # No center price was given so we simply calculate the center price calculated_center_price = self._calculate_center_price(suppress_errors) else: - # Center price was given so we only use the calculated center price - # for quote to base asset conversion + # 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 From a8f25975072036e7db71ae5d9fd8bb07bda231f0 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 20 Sep 2018 15:00:36 +0300 Subject: [PATCH 0786/1846] Update ConfigElements for dynamic center price --- dexbot/strategies/relative_orders.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 3c11ca7ed..35904eaf4 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -29,8 +29,11 @@ def configure(cls, return_base_config=True): , (0.01, 1000, 2, '%')), ConfigElement('center_price', 'float', 0, 'Center price', 'Fixed center price expressed in base asset: base/quote', (0, None, 8, '')), - ConfigElement('center_price_dynamic', 'bool', True, 'Update center price from closest market orders', - 'Always calculate the middle from the closest market orders', None), + ConfigElement('center_price_dynamic', 'bool', True, 'Measure center price from market orders', + 'Estimate the center from closest opposite orders or from a depth', None), + ConfigElement('center_price_depth', 'float', 0, 'Measurement depth', + 'Cumulative quote amount from which depth center price will be measured' + , (0.00000001, 1000000000, 8, '')), ConfigElement('center_price_offset', 'bool', False, 'Center price offset based on asset balances', 'Automatically adjust orders up or down based on the imbalance of your assets', None), ConfigElement('manual_offset', 'float', 0, 'Manual center price offset', From 4265dad573cc2bdbb2840656b5392f286b3c5c2b Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 20 Sep 2018 15:28:37 +0300 Subject: [PATCH 0787/1846] WIP Change center price calculation with depth --- dexbot/strategies/relative_orders.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 35904eaf4..605a9421e 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -70,9 +70,11 @@ def __init__(self, *args, **kwargs): self.error_onMarketUpdate = self.error self.error_onAccount = self.error - self.is_center_price_dynamic = self.worker["center_price_dynamic"] + # Worker parameters + self.is_center_price_dynamic = self.worker['center_price_dynamic'] if self.is_center_price_dynamic: self.center_price = None + self.center_price_depth = self.worker.get('center_price_depth', 0) else: self.center_price = self.worker["center_price"] @@ -86,6 +88,7 @@ def __init__(self, *args, **kwargs): self.dynamic_spread = self.worker.get('dynamic_spread', False) self.market_depth_amount = self.worker.get('market_depth_amount', 0) self.dynamic_spread_factor = self.worker.get('dynamic_spread_factor', 1) / 100 + self.is_reset_on_partial_fill = self.worker.get('reset_on_partial_fill', True) self.partial_fill_threshold = self.worker.get('partial_fill_threshold', 30) / 100 self.is_reset_on_price_change = self.worker.get('reset_on_price_change', False) @@ -155,6 +158,7 @@ def amount_base(self): return self.order_size def calculate_order_prices(self): + center_price = None # Calculate spread if dynamic spread option in use, this calculation doesn't include own orders on the market if self.dynamic_spread: @@ -163,15 +167,19 @@ def calculate_order_prices(self): spread = self.spread if self.is_center_price_dynamic: + if self.center_price_depth > 0: + # Calculate with quote amount if given + center_price = self.get_market_center_price(quote_amount=self.center_price_depth) + self.center_price = self.calculate_center_price( - None, + center_price, self.is_asset_offset, spread, self['order_ids'], self.manual_offset ) else: - # User has given center price to use + # User has given center price to use, calculate offsets and spread self.center_price = self.calculate_center_price( self.center_price, self.is_asset_offset, From 53dd99a1645a4aa2063e7eef1fab6b8b11b8566e Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 20 Sep 2018 18:40:36 +0500 Subject: [PATCH 0788/1846] Use string formatting to round price --- dexbot/strategies/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index e7e7abf02..e3f1513ef 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -914,7 +914,7 @@ def place_market_buy_order(self, amount, price, return_none=False, *args, **kwar self.disabled = True return None - self.log.info('Placing a buy order for {} {} @ {}'.format(base_amount, symbol, round(price, 8))) + self.log.info('Placing a buy order for {} {} @ {;.8f}'.format(base_amount, symbol, price) # Place the order buy_transaction = self.retry_action( @@ -965,7 +965,7 @@ def place_market_sell_order(self, amount, price, return_none=False, *args, **kwa self.disabled = True return None - self.log.info('Placing a sell order for {} {} @ {}'.format(quote_amount, symbol, round(price, 8))) + self.log.info('Placing a sell order for {} {} @ {:.8f}'.format(quote_amount, symbol, price)) # Place the order sell_transaction = self.retry_action( From 0e17c358330332e22469b87c893e809704e02374 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 20 Sep 2018 18:49:24 +0500 Subject: [PATCH 0789/1846] Allow to control returnOrderId This is needed to be able to place orders in non-blocking mode. --- dexbot/strategies/base.py | 49 ++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index e3f1513ef..1dad34352 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -213,6 +213,9 @@ def __init__(self, # Order expiration time in seconds self.expiration = 60 * 60 * 24 * 365 * 5 + # buy/sell actions will return order id by default + self.returnOrderId = 'head' + # A private logger that adds worker identify data to the LogRecord self.log = logging.LoggerAdapter( logging.getLogger('dexbot.per_worker'), @@ -909,12 +912,12 @@ def place_market_buy_order(self, amount, price, return_none=False, *args, **kwar return None # Make sure we have enough balance for the order - if self.balance(self.market['base']) < base_amount: + if self.returnOrderId and self.balance(self.market['base']) < base_amount: self.log.critical("Insufficient buy balance, needed {} {}".format(base_amount, symbol)) self.disabled = True return None - self.log.info('Placing a buy order for {} {} @ {;.8f}'.format(base_amount, symbol, price) + self.log.info('Placing a buy order for {} {} @ {;.8f}'.format(base_amount, symbol, price)) # Place the order buy_transaction = self.retry_action( @@ -923,21 +926,23 @@ def place_market_buy_order(self, amount, price, return_none=False, *args, **kwar Amount(amount=amount, asset=self.market["quote"]), account=self.account.name, expiration=self.expiration, - returnOrderId="head", + returnOrderId=self.returnOrderId, fee_asset=self.fee_asset['id'], *args, **kwargs ) self.log.debug('Placed buy order {}'.format(buy_transaction)) - buy_order = self.get_order(buy_transaction['orderid'], return_none=return_none) - if buy_order and 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 + if self.returnOrderId: + buy_order = self.get_order(buy_transaction['orderid'], return_none=return_none) + if buy_order and 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 + else: + return True def place_market_sell_order(self, amount, price, return_none=False, *args, **kwargs): """ Places a sell order in the market @@ -960,7 +965,7 @@ def place_market_sell_order(self, amount, price, return_none=False, *args, **kwa return None # Make sure we have enough balance for the order - if self.balance(self.market['quote']) < quote_amount: + if self.returnOrderId and self.balance(self.market['quote']) < quote_amount: self.log.critical("Insufficient sell balance, needed {} {}".format(amount, symbol)) self.disabled = True return None @@ -974,21 +979,23 @@ def place_market_sell_order(self, amount, price, return_none=False, *args, **kwa Amount(amount=amount, asset=self.market["quote"]), account=self.account.name, expiration=self.expiration, - returnOrderId="head", + returnOrderId=self.returnOrderId, fee_asset=self.fee_asset['id'], *args, **kwargs ) self.log.debug('Placed sell order {}'.format(sell_transaction)) - sell_order = self.get_order(sell_transaction['orderid'], return_none=return_none) - if sell_order and 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 + if self.returnOrderId: + sell_order = self.get_order(sell_transaction['orderid'], return_none=return_none) + if sell_order and 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 + else: + return True def retry_action(self, action, *args, **kwargs): """ Perform an action, and if certain suspected-to-be-spurious grapheme bugs occur, From 39ecc775dd8723d65ba33220fed6669aadbb2d56 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 20 Sep 2018 20:35:20 +0500 Subject: [PATCH 0790/1846] Refactor fallback logic when target spread is not reached Instead of relying on allocation status of base and quote, just use balance changes as a trigger. Whether balances are not changing and bootstrap is off and target spread is not reached, we can safely proceed to a fallback logic. --- dexbot/strategies/staggered_orders.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 77886760e..c06e257fc 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -259,9 +259,10 @@ def maintain_strategy(self, *args, **kwargs): 'balances'.format(self.min_check_interval)) self.current_check_interval = self.min_check_interval - # Do not continue whether assets is not fully allocated - if (not base_allocated or not quote_allocated) or self.bootstrapping: - # Further checks should be performed on next maintenance + # Do not continue whether balances are changing or bootstrap is on + if (self.bootstrapping or + self.base_balance_history[0] != self.base_balance_history[2] or + self.quote_balance_history[0] != self.quote_balance_history[2]): self.last_check = datetime.now() self.log_maintenance_time() return @@ -276,15 +277,15 @@ def maintain_strategy(self, *args, **kwargs): if self.market_center_price > highest_buy_price * (1 + self.target_spread): # Cancel lowest buy order because center price moved up. # On the next run there will be placed next buy order closer to the new center - self.log.info('No avail balances and we not in bootstrap mode and target spread is not reached. ' - 'Cancelling lowest buy order as a fallback.') + self.log.info('Free balances are not changing and we are not in bootstrap mode and target spread is ' + 'not reached. Cancelling lowest buy order as a fallback.') self.cancel(self.buy_orders[-1]) else: if self.market_center_price < lowest_sell_price * (1 - self.target_spread): # Cancel highest sell order because center price moved down. # On the next run there will be placed next sell closer to the new center - self.log.info('No avail balances and we not in bootstrap mode and target spread is not reached. ' - 'Cancelling highest sell order as a fallback.') + self.log.info('Free balances are not changing and we are not in bootstrap mode and target spread is ' + 'not reached. Cancelling highest sell order as a fallback.') self.cancel(self.sell_orders[-1]) self.last_check = datetime.now() From 6a87547f12757ea31d01f61e10eebdc39fbc6ea4 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 22 Sep 2018 21:17:07 +0500 Subject: [PATCH 0791/1846] Improve fallback logic Cancel furthest order only whether target spread is not reached. --- dexbot/strategies/staggered_orders.py | 33 +++++++++++++++++---------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c06e257fc..19063cbfd 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -171,18 +171,6 @@ def maintain_strategy(self, *args, **kwargs): self.log.warning('Cannot calculate center price on empty market, please set is manually') return - # Get highest buy and lowest sell prices from orders - highest_buy_price = 0 - lowest_sell_price = 0 - - if self.buy_orders: - highest_buy_price = self.buy_orders[0].get('price') - - if self.sell_orders: - lowest_sell_price = self.sell_orders[0].get('price') - # Invert the sell price to BASE so it can be used in comparison - lowest_sell_price = lowest_sell_price ** -1 - # Calculate balances self.refresh_balances() @@ -269,6 +257,27 @@ def maintain_strategy(self, *args, **kwargs): # There are no funds and current orders aren't close enough, try to fix the situation by shifting orders. # This is a fallback logic. + + # Get highest buy and lowest sell prices from orders + highest_buy_price = 0 + lowest_sell_price = 0 + + if self.buy_orders: + highest_buy_price = self.buy_orders[0].get('price') + + if self.sell_orders: + lowest_sell_price = self.sell_orders[0].get('price') + # Invert the sell price to BASE so it can be used in comparison + lowest_sell_price = lowest_sell_price ** -1 + + if highest_buy_price and lowest_sell_price: + self.actual_spread = (lowest_sell_price / highest_buy_price) - 1 + if self.actual_spread < self.target_spread + self.increment: + # Target spread is reached, no need to cancel anything + self.last_check = datetime.now() + self.log_maintenance_time() + return + # Measure which price is closer to the center buy_distance = self.market_center_price - highest_buy_price sell_distance = lowest_sell_price - self.market_center_price From 84102b72f45f22cd73c21e2214d1c9c1673c8956 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 24 Sep 2018 12:28:14 +0300 Subject: [PATCH 0792/1846] Change code styling on ConfigElement list --- dexbot/strategies/relative_orders.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 605a9421e..cecfaf545 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -22,18 +22,18 @@ def configure(cls, return_base_config=True): ConfigElement('dynamic_spread', 'bool', False, 'Dynamic spread', 'Enable dynamic spread which overrides the spread field', None), ConfigElement('market_depth_amount', 'float', 0, 'Market depth', - 'From which depth will market spread be measured? (QUOTE amount)' - , (0.00000001, 1000000000, 8, '')), + 'From which depth will market spread be measured? (QUOTE amount)', + (0.00000001, 1000000000, 8, '')), ConfigElement('dynamic_spread_factor', 'float', 1, 'Dynamic spread factor', - 'How many percent will own spread be compared to market spread?' - , (0.01, 1000, 2, '%')), + 'How many percent will own spread be compared to market spread?', + (0.01, 1000, 2, '%')), ConfigElement('center_price', 'float', 0, 'Center price', 'Fixed center price expressed in base asset: base/quote', (0, None, 8, '')), ConfigElement('center_price_dynamic', 'bool', True, 'Measure center price from market orders', 'Estimate the center from closest opposite orders or from a depth', None), ConfigElement('center_price_depth', 'float', 0, 'Measurement depth', - 'Cumulative quote amount from which depth center price will be measured' - , (0.00000001, 1000000000, 8, '')), + 'Cumulative quote amount from which depth center price will be measured', + (0.00000001, 1000000000, 8, '')), ConfigElement('center_price_offset', 'bool', False, 'Center price offset based on asset balances', 'Automatically adjust orders up or down based on the imbalance of your assets', None), ConfigElement('manual_offset', 'float', 0, 'Manual center price offset', From 9d3354f79f6798db3eb5692007ff9fb2eeac695e Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 24 Sep 2018 12:29:16 +0300 Subject: [PATCH 0793/1846] Refactor calculate_center_price() --- dexbot/strategies/relative_orders.py | 71 ++++++++++++++++++---------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index cecfaf545..026fde627 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -158,15 +158,16 @@ def amount_base(self): return self.order_size def calculate_order_prices(self): + # Set center price as None, in case dynamic has not amount given, center price is calculated from market orders center_price = None + spread = self.spread # Calculate spread if dynamic spread option in use, this calculation doesn't include own orders on the market if self.dynamic_spread: spread = self.get_market_spread(quote_amount=self.market_depth_amount) * self.dynamic_spread_factor - else: - spread = self.spread if self.is_center_price_dynamic: + # Calculate center price from the market orders if self.center_price_depth > 0: # Calculate with quote amount if given center_price = self.get_market_center_price(quote_amount=self.center_price_depth) @@ -250,8 +251,8 @@ def _calculate_center_price(self, suppress_errors=False): self.disabled = True return None - center_price = highest_bid['price'] * math.sqrt(lowest_ask['price'] / highest_bid['price']) - return center_price + # Calculate center price between two closest orders on the market + return highest_bid['price'] * math.sqrt(lowest_ask['price'] / highest_bid['price']) def calculate_center_price(self, center_price=None, asset_offset=False, spread=None, order_ids=None, manual_offset=0, suppress_errors=False): @@ -269,31 +270,53 @@ def calculate_center_price(self, center_price=None, asset_offset=False, spread=N if center_price: calculated_center_price = center_price + # Calculate asset based offset to the center price if asset_offset: - total_balance = self.count_asset(order_ids) - total = (total_balance['quote'] * calculated_center_price) + total_balance['base'] - - if not total: # Prevent division by zero - balance = 0 - else: - # Returns a value between -1 and 1 - balance = (total_balance['base'] / total) * 2 - 1 - - if balance < 0: - # With less of base asset center price should be offset downward - calculated_center_price = calculated_center_price / math.sqrt(1 + spread * (balance * -1)) - elif balance > 0: - # With more of base asset center price will be offset upwards - calculated_center_price = calculated_center_price * math.sqrt(1 + spread * balance) - else: - calculated_center_price = calculated_center_price + calculated_center_price = self.calculate_asset_offset(calculated_center_price, order_ids, spread) # Calculate final_offset_price if manual center price offset is given if manual_offset: - calculated_center_price = calculated_center_price + (calculated_center_price * manual_offset) + calculated_center_price = self.calculate_manual_offset(calculated_center_price, manual_offset) + print('Calculated center price is {}'.format(calculated_center_price)) return calculated_center_price + def calculate_asset_offset(self, center_price, order_ids, spread): + """ Adds offset based on the asset balance of the worker to the center price + + :param float | center_price: Center price + :param list | order_ids: List of order ids that are used to calculate balance + :param float | spread: Spread percentage as float (eg. 0.01) + :return: Center price with asset offset + """ + total_balance = self.count_asset(order_ids) + total = (total_balance['quote'] * center_price) + total_balance['base'] + + if not total: # Prevent division by zero + balance = 0 + else: + # Returns a value between -1 and 1 + balance = (total_balance['base'] / total) * 2 - 1 + + if balance < 0: + # With less of base asset center price should be offset downward + center_price = center_price / math.sqrt(1 + spread * (balance * -1)) + elif balance > 0: + # With more of base asset center price will be offset upwards + center_price = center_price * math.sqrt(1 + spread * balance) + + return center_price + + @staticmethod + def calculate_manual_offset(center_price, manual_offset): + """ Adds manual offset to given center price + + :param float | center_price: + :param float | manual_offset: + :return: Center price with manual offset + """ + return center_price + (center_price * manual_offset) + def check_orders(self, *args, **kwargs): """ Tests if the orders need updating """ @@ -337,11 +360,11 @@ def check_orders(self, *args, **kwargs): # Check center price change when using market center price with reset option on change if self.is_reset_on_price_change and self.is_center_price_dynamic: + spread = self.spread + # Calculate spread if dynamic spread option in use, this calculation includes own orders on the market if self.dynamic_spread: spread = self.get_market_spread(quote_amount=self.market_depth_amount) * self.dynamic_spread_factor - else: - spread = self.spread center_price = self.calculate_center_price( None, From 3ed28409be70a6dc14568522670396dc67324969 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 24 Sep 2018 15:19:56 +0300 Subject: [PATCH 0794/1846] Remove old settings_window.ui --- dexbot/views/ui/global_settings_window.ui | 222 ---------------------- 1 file changed, 222 deletions(-) delete mode 100644 dexbot/views/ui/global_settings_window.ui diff --git a/dexbot/views/ui/global_settings_window.ui b/dexbot/views/ui/global_settings_window.ui deleted file mode 100644 index a1863146f..000000000 --- a/dexbot/views/ui/global_settings_window.ui +++ /dev/null @@ -1,222 +0,0 @@ - - - global_settings - - - - 0 - 0 - 600 - 400 - - - - - 0 - 0 - - - - - 600 - 400 - - - - - 600 - 600 - - - - TabWidget - - - 0 - - - - Nodes - - - - - 9 - 55 - 580 - 192 - - - - - 580 - 0 - - - - - 580 - 16777215 - - - - true - - - QAbstractItemView::DragDrop - - - false - - - true - - - false - - - false - - - - # - - - - - Node - - - - - 1 - - - wss://testnet.nodes.bitshares.ws - - - ItemIsSelectable|ItemIsEditable|ItemIsDragEnabled|ItemIsUserCheckable|ItemIsEnabled - - - - - 2 - - - wss://node.testnet.bitshares.eu - - - - - 3 - - - wss://testnet.dex.trading - - - - - 4 - - - wss://testnet.bitshares.apasia.tech/ws - - - - - - - 15 - 331 - 176 - 27 - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Save - - - false - - - - - - 150 - 10 - 436 - 38 - - - - - - - - 150 - 20 - - - - border: 0px; background-color: #3a5162; width: 100px; height: 20px; border-radius: 5px; color: #fff; - - - Move down - - - - - - - - 150 - 20 - - - - border: 0px; background-color: #3a5162; width: 100px; height: 20px; border-radius: 5px; color: #fff; - - - Move up - - - - - - - - 150 - 20 - - - - border: 0px; background-color: #623a3a; width: 100px; height: 20px; border-radius: 5px; color: #fff; - - - Remove - - - - - - - - 150 - 20 - - - - border: 0px; background-color: #3a623a; width: 100px; height: 20px; border-radius: 5px; color: #fff; - - - Add - - - - - - - - - - From 6e83b5902f6ccbe6c3a198ccf81ac3295767678d Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 24 Sep 2018 15:20:21 +0300 Subject: [PATCH 0795/1846] Add new settings_window.ui file --- dexbot/views/ui/settings_window.ui | 186 +++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 dexbot/views/ui/settings_window.ui diff --git a/dexbot/views/ui/settings_window.ui b/dexbot/views/ui/settings_window.ui new file mode 100644 index 000000000..e485f066a --- /dev/null +++ b/dexbot/views/ui/settings_window.ui @@ -0,0 +1,186 @@ + + + settings_dialog + + + + 0 + 0 + 626 + 386 + + + + Dialog + + + + true + + + + 9 + 9 + 602 + 321 + + + + 0 + + + + Nodes + + + + + + + + + + 150 + 20 + + + + PointingHandCursor + + + border: 0px; background-color: #3a5162; width: 100px; height: 20px; border-radius: 5px; color: #fff; + + + Move down + + + + + + + + 150 + 20 + + + + PointingHandCursor + + + border: 0px; background-color: #3a5162; width: 100px; height: 20px; border-radius: 5px; color: #fff; + + + Move up + + + + + + + + 150 + 20 + + + + PointingHandCursor + + + border: 0px; background-color: #623a3a; width: 100px; height: 20px; border-radius: 5px; color: #fff; + + + Remove + + + + + + + + 150 + 20 + + + + PointingHandCursor + + + border: 0px; background-color: #3a623a; width: 100px; height: 20px; border-radius: 5px; color: #fff; + + + Add + + + + + + + + + + + 580 + 0 + + + + + 580 + 16777215 + + + + true + + + QAbstractItemView::DragDrop + + + false + + + true + + + false + + + false + + + + # + + + + + Node + + + + + 1 + + + test + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Save + + + false + + + + + + + + + + From 1e9a8a4b325877cf8b199d27a0f4fba6456daa12 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 24 Sep 2018 15:20:41 +0300 Subject: [PATCH 0796/1846] Add SettingsView file --- dexbot/views/settings.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 dexbot/views/settings.py diff --git a/dexbot/views/settings.py b/dexbot/views/settings.py new file mode 100644 index 000000000..ca8183e38 --- /dev/null +++ b/dexbot/views/settings.py @@ -0,0 +1,17 @@ +from .ui.settings_window_ui import Ui_settings_dialog +from dexbot.controllers.settings_controller import SettingsController + +from PyQt5 import QtWidgets + + +class SettingsView(QtWidgets.QDialog, Ui_settings_dialog): + + def __init__(self): + super().__init__() + + # Initialize view controller + controller = SettingsController() + + self.setupUi(self) + + # Add items to list From 39e34814cca498f2e147ea7b32ff880cf2e9444a Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 24 Sep 2018 15:20:56 +0300 Subject: [PATCH 0797/1846] Add settings_controller file --- dexbot/controllers/settings_controller.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 dexbot/controllers/settings_controller.py diff --git a/dexbot/controllers/settings_controller.py b/dexbot/controllers/settings_controller.py new file mode 100644 index 000000000..5a7140e98 --- /dev/null +++ b/dexbot/controllers/settings_controller.py @@ -0,0 +1,14 @@ + + +class SettingsController: + + def __init__(self): + print('SettingsController loaded...') + + @property + def nodes(self): + """ Defines a list of nodes that are stored in the config file- + + :return: Nodes list + """ + pass From a29b448aa19989f43ced46f43d2c1bd066b0325b Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 24 Sep 2018 15:21:24 +0300 Subject: [PATCH 0798/1846] Add placeholder settings button to worker list --- dexbot/views/ui/worker_list_window.ui | 63 ++++++++++++++++++++++++++- dexbot/views/worker_list.py | 7 +++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/dexbot/views/ui/worker_list_window.ui b/dexbot/views/ui/worker_list_window.ui index c8e7a0d99..1d6259d00 100644 --- a/dexbot/views/ui/worker_list_window.ui +++ b/dexbot/views/ui/worker_list_window.ui @@ -114,8 +114,67 @@ QScrollBar { 100 - - + + + + + + 0 + 0 + + + + + 50 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + PointingHandCursor + + + border: 0px; background-color: #3A6257; width: 250px; height: 30px; border-radius: 10px; color: #ffffff; + + + settings + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index ac1dd8002..506d265c2 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -6,6 +6,7 @@ from dexbot.qt_queue.idle_queue import idle_add from .ui.worker_list_window_ui import Ui_MainWindow from .create_worker import CreateWorkerView +from .settings import SettingsView from .worker_item import WorkerItemWidget from .errors import gui_error from .layouts.flow_layout import FlowLayout @@ -32,6 +33,7 @@ def __init__(self, main_ctrl): self.layout = FlowLayout(self.scrollAreaContent) self.add_worker_button.clicked.connect(lambda: self.handle_add_worker()) + self.settings_button.clicked.connect(lambda: self.handle_open_settings()) # Load worker widgets from config file workers = self.config.workers_data @@ -92,6 +94,11 @@ def handle_add_worker(self): self.config.add_worker_config(worker_name, create_worker_dialog.worker_data) self.add_worker_widget(worker_name) + @gui_error + def handle_open_settings(self): + settings_dialog = SettingsView() + return_value = settings_dialog.exec_() + def set_worker_name(self, worker_name, value): self.worker_widgets[worker_name].set_worker_name(value) From 8af066f3634349e7baf80916e87086e32a9279fb Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 24 Sep 2018 15:21:38 +0300 Subject: [PATCH 0799/1846] Change comment in StrategyBase get_market_spread() --- dexbot/strategies/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index e7e7abf02..e4f897513 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -703,8 +703,7 @@ def get_market_sell_price(self, quote_amount=0, base_amount=00): return base_amount / quote_amount def get_market_spread(self, quote_amount=0, base_amount=0): - """ Returns the market spread %, including own orders, from specified depth, enhanced with moving average or - weighted moving average. + """ Returns the market spread %, including own orders, from specified depth. :param float | quote_amount: :param float | base_amount: From 4b4fdb42732bd6dced792cd2e654e809a50dff99 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 25 Sep 2018 20:08:00 +0500 Subject: [PATCH 0800/1846] Fix price calculation in get_market_xxx_price() Closes: #314 --- dexbot/strategies/base.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index e7e7abf02..cdfb3f8aa 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -611,21 +611,21 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0): for order in market_buy_orders: if base: # BASE amount was given - if base_amount < target_amount: + if order['base']['amount'] <= missing_amount: quote_amount += order['quote']['amount'] base_amount += order['base']['amount'] missing_amount -= order['base']['amount'] - elif base_amount > missing_amount: + else: base_amount += missing_amount quote_amount += missing_amount / order['price'] break elif not base: # QUOTE amount was given - if quote_amount < target_amount: + if order['quote']['amount'] <= missing_amount: quote_amount += order['quote']['amount'] base_amount += order['base']['amount'] missing_amount -= order['quote']['amount'] - elif quote_amount > missing_amount: + else: base_amount += missing_amount * order['price'] quote_amount += missing_amount break @@ -680,22 +680,21 @@ def get_market_sell_price(self, quote_amount=0, base_amount=00): for order in market_sell_orders: if quote: # QUOTE amount was given - if quote_amount < target_amount: + if order['quote']['amount'] <= missing_amount: quote_amount += order['quote']['amount'] base_amount += order['base']['amount'] missing_amount -= order['quote']['amount'] - elif quote_amount > missing_amount: + else: base_amount += missing_amount * order['price'] quote_amount += missing_amount break - elif not quote: # BASE amount was given - if base_amount < target_amount: + if order['base']['amount'] <= missing_amount: quote_amount += order['quote']['amount'] base_amount += order['base']['amount'] missing_amount -= order['base']['amount'] - elif base_amount > missing_amount: + else: base_amount += missing_amount quote_amount += missing_amount / order['price'] break From 10025a461e595ad284aa0d881ace2035688d82fa Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 26 Sep 2018 08:24:55 +0300 Subject: [PATCH 0801/1846] Change dexbot version number to 0.6.5 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 64508fe79..6a58189ea 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.6.4' +VERSION = '0.6.5' AUTHOR = 'Codaone Oy' __version__ = VERSION From d955c3cf46646c28f1ab8fce17fa6b2c0b4c9f38 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 26 Sep 2018 09:39:59 +0300 Subject: [PATCH 0802/1846] Fix formating error in logging --- dexbot/strategies/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 1dad34352..46ee86480 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -917,7 +917,7 @@ def place_market_buy_order(self, amount, price, return_none=False, *args, **kwar self.disabled = True return None - self.log.info('Placing a buy order for {} {} @ {;.8f}'.format(base_amount, symbol, price)) + self.log.info('Placing a buy order for {} {} @ {:.8f}'.format(base_amount, symbol, price)) # Place the order buy_transaction = self.retry_action( From 19d237156f5f05951ff5034b3fbee206ba748f68 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 26 Sep 2018 12:03:28 +0300 Subject: [PATCH 0803/1846] Change dexbot version number to 0.6.6 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 6a58189ea..649c51193 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.6.5' +VERSION = '0.6.6' AUTHOR = 'Codaone Oy' __version__ = VERSION From c11700edc38404204fb7a4d8c318b495bdf3ed1f Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 26 Sep 2018 13:24:06 +0300 Subject: [PATCH 0804/1846] Change dexbot version number to 0.6.7 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 649c51193..788e458f4 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.6.6' +VERSION = '0.6.7' AUTHOR = 'Codaone Oy' __version__ = VERSION From 326349d30070a56c7edcf6458cafcc42f532e7e7 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 28 Sep 2018 13:27:10 +0300 Subject: [PATCH 0805/1846] Change worker list layout and settings button --- dexbot/views/ui/worker_list_window.ui | 259 ++++++++++++++++++-------- 1 file changed, 178 insertions(+), 81 deletions(-) diff --git a/dexbot/views/ui/worker_list_window.ui b/dexbot/views/ui/worker_list_window.ui index 1d6259d00..aa9f29da4 100644 --- a/dexbot/views/ui/worker_list_window.ui +++ b/dexbot/views/ui/worker_list_window.ui @@ -37,6 +37,13 @@ QScrollBar { + + + + Qt::Horizontal + + + @@ -72,7 +79,7 @@ QScrollBar { 0 0 1029 - 336 + 358 @@ -90,15 +97,8 @@ QScrollBar { - - - - Qt::Horizontal - - - - + true @@ -114,74 +114,30 @@ QScrollBar { 100 - - - + + + - + 0 0 + + + 120 + 50 + + - 50 + 120 16777215 - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - PointingHandCursor - - - border: 0px; background-color: #3A6257; width: 250px; height: 30px; border-radius: 10px; color: #ffffff; - - - settings - - - + - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - - - - + 0 @@ -214,40 +170,181 @@ QScrollBar { - - - - - 0 - 0 - + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 250 + 80 + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + 20 + 60 + + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + + 250 + 25 + + + + PointingHandCursor + + + -1 + + + border: 0px; background-color: #3A6257; width: 250px; height: 20px; border-radius: 10px; color: #ffffff; + + + Add worker + + + + + + + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + Qt::RightToLeft + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + - 100 + 80 0 - 250 - 16777214 + 80 + 16777215 PointingHandCursor - - -1 - - border: 0px; background-color: #3A6257; width: 250px; height: 20px; border-radius: 10px; color: #ffffff; + border: 0px; background-color: #3A6257; width: 250px; height: 30px; border-radius: 10px; color: #ffffff; - Add worker + Settings + + + + Qt::Vertical + + + + 20 + 40 + + + + From 99c267998065dc985982cf8b93deda00e87a4b5e Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 28 Sep 2018 13:27:46 +0300 Subject: [PATCH 0806/1846] Change settings window --- dexbot/views/ui/settings_window.ui | 450 ++++++++++++++++++----------- 1 file changed, 282 insertions(+), 168 deletions(-) diff --git a/dexbot/views/ui/settings_window.ui b/dexbot/views/ui/settings_window.ui index e485f066a..5fe976dab 100644 --- a/dexbot/views/ui/settings_window.ui +++ b/dexbot/views/ui/settings_window.ui @@ -6,180 +6,294 @@ 0 0 - 626 - 386 + 690 + 360 + + + 0 + 0 + + + + + 690 + 360 + + - Dialog + DEXBot - Settings - - - true - - - - 9 - 9 - 602 - 321 - - - - 0 - - - - Nodes - - - - - - - - - - 150 - 20 - - - - PointingHandCursor - - - border: 0px; background-color: #3a5162; width: 100px; height: 20px; border-radius: 5px; color: #fff; - - - Move down - - - - - - - - 150 - 20 - - - - PointingHandCursor - - - border: 0px; background-color: #3a5162; width: 100px; height: 20px; border-radius: 5px; color: #fff; - - - Move up - - - - - - - - 150 - 20 - - - - PointingHandCursor - - - border: 0px; background-color: #623a3a; width: 100px; height: 20px; border-radius: 5px; color: #fff; - - - Remove - - - - - - - - 150 - 20 - - - - PointingHandCursor - - - border: 0px; background-color: #3a623a; width: 100px; height: 20px; border-radius: 5px; color: #fff; - - - Add - - - - - - - - - - - 580 - 0 - - - - - 580 - 16777215 - - - - true - - - QAbstractItemView::DragDrop - - - false - - - true - - - false - - - false - - - - # - - - - - Node - - + + + + + true + + + 0 + + + + Nodes + + + + + + + + + + 0 + 0 + + + + + 550 + 220 + + + + + 550 + 16777215 + + + + + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + + + false + + + QAbstractItemView::NoDragDrop + + + false + + + true + + + false + + + false + + + + Node + + + + + + + + + 0 + 100 + + + + + 16777215 + 220 + + + + + + + + 60 + 20 + + + + PointingHandCursor + + + border: 0px; background-color: #3a623a; width: 100px; height: 20px; border-radius: 5px; color: #fff; + + + Add + + + + + + + + 60 + 20 + + + + PointingHandCursor + + + border: 0px; background-color: #623a3a; width: 100px; height: 20px; border-radius: 5px; color: #fff; + + + Delete + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 60 + 20 + + + + PointingHandCursor + + + border: 0px; background-color: #3a5162; width: 100px; height: 20px; border-radius: 5px; color: #fff; + + + Up + + + + + + + + 60 + 20 + + + + PointingHandCursor + + + border: 0px; background-color: #3a5162; width: 100px; height: 20px; border-radius: 5px; color: #fff; + + + Down + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 60 + 20 + + + + border: 0px; background-color: #3a5162; width: 100px; height: 20px; border-radius: 5px; color: #fff; + + + Defaults + + + + + + + + + - - 1 - - - test - + + + + + + + 40 + 16777215 + + + + Qt::LeftToRight + + + + + + Status: + + + + + + + + + + No changes detected + + + + + + + + 250 + 0 + + + + + 250 + 16777215 + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Save + + + false + + + + + - - - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Save - - - false - - - - - - + + + + + From fac65c7ed92aec57c6524c5b4de148e1d104d7ad Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 28 Sep 2018 13:29:10 +0300 Subject: [PATCH 0807/1846] Add buttons to SettingsView --- dexbot/views/settings.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/dexbot/views/settings.py b/dexbot/views/settings.py index ca8183e38..14673e915 100644 --- a/dexbot/views/settings.py +++ b/dexbot/views/settings.py @@ -1,5 +1,5 @@ -from .ui.settings_window_ui import Ui_settings_dialog from dexbot.controllers.settings_controller import SettingsController +from dexbot.views.ui.settings_window_ui import Ui_settings_dialog from PyQt5 import QtWidgets @@ -10,8 +10,23 @@ def __init__(self): super().__init__() # Initialize view controller - controller = SettingsController() + self.controller = SettingsController(self) self.setupUi(self) - # Add items to list + # Initialize list of nodes + self.controller.initialize_node_list() + + # Since we are using "parents" for listing the nodes, they are actually "children" for the root item + self.root_item = self.nodes_tree_widget.invisibleRootItem() + + # List controls + self.add_button.clicked.connect(self.controller.add_node) + self.remove_button.clicked.connect(self.controller.remove_node) + self.move_up_button.clicked.connect(self.controller.move_up) + self.move_down_button.clicked.connect(self.controller.move_down) + self.restore_defaults_button.clicked.connect(self.controller.restore_defaults) + + # Tab controls + self.button_box.rejected.connect(self.reject) + self.button_box.accepted.connect(self.controller.save_settings) From c10f995d460cf61d062462b81e38368cd34fcdff Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 28 Sep 2018 13:29:59 +0300 Subject: [PATCH 0808/1846] Add functionality to settings_controller.py --- dexbot/controllers/settings_controller.py | 105 +++++++++++++++++++++- 1 file changed, 101 insertions(+), 4 deletions(-) diff --git a/dexbot/controllers/settings_controller.py b/dexbot/controllers/settings_controller.py index 5a7140e98..48c1fa876 100644 --- a/dexbot/controllers/settings_controller.py +++ b/dexbot/controllers/settings_controller.py @@ -1,14 +1,111 @@ +from dexbot.config import Config + +from PyQt5.QtWidgets import QTreeWidgetItem +from PyQt5.QtCore import Qt class SettingsController: - def __init__(self): - print('SettingsController loaded...') + def __init__(self, view): + self.config = Config() + self.view = view + + def add_node(self): + item = QTreeWidgetItem(self.view.nodes_tree_widget) + item.setText(0, '') + item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) + + # Scroll to the new item and activate editing + self.view.nodes_tree_widget.scrollToItem(item) + self.view.nodes_tree_widget.editItem(item) + + self.view.notification_label.setText('Unsaved changes detected; Node added.') + + def move_up(self): + node_item = self.view.nodes_tree_widget.currentItem() + current_index = self.view.nodes_tree_widget.indexOfTopLevelItem(node_item) + + # This prevents moving item out of the list + if current_index > 0: + item = self.view.nodes_tree_widget.takeTopLevelItem(current_index) + self.view.root_item.insertChild(current_index - 1, item) + self.view.notification_label.setText('Unsaved changes detected; List order has changed.') + + def move_down(self): + node_item = self.view.nodes_tree_widget.currentItem() + current_index = self.view.nodes_tree_widget.indexOfTopLevelItem(node_item) + + # This prevents moving item out of the list + if current_index < (self.view.root_item.childCount() - 1): + item = self.view.nodes_tree_widget.takeTopLevelItem(current_index) + self.view.root_item.insertChild(current_index + 1, item) + item.setSelected() + self.view.notification_label.setText('Unsaved changes detected; List order has changed.') + + def save_settings(self): + nodes = [] + + child_count = self.view.root_item.childCount() + + for index in range(child_count): + nodes.append(self.view.root_item.child(index).text(0)) + + # Send the nodes to controller to handle the save + self.save_nodes_to_config(nodes) + self.initialize_node_list() + + def remove_node(self): + node = self.view.nodes_tree_widget.currentItem() + + if node: + # Delete only if node selected, + index = self.view.nodes_tree_widget.indexOfTopLevelItem(node) + self.view.nodes_tree_widget.takeTopLevelItem(index) + self.view.notification_label.setText('Unsaved changes detected; Node removed.') + + def initialize_node_list(self, nodes=None): + """ Populates Tree Widget with nodes + + :param nodes: List of nodes that can be applied to the widget instead of getting them from the config file. + """ + # Make sure there are no widgets in the list + self.view.nodes_tree_widget.clear() + + # Get nodes from the config file + if nodes is None: + nodes = self.view.controller.nodes + + # Add nodes to the widget list + for node in nodes: + item = QTreeWidgetItem(self.view.nodes_tree_widget) + item.setText(0, node) + item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) + + def save_nodes_to_config(self, nodes): + """ Save nodes to the config file + """ + # Remove empty nodes before saving, this is just to make sure no empty strings end up in config file + nodes = self.remove_empty_items(nodes) + + self.config['node'] = nodes + self.config.save_config() + # Update status + self.view.notification_label.setText('Settings successfully saved!') + + def restore_defaults(self): + self.initialize_node_list(nodes=self.config.node_list) + self.view.notification_label.setText('Restored default nodes. Remember to save changes!') + + @staticmethod + def remove_empty_items(items_list): + """ Removes empty strings from a list + """ + return list(filter(None, items_list)) @property def nodes(self): - """ Defines a list of nodes that are stored in the config file- + """ Returns nodes list from the config file :return: Nodes list """ - pass + return self.config.get('node') From c5a316341f2b3e66603a1fc10deeb26ee7a9a807 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 28 Sep 2018 14:17:30 +0300 Subject: [PATCH 0809/1846] Change save and cancel buttons for dialog instead --- dexbot/views/settings.py | 2 +- dexbot/views/ui/settings_window.ui | 44 +++++++++++++++--------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/dexbot/views/settings.py b/dexbot/views/settings.py index 14673e915..47e124dc0 100644 --- a/dexbot/views/settings.py +++ b/dexbot/views/settings.py @@ -27,6 +27,6 @@ def __init__(self): self.move_down_button.clicked.connect(self.controller.move_down) self.restore_defaults_button.clicked.connect(self.controller.restore_defaults) - # Tab controls + # Dialog controls self.button_box.rejected.connect(self.reject) self.button_box.accepted.connect(self.controller.save_settings) diff --git a/dexbot/views/ui/settings_window.ui b/dexbot/views/ui/settings_window.ui index 5fe976dab..d426b45bf 100644 --- a/dexbot/views/ui/settings_window.ui +++ b/dexbot/views/ui/settings_window.ui @@ -264,28 +264,6 @@ - - - - - 250 - 0 - - - - - 250 - 16777215 - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Save - - - false - - - @@ -293,6 +271,28 @@ + + + + + 250 + 0 + + + + + 16777215 + 16777215 + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Save + + + false + + + From 7a18d57c2202c1b802393e7f6309876fd3af3c66 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 28 Sep 2018 15:06:36 +0300 Subject: [PATCH 0810/1846] Fix crash when moving node --- dexbot/controllers/settings_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/controllers/settings_controller.py b/dexbot/controllers/settings_controller.py index 48c1fa876..194ecadee 100644 --- a/dexbot/controllers/settings_controller.py +++ b/dexbot/controllers/settings_controller.py @@ -39,7 +39,7 @@ def move_down(self): if current_index < (self.view.root_item.childCount() - 1): item = self.view.nodes_tree_widget.takeTopLevelItem(current_index) self.view.root_item.insertChild(current_index + 1, item) - item.setSelected() + self.view.notification_label.setText('Unsaved changes detected; List order has changed.') def save_settings(self): From a9119d5d266a0ca6b7cd77958d6701a1ed7a8d2b Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 28 Sep 2018 15:18:43 +0300 Subject: [PATCH 0811/1846] Change dexbot version number to 0.6.8 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 788e458f4..322fd6b01 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.6.7' +VERSION = '0.6.8' AUTHOR = 'Codaone Oy' __version__ = VERSION From b1a5c346abb191a894a0eeb068315b8645e3dd30 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Sat, 29 Sep 2018 17:57:32 +0300 Subject: [PATCH 0812/1846] two small fixes --- dexbot/strategies/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index f972f7423..94060594e 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -506,7 +506,7 @@ def get_highest_market_buy_order(self, orders=None): return order - def get_highest_own_buy(self, orders=None): + def get_highest_own_buy_order(self, orders=None): """ Returns highest own buy order. :param list | orders: @@ -818,7 +818,7 @@ def get_own_spread(self): """ try: # Try fetching own orders - highest_own_buy_price = self.get_highest_market_buy_order().get('price') + highest_own_buy_price = self.get_highest_own_buy_order().get('price') lowest_own_sell_price = self.get_lowest_own_sell_order().get('price') except AttributeError: return None From 6e85bd6f7e62cb09c07a2eab1bb5d66157c753ed Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 1 Oct 2018 08:35:42 +0300 Subject: [PATCH 0813/1846] Change dexbot version number to 0.6.9 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 322fd6b01..6b6b55490 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.6.8' +VERSION = '0.6.9' AUTHOR = 'Codaone Oy' __version__ = VERSION From 693f267b459265e3720935193f07e04aac4e73c5 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 1 Oct 2018 10:38:55 +0300 Subject: [PATCH 0814/1846] Remove debug printing --- 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 026fde627..f2b36d1f2 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -278,7 +278,6 @@ def calculate_center_price(self, center_price=None, asset_offset=False, spread=N if manual_offset: calculated_center_price = self.calculate_manual_offset(calculated_center_price, manual_offset) - print('Calculated center price is {}'.format(calculated_center_price)) return calculated_center_price def calculate_asset_offset(self, center_price, order_ids, spread): From 3cb4a038672ebc3a45c806739630dcf7bedba7e6 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 1 Oct 2018 10:50:46 +0300 Subject: [PATCH 0815/1846] Change dexbot version number to 0.6.10 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 64508fe79..45ef9ab70 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.6.4' +VERSION = '0.6.10' AUTHOR = 'Codaone Oy' __version__ = VERSION From 4b1095feae57dcf875920f9c83b823238f595be7 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 1 Oct 2018 12:34:07 +0300 Subject: [PATCH 0816/1846] Update default nodes list --- dexbot/config.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/dexbot/config.py b/dexbot/config.py index 1beda59fb..b5ac113d2 100644 --- a/dexbot/config.py +++ b/dexbot/config.py @@ -169,7 +169,6 @@ def construct_mapping(mapping_loader, node): def node_list(self): """ A pre-defined list of Bitshares nodes. """ return [ - "wss://status200.bitshares.apasia.tech/ws", "wss://eu.openledger.info/ws", "wss://bitshares.openledger.info/ws", "wss://dexnode.net/ws", @@ -184,5 +183,57 @@ def node_list(self): "wss://bitshares.dacplay.org/ws", "wss://bit.btsabc.org/ws", "wss://bts.ai.la/ws", - "wss://ws.gdex.top" + "wss://ws.gdex.top", + "wss://na.openledger.info/ws", + "wss://node.btscharts.com/ws", + "wss://status200.bitshares.apasia.tech/ws", + "wss://new-york.bitshares.apasia.tech/ws", + "wss://dallas.bitshares.apasia.tech/ws", + "wss://chicago.bitshares.apasia.tech/ws", + "wss://atlanta.bitshares.apasia.tech/ws", + "wss://us-la.bitshares.apasia.tech/ws", + "wss://seattle.bitshares.apasia.tech/ws", + "wss://miami.bitshares.apasia.tech/ws", + "wss://valley.bitshares.apasia.tech/ws", + "wss://canada6.daostreet.com", + "wss://bitshares.nu/ws", + "wss://api.open-asset.tech/ws", + "wss://france.bitshares.apasia.tech/ws", + "wss://england.bitshares.apasia.tech/ws", + "wss://netherlands.bitshares.apasia.tech/ws", + "wss://australia.bitshares.apasia.tech/ws", + "wss://dex.rnglab.org", + "wss://la.dexnode.net/ws", + "wss://api-ru.bts.blckchnd.com", + "wss://node.market.rudex.org", + "wss://api.bitsharesdex.com", + "wss://api.fr.bitsharesdex.com", + "wss://blockzms.xyz/ws", + "wss://eu.nodes.bitshares.ws", + "wss://us.nodes.bitshares.ws", + "wss://sg.nodes.bitshares.ws", + "wss://ws.winex.pro", + "wss://api.bts.mobi/ws", + "wss://api.btsxchng.com", + "wss://api.bts.network/", + "wss://btsws.roelandp.nl/ws", + "wss://api.bitshares.bhuz.info/ws", + "wss://bts-api.lafona.net/ws", + "wss://kimziv.com/ws", + "wss://api.btsgo.net/ws", + "wss://bts.proxyhosts.info/wss", + "wss://bts.open.icowallet.net/ws", + "wss://de.bts.dcn.cx/ws", + "wss://fi.bts.dcn.cx/ws", + "wss://crazybit.online", + "wss://freedom.bts123.cc:15138/", + "wss://bitshares.bts123.cc:15138/", + "wss://api.bts.ai", + "wss://ws.hellobts.com", + "wss://bitshares.cyberit.io", + "wss://bts-seoul.clockwork.gr", + "wss://bts.liuye.tech:4443/ws", + "wss://btsfullnode.bangzi.info/ws", + "wss://api.dex.trading/", + "wss://citadel.li/node" ] From 1338ad894ed027ba95c2e73c66e293d9edb43c4d Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 1 Oct 2018 13:42:20 +0300 Subject: [PATCH 0817/1846] Fix bug where selection was lost when moving item --- dexbot/controllers/settings_controller.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/dexbot/controllers/settings_controller.py b/dexbot/controllers/settings_controller.py index 194ecadee..98cdfe9bc 100644 --- a/dexbot/controllers/settings_controller.py +++ b/dexbot/controllers/settings_controller.py @@ -22,24 +22,37 @@ def add_node(self): self.view.notification_label.setText('Unsaved changes detected; Node added.') def move_up(self): - node_item = self.view.nodes_tree_widget.currentItem() - current_index = self.view.nodes_tree_widget.indexOfTopLevelItem(node_item) + """ Move item up in the widget tree list + """ + current_index = self.view.nodes_tree_widget.indexOfTopLevelItem(self.view.nodes_tree_widget.currentItem()) # This prevents moving item out of the list if current_index > 0: + # Take the item out of the widget list item = self.view.nodes_tree_widget.takeTopLevelItem(current_index) + + # Put item back to the list in new position self.view.root_item.insertChild(current_index - 1, item) + + # Keep moved item selected + self.view.nodes_tree_widget.setCurrentItem(item) self.view.notification_label.setText('Unsaved changes detected; List order has changed.') def move_down(self): - node_item = self.view.nodes_tree_widget.currentItem() - current_index = self.view.nodes_tree_widget.indexOfTopLevelItem(node_item) + """ Move item down in the widget tree list + """ + current_index = self.view.nodes_tree_widget.indexOfTopLevelItem(self.view.nodes_tree_widget.currentItem()) # This prevents moving item out of the list if current_index < (self.view.root_item.childCount() - 1): + # Take the item out of the widget list item = self.view.nodes_tree_widget.takeTopLevelItem(current_index) + + # Put item back to the list in new position self.view.root_item.insertChild(current_index + 1, item) + # Keep moved item selected + self.view.nodes_tree_widget.setCurrentItem(item) self.view.notification_label.setText('Unsaved changes detected; List order has changed.') def save_settings(self): From 0a54fb0574be53122e73b489a2d206f0972f24c8 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 1 Oct 2018 13:42:50 +0300 Subject: [PATCH 0818/1846] Add comments to some functions --- dexbot/controllers/settings_controller.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dexbot/controllers/settings_controller.py b/dexbot/controllers/settings_controller.py index 98cdfe9bc..0666cb4cc 100644 --- a/dexbot/controllers/settings_controller.py +++ b/dexbot/controllers/settings_controller.py @@ -11,6 +11,8 @@ def __init__(self, view): self.view = view def add_node(self): + """ Add item in the widget tree list + """ item = QTreeWidgetItem(self.view.nodes_tree_widget) item.setText(0, '') item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) @@ -56,6 +58,8 @@ def move_down(self): self.view.notification_label.setText('Unsaved changes detected; List order has changed.') def save_settings(self): + """ Save items in the tree widget list into the config file and reload the items + """ nodes = [] child_count = self.view.root_item.childCount() @@ -68,6 +72,8 @@ def save_settings(self): self.initialize_node_list() def remove_node(self): + """ Remove item from the widget tree list + """ node = self.view.nodes_tree_widget.currentItem() if node: From 617033faddfddba75e1e16de5ca046568d613a31 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 1 Oct 2018 13:43:20 +0300 Subject: [PATCH 0819/1846] Refactor remove_empty_items parameter name --- dexbot/controllers/settings_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/controllers/settings_controller.py b/dexbot/controllers/settings_controller.py index 0666cb4cc..374419afd 100644 --- a/dexbot/controllers/settings_controller.py +++ b/dexbot/controllers/settings_controller.py @@ -116,10 +116,10 @@ def restore_defaults(self): self.view.notification_label.setText('Restored default nodes. Remember to save changes!') @staticmethod - def remove_empty_items(items_list): + def remove_empty_items(items): """ Removes empty strings from a list """ - return list(filter(None, items_list)) + return list(filter(None, items)) @property def nodes(self): From 8e2bbb9aab3862949e78a9a6cfb3d26ba21814bb Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 1 Oct 2018 13:48:43 +0300 Subject: [PATCH 0820/1846] Change dexbot version number to 0.6.11 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 45ef9ab70..67abefe4b 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.6.10' +VERSION = '0.6.11' AUTHOR = 'Codaone Oy' __version__ = VERSION From c7de06ed0be2c18b0617d1d5038ce51d033dc05c Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 1 Oct 2018 13:55:51 +0300 Subject: [PATCH 0821/1846] Change dexbot version number to 0.7.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 67abefe4b..391f9735d 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.6.11' +VERSION = '0.7.0' AUTHOR = 'Codaone Oy' __version__ = VERSION From c97fa545258c070123bca78562cb3a880402b921 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 1 Oct 2018 15:02:06 +0300 Subject: [PATCH 0822/1846] Add websocket-client version 0.48.0 to setup.py --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1e06be4c2..f02964afd 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,8 @@ 'ruamel.yaml>=0.15.37', 'sdnotify', 'appdirs>=1.4.3', - 'pycryptodomex==3.6.4' + 'pycryptodomex==3.6.4', + 'websocket-client==0.48.0' ] From 015803b701d1474d3f56e8c7a5825ee5b1a0b36f Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 1 Oct 2018 15:21:08 +0300 Subject: [PATCH 0823/1846] Change dexbot version number to 0.7.1 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 391f9735d..508996598 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.0' +VERSION = '0.7.1' AUTHOR = 'Codaone Oy' __version__ = VERSION From ec0730eb674cdde650f287b532d29b13b76a589e Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 2 Oct 2018 12:13:27 +0300 Subject: [PATCH 0824/1846] Add "Help" button which opens GitHub documentation --- dexbot/views/ui/worker_list_window.ui | 40 +++++++++++++++++++++++---- dexbot/views/worker_list.py | 6 ++++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/dexbot/views/ui/worker_list_window.ui b/dexbot/views/ui/worker_list_window.ui index aa9f29da4..b93d0b20e 100644 --- a/dexbot/views/ui/worker_list_window.ui +++ b/dexbot/views/ui/worker_list_window.ui @@ -79,7 +79,7 @@ QScrollBar { 0 0 1029 - 358 + 340 @@ -279,13 +279,13 @@ QScrollBar { 120 - 0 + 100 120 - 16777215 + 100 @@ -318,7 +318,7 @@ QScrollBar { 80 - 16777215 + 25 @@ -333,14 +333,42 @@ QScrollBar { - + + + + 80 + 0 + + + + + 80 + 25 + + + + PointingHandCursor + + + border: 0px; background-color: #3A6257; width: 250px; height: 30px; border-radius: 10px; color: #ffffff; + + + Help + + + + + Qt::Vertical + + QSizePolicy::Fixed + 20 - 40 + 25 diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 506d265c2..8b606facc 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -1,5 +1,6 @@ import time from threading import Thread +import webbrowser from dexbot import __version__ from dexbot.qt_queue.queue_dispatcher import ThreadDispatcher @@ -34,6 +35,7 @@ def __init__(self, main_ctrl): self.add_worker_button.clicked.connect(lambda: self.handle_add_worker()) self.settings_button.clicked.connect(lambda: self.handle_open_settings()) + self.help_button.clicked.connect(lambda: self.handle_open_documentation()) # Load worker widgets from config file workers = self.config.workers_data @@ -99,6 +101,10 @@ def handle_open_settings(self): settings_dialog = SettingsView() return_value = settings_dialog.exec_() + @staticmethod + def handle_open_documentation(): + webbrowser.open('https://github.com/Codaone/DEXBot/wiki') + def set_worker_name(self, worker_name, value): self.worker_widgets[worker_name].set_worker_name(value) From 264155f2069a9d7c47eaf4a4e0af5d5aa443f46f Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 3 Oct 2018 15:08:43 +0300 Subject: [PATCH 0825/1846] Add info icon --- dexbot/resources/img/info.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 dexbot/resources/img/info.svg diff --git a/dexbot/resources/img/info.svg b/dexbot/resources/img/info.svg new file mode 100644 index 000000000..f7c3197e9 --- /dev/null +++ b/dexbot/resources/img/info.svg @@ -0,0 +1 @@ + From 61358d9d13829923ed18adf9c17171da17c5c7c2 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 3 Oct 2018 15:09:32 +0300 Subject: [PATCH 0826/1846] Add info button to the worker item widget --- dexbot/resources/icons.qrc | 1 + dexbot/views/ui/worker_item_widget.ui | 93 ++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/dexbot/resources/icons.qrc b/dexbot/resources/icons.qrc index effdadc5b..3a3d97c6a 100644 --- a/dexbot/resources/icons.qrc +++ b/dexbot/resources/icons.qrc @@ -1,5 +1,6 @@ + img/info.svg svg/modifystrategy.svg svg/simplestrategy.svg diff --git a/dexbot/views/ui/worker_item_widget.ui b/dexbot/views/ui/worker_item_widget.ui index b06470df5..310076e76 100644 --- a/dexbot/views/ui/worker_item_widget.ui +++ b/dexbot/views/ui/worker_item_widget.ui @@ -478,6 +478,97 @@ margin: 0px; + + + + + 0 + 60 + + + + + Source Sans Pro + 75 + true + + + + + 0 + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + 0 + 24 + + + + + Source Sans Pro + 75 + true + + + + PointingHandCursor + + + color: #ffffff + + + + :/worker_widget/img/info.svg:/worker_widget/img/info.svg + + + + 30 + 30 + + + + true + + + + + + + + Source Sans Pro + 6 + 75 + true + + + + + + + DETAILS + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + @@ -509,7 +600,7 @@ margin: 0px; 5 - + From 6d8aec33262aa8f54e1370f88b5ef98c3565a8d2 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 3 Oct 2018 15:10:25 +0300 Subject: [PATCH 0827/1846] Add worker details dialog and basic functionality --- .../controllers/worker_details_controller.py | 27 ++ dexbot/views/ui/worker_details_window.ui | 252 ++++++++++++++++++ dexbot/views/worker_details.py | 26 ++ dexbot/views/worker_item.py | 6 + 4 files changed, 311 insertions(+) create mode 100644 dexbot/controllers/worker_details_controller.py create mode 100644 dexbot/views/ui/worker_details_window.ui create mode 100644 dexbot/views/worker_details.py diff --git a/dexbot/controllers/worker_details_controller.py b/dexbot/controllers/worker_details_controller.py new file mode 100644 index 000000000..3cf453a8b --- /dev/null +++ b/dexbot/controllers/worker_details_controller.py @@ -0,0 +1,27 @@ + + +class WorkerDetailsController: + + def __init__(self, view, worker_name, config): + """ Initializes controller + + :param view: WorkerDetailsView + :param worker_name: Worker's name + :param config: Worker's config + """ + self.view = view + self.worker_name = worker_name + self.config = config['workers'].get(self.worker_name) + + def initialize_worker_data(self): + """ Initializes details view with worker's data + + """ + # Worker information + self.view.worker_name.setText(self.worker_name) + self.view.worker_account.setText(self.config.get('account')) + + # Common strategy information + self.view.strategy_name.setText(self.config.get('module')) + self.view.market.setText(self.config.get('market')) + self.view.fee_asset.setText(self.config.get('fee_asset')) diff --git a/dexbot/views/ui/worker_details_window.ui b/dexbot/views/ui/worker_details_window.ui new file mode 100644 index 000000000..c337e0104 --- /dev/null +++ b/dexbot/views/ui/worker_details_window.ui @@ -0,0 +1,252 @@ + + + details_dialog + + + + 0 + 0 + 660 + 460 + + + + + 0 + 0 + + + + + 660 + 460 + + + + DEXBot - Worker details + + + + + + + 0 + 35 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 35 + + + + QDialogButtonBox::Close + + + + + + + + + + true + + + + + + QTabWidget::North + + + QTabWidget::Rounded + + + 0 + + + true + + + false + + + + + 0 + 0 + + + + + 600 + 350 + + + + + true + + + + + + + Worker + + + + + + + + + Basic information + + + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + Worker name + + + + + + + + + + + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + Worker account + + + + + + + + + + + + + + + + + Strategy parameters + + + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + Strategy + + + + + + + + + + + + + + Market + + + + + + + + + + + + + + Fee asset + + + + + + + + + + + + + + + + + + + + + + diff --git a/dexbot/views/worker_details.py b/dexbot/views/worker_details.py new file mode 100644 index 000000000..b2215b3e5 --- /dev/null +++ b/dexbot/views/worker_details.py @@ -0,0 +1,26 @@ +from dexbot.controllers.worker_details_controller import WorkerDetailsController +from dexbot.views.ui.worker_details_window_ui import Ui_details_dialog + +from PyQt5 import QtWidgets + + +class WorkerDetailsView(QtWidgets.QDialog, Ui_details_dialog): + + def __init__(self, worker_name, config): + super().__init__() + + self.config = config + + # Initialize view controller + self.controller = WorkerDetailsController(self, worker_name, self.config) + + self.setupUi(self) + + # Add worker's name to the dialog title + self.setWindowTitle("DEXBot - {} details".format(worker_name)) + + # Initialize other data to the dialog + self.controller.initialize_worker_data() + + # Dialog controls + self.button_box.rejected.connect(self.reject) diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 84d0fae6c..217aa0218 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -2,6 +2,7 @@ from .ui.worker_item_widget_ui import Ui_widget from .confirmation import ConfirmationDialog +from .worker_details import WorkerDetailsView from .edit_worker import EditWorkerView from dexbot.storage import db_worker from dexbot.controllers.worker_controller import WorkerController @@ -24,6 +25,7 @@ def __init__(self, worker_name, config, main_ctrl, view): self.setupUi(self) self.edit_button.clicked.connect(lambda: self.handle_edit_worker()) + self.details_button.clicked.connect(lambda: self.handle_open_details()) self.toggle.mouseReleaseEvent = lambda _: self.toggle_worker() self.onoff.mouseReleaseEvent = lambda _: self.toggle_worker() @@ -151,6 +153,10 @@ def reload_widget(self, worker_name): self.setup_ui_data(self.worker_config) self._pause_worker() + def handle_open_details(self): + details_dialog = WorkerDetailsView(self.worker_name, self.worker_config) + details_dialog.exec_() + @gui_error def handle_edit_worker(self): edit_worker_dialog = EditWorkerView(self, self.main_ctrl.bitshares_instance, From 119be4a05555b73cb02d3b6f957198283b1bb875 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 4 Oct 2018 09:22:02 +0300 Subject: [PATCH 0828/1846] Fix settings window scaling vertically --- dexbot/views/ui/settings_window.ui | 73 +++++++++++++----------------- 1 file changed, 31 insertions(+), 42 deletions(-) diff --git a/dexbot/views/ui/settings_window.ui b/dexbot/views/ui/settings_window.ui index d426b45bf..f56deb0a6 100644 --- a/dexbot/views/ui/settings_window.ui +++ b/dexbot/views/ui/settings_window.ui @@ -7,11 +7,11 @@ 0 0 690 - 360 + 400 - + 0 0 @@ -19,7 +19,7 @@ 690 - 360 + 400 @@ -45,23 +45,11 @@ - + 0 0 - - - 550 - 220 - - - - - 550 - 16777215 - - @@ -95,18 +83,6 @@ - - - 0 - 100 - - - - - 16777215 - 220 - - @@ -151,10 +127,13 @@ Qt::Vertical + + QSizePolicy::Fixed + 20 - 40 + 20 @@ -202,10 +181,13 @@ Qt::Vertical + + QSizePolicy::Fixed + 20 - 40 + 20 @@ -213,6 +195,12 @@ + + 0 + 0 + + + 60 20 @@ -226,6 +214,19 @@ + + + + Qt::Vertical + + + + 0 + 0 + + + + @@ -273,18 +274,6 @@ - - - 250 - 0 - - - - - 16777215 - 16777215 - - QDialogButtonBox::Cancel|QDialogButtonBox::Save From 1ed2e049bf5474b7c93ff5de4a2350f6a0cb359e Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 5 Oct 2018 15:29:27 +0300 Subject: [PATCH 0829/1846] Add three new widgets for details view --- dexbot/views/ui/tabs/__init__.py | 0 dexbot/views/ui/tabs/graph_tab.ui | 37 +++++++++++++++ dexbot/views/ui/tabs/table_tab.ui | 79 +++++++++++++++++++++++++++++++ dexbot/views/ui/tabs/text_tab.ui | 33 +++++++++++++ 4 files changed, 149 insertions(+) create mode 100644 dexbot/views/ui/tabs/__init__.py create mode 100644 dexbot/views/ui/tabs/graph_tab.ui create mode 100644 dexbot/views/ui/tabs/table_tab.ui create mode 100644 dexbot/views/ui/tabs/text_tab.ui diff --git a/dexbot/views/ui/tabs/__init__.py b/dexbot/views/ui/tabs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dexbot/views/ui/tabs/graph_tab.ui b/dexbot/views/ui/tabs/graph_tab.ui new file mode 100644 index 000000000..cf157e2c9 --- /dev/null +++ b/dexbot/views/ui/tabs/graph_tab.ui @@ -0,0 +1,37 @@ + + + Graph_Tab + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dexbot/views/ui/tabs/table_tab.ui b/dexbot/views/ui/tabs/table_tab.ui new file mode 100644 index 000000000..9d50242c9 --- /dev/null +++ b/dexbot/views/ui/tabs/table_tab.ui @@ -0,0 +1,79 @@ + + + Table_Tab + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + + + + + + + QAbstractItemView::NoEditTriggers + + + true + + + Qt::SolidLine + + + true + + + 20 + + + 4 + + + 120 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dexbot/views/ui/tabs/text_tab.ui b/dexbot/views/ui/tabs/text_tab.ui new file mode 100644 index 000000000..69b3f44d8 --- /dev/null +++ b/dexbot/views/ui/tabs/text_tab.ui @@ -0,0 +1,33 @@ + + + Text_Tab + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + + + + + + + + + + + + + + From 5baf05ba3eef4494f1bdc78ec5637b23fb34e9eb Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 5 Oct 2018 15:30:34 +0300 Subject: [PATCH 0830/1846] Change how config is passed into DetailsController --- dexbot/controllers/worker_details_controller.py | 2 +- dexbot/views/worker_details.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/controllers/worker_details_controller.py b/dexbot/controllers/worker_details_controller.py index 3cf453a8b..4089ace03 100644 --- a/dexbot/controllers/worker_details_controller.py +++ b/dexbot/controllers/worker_details_controller.py @@ -11,7 +11,7 @@ def __init__(self, view, worker_name, config): """ self.view = view self.worker_name = worker_name - self.config = config['workers'].get(self.worker_name) + self.config = config def initialize_worker_data(self): """ Initializes details view with worker's data diff --git a/dexbot/views/worker_details.py b/dexbot/views/worker_details.py index b2215b3e5..b4999a9c9 100644 --- a/dexbot/views/worker_details.py +++ b/dexbot/views/worker_details.py @@ -9,7 +9,7 @@ class WorkerDetailsView(QtWidgets.QDialog, Ui_details_dialog): def __init__(self, worker_name, config): super().__init__() - self.config = config + self.config = config['workers'].get(worker_name) # Initialize view controller self.controller = WorkerDetailsController(self, worker_name, self.config) From dc147d9ba0b8e868f84a9ab7ad6780f5298a6b2d Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 5 Oct 2018 15:31:44 +0300 Subject: [PATCH 0831/1846] Add configure_details to strategies --- dexbot/strategies/base.py | 33 +++++++++++++++++++++++++++ dexbot/strategies/relative_orders.py | 9 +++++++- dexbot/strategies/staggered_orders.py | 7 ++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 94060594e..5d3df0cd7 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -44,6 +44,16 @@ """ ConfigElement = collections.namedtuple('ConfigElement', 'key type default title description extra') +""" Strategies have different needs for the details they want to show for the user. These elements help to build a + custom details window for the strategy. + + Tuple fields as follows: + - Type: 'graph', 'text', 'table' + - Name: The name of the tab, shows at the top + - Title: The title is shown inside the tab +""" +DetailElement = collections.namedtuple('DetailTab', 'type name title') + class StrategyBase(BaseStrategy, Storage, StateMachine, Events): """ A strategy based on this class is intended to work in one market. This class contains @@ -127,6 +137,29 @@ def configure(cls, return_base_config=True): return base_config return [] + @classmethod + def configure_details(cls, include_default_tabs=True): + """ Return a list of ConfigElement objects defining the configuration values for this class. + + User interfaces should then generate widgets based on these values, gather data and save back to + the config dictionary for the worker. + + NOTE: When overriding you almost certainly will want to call the ancestor and then + add your config values to the list. + + :param include_default_tabs: bool: + :return: Returns a list of Detail elements + """ + + # Common configs + details = [ + DetailElement('text', 'Log', 'Worker\'s log') + ] + + if include_default_tabs: + return details + return [] + def __init__(self, name, config=None, diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index f2b36d1f2..e5cd7bc74 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -1,7 +1,7 @@ import math from datetime import datetime, timedelta -from dexbot.strategies.base import StrategyBase, ConfigElement +from dexbot.strategies.base import StrategyBase, ConfigElement, DetailElement from dexbot.qt_queue.idle_queue import idle_add @@ -54,6 +54,13 @@ def configure(cls, return_base_config=True): (30, 157680000, '')) ] + @classmethod + def configure_details(cls, include_default_tabs=True): + return StrategyBase.configure_details(include_default_tabs) + [ + DetailElement('graph', 'Profit', 'Profit for the past month'), + DetailElement('table', 'Buy orders', 'Open buy orders') + ] + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.log.info("Initializing Relative Orders") diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 19063cbfd..c388791f7 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -4,6 +4,7 @@ from bitshares.asset import Asset from dexbot.basestrategy import BaseStrategy, ConfigElement +from dexbot.strategies.base import StrategyBase, DetailElement from dexbot.qt_queue.idle_queue import idle_add @@ -70,6 +71,12 @@ def configure(cls, return_base_config=True): 'Allow to execute orders by market', None) ] + @classmethod + def configure_details(cls, include_default_tabs=True): + return StrategyBase.configure_details(include_default_tabs) + [ + DetailElement('text', 'Test', 'This is a test') + ] + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) From 898f57832d265f569535f5e6c9f3895f0326fb1e Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 5 Oct 2018 15:32:54 +0300 Subject: [PATCH 0832/1846] Change worker_details_window.ui default size --- dexbot/views/ui/worker_details_window.ui | 94 ++++++++++++------------ 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/dexbot/views/ui/worker_details_window.ui b/dexbot/views/ui/worker_details_window.ui index c337e0104..92f67ac4d 100644 --- a/dexbot/views/ui/worker_details_window.ui +++ b/dexbot/views/ui/worker_details_window.ui @@ -6,8 +6,8 @@ 0 0 - 660 - 460 + 700 + 500 @@ -18,56 +18,16 @@ - 660 - 460 + 700 + 500 DEXBot - Worker details - - - - - - 0 - 35 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 35 - - - - QDialogButtonBox::Close - - - - - - - - + + + true @@ -245,6 +205,46 @@ + + + + + 0 + 35 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 35 + + + + QDialogButtonBox::Close + + + + + + From 5679cc44e2930cfa0c9ef3e89e592d342f51a936 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 5 Oct 2018 15:33:10 +0300 Subject: [PATCH 0833/1846] WIP Add different test tabs to worker details --- dexbot/views/worker_details.py | 43 ++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/dexbot/views/worker_details.py b/dexbot/views/worker_details.py index b4999a9c9..7ac65e8bc 100644 --- a/dexbot/views/worker_details.py +++ b/dexbot/views/worker_details.py @@ -1,10 +1,16 @@ from dexbot.controllers.worker_details_controller import WorkerDetailsController from dexbot.views.ui.worker_details_window_ui import Ui_details_dialog +from dexbot.views.ui.tabs.graph_tab_ui import Ui_Graph_Tab +from dexbot.views.ui.tabs.table_tab_ui import Ui_Table_Tab +from dexbot.views.ui.tabs.text_tab_ui import Ui_Text_Tab -from PyQt5 import QtWidgets +from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5.QtWidgets import QWidget +import importlib -class WorkerDetailsView(QtWidgets.QDialog, Ui_details_dialog): + +class WorkerDetailsView(QtWidgets.QDialog, Ui_details_dialog, Ui_Graph_Tab, Ui_Table_Tab, Ui_Text_Tab): def __init__(self, worker_name, config): super().__init__() @@ -19,8 +25,41 @@ def __init__(self, worker_name, config): # Add worker's name to the dialog title self.setWindowTitle("DEXBot - {} details".format(worker_name)) + # Get strategy class from the config + strategy_class = getattr(importlib.import_module(self.config.get('module')), 'Strategy') + details = strategy_class.configure_details() + # Initialize other data to the dialog self.controller.initialize_worker_data() + # Testing that each tab works as intended, Todo: Remove these after dynamic creation works + self.tab_1 = QWidget(self) + self.tab_2 = QWidget(self) + self.tab_3 = QWidget(self) + self.tab_4 = QWidget(self) + + graph_tab = Ui_Graph_Tab() + table_tab = Ui_Table_Tab() + table_tab_2 = Ui_Table_Tab() + text_tab = Ui_Text_Tab() + + graph_tab.setupUi(self.tab_1) + table_tab.setupUi(self.tab_2) + table_tab_2.setupUi(self.tab_4) + text_tab.setupUi(self.tab_3) + + graph_tab.graph_wrap.setTitle('Profit estimate') + table_tab.table_wrap.setTitle('Worker\'s buy orders in the market') + table_tab_2.table_wrap.setTitle('Worker\'s sell orders in the market') + text_tab.text_wrap.setTitle('Local log') + + self.tabs_widget.addTab(self.tab_1, 'Profit') + self.tabs_widget.addTab(self.tab_2, 'Buy Orders') + self.tabs_widget.addTab(self.tab_4, 'Sell Orders') + self.tabs_widget.addTab(self.tab_3, 'Log') + # Dialog controls self.button_box.rejected.connect(self.reject) + + # Add tabs to the details view + # Todo: Continue from here From ba4183ef4145ac656c6f1e34255978525e9a3332 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 5 Oct 2018 20:34:13 +0500 Subject: [PATCH 0834/1846] Fix typos --- dexbot/strategies/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 94060594e..cc4db4bdc 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -474,7 +474,7 @@ def get_market_fee(self): return self.fee_asset.market_fee_percent def get_market_buy_orders(self, depth=10): - """ Fetches most reset data and returns list of buy orders. + """ Fetches most recent data and returns list of buy orders. :param int | depth: Amount of buy orders returned, Default=10 :return: List of market sell orders @@ -482,7 +482,7 @@ def get_market_buy_orders(self, depth=10): return self.get_market_orders(depth=depth)['bids'] def get_market_sell_orders(self, depth=10): - """ Fetches most reset data and returns list of sell orders. + """ Fetches most recent data and returns list of sell orders. :param int | depth: Amount of sell orders returned, Default=10 :return: List of market sell orders @@ -1089,7 +1089,7 @@ def quote_asset(self): def all_own_orders(self, refresh=True): """ Return the worker's open orders in all markets - :param bool | refresh: Use most resent data + :param bool | refresh: Use most recent data :return: List of Order objects """ # Refresh account data From e7bb1d67344660039e5f35f6b7fec7be4cd06206 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 5 Oct 2018 21:08:46 +0500 Subject: [PATCH 0835/1846] Refactor get_market_xxx_price() to allow exclude own orders --- dexbot/strategies/base.py | 75 ++++++++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index cc4db4bdc..25e5c4aca 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -479,7 +479,9 @@ def get_market_buy_orders(self, depth=10): :param int | depth: Amount of buy orders returned, Default=10 :return: List of market sell orders """ - return self.get_market_orders(depth=depth)['bids'] + orders = self.get_limit_orders(depth=depth) + buy_orders = self.filter_buy_orders(orders) + return buy_orders def get_market_sell_orders(self, depth=10): """ Fetches most recent data and returns list of sell orders. @@ -487,7 +489,9 @@ def get_market_sell_orders(self, depth=10): :param int | depth: Amount of sell orders returned, Default=10 :return: List of market sell orders """ - return self.get_market_orders(depth=depth)['asks'] + orders = self.get_limit_orders(depth=depth) + sell_orders = self.filter_sell_orders(orders) + return sell_orders def get_highest_market_buy_order(self, orders=None): """ Returns the highest buy order that is not own, regardless of order size. @@ -560,8 +564,8 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors :return: Market center price as float """ - buy_price = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount) - sell_price = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount) + buy_price = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount, exclude_own_orders=False) + sell_price = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount, exclude_own_orders=False) if buy_price is None or buy_price == 0.0: if not suppress_errors: @@ -578,19 +582,31 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors # Calculate and return market center price return buy_price * math.sqrt(sell_price / buy_price) - def get_market_buy_price(self, quote_amount=0, base_amount=0): + def get_market_buy_price(self, quote_amount=0, base_amount=0, exclude_own_orders=True): """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with moving average or weighted moving average :param float | quote_amount: :param float | base_amount: - :return: + :param bool | exclude_own_orders: Exclude own orders when calculating a price + :return: price as float """ - # Like get_market_sell_price(), but defaulting to base_amount if both base and quote are specified. - # In case amount is not given, return price of the lowest sell order on the market + market_buy_orders = [] + + # Exclude own orders from orderbook if needed + if exclude_own_orders: + market_buy_orders = self.get_market_buy_orders(depth=self.fetch_depth) + own_buy_orders_ids = [o['id'] for o in self.get_own_buy_orders()] + market_buy_orders = [o for o in market_buy_orders if o['id'] not in own_buy_orders_ids] + + # In case amount is not given, return price of the highest buy order on the market if quote_amount == 0 and base_amount == 0: - return self.ticker().get('highestBid') + if exclude_own_orders: + return float(market_buy_orders[0]['price']) + else: + return float(self.ticker().get('highestBid')) + # Like get_market_sell_price(), but defaulting to base_amount if both base and quote are specified. asset_amount = base_amount """ Since the purpose is never get both quote and base amounts, favor base amount if both given because @@ -602,7 +618,8 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0): asset_amount = quote_amount base = False - market_buy_orders = self.get_market_buy_orders(depth=self.fetch_depth) + if not market_buy_orders: + market_buy_orders = self.get_market_buy_orders(depth=self.fetch_depth) market_fee = self.get_market_fee() target_amount = asset_amount * (1 + market_fee) @@ -635,9 +652,22 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0): return base_amount / quote_amount - def get_market_orders(self, depth=1): + def get_limit_orders(self, depth=1): + """ Returns orders from the current market. Orders are sorted by price. + + get_limit_orders() call does not have any depth limit. + + :param int | depth: Amount of orders per side will be fetched, default=1 + :return: Returns a list of orders or None + """ + orders = self.bitshares.rpc.get_limit_orders(self.market['base']['id'], self.market['quote']['id'], depth) + return [Order(o, bitshares_instance=self.bitshares) for o in orders] + + def get_orderbook_orders(self, depth=1): """ Returns orders from the current market split in bids and asks. Orders are sorted by price. + Market.orderbook() call has hard-limit of depth=50 enforced by bitshares node. + bids = buy orders asks = sell orders @@ -646,7 +676,7 @@ def get_market_orders(self, depth=1): """ return self.market.orderbook(depth) - def get_market_sell_price(self, quote_amount=0, base_amount=00): + def get_market_sell_price(self, quote_amount=0, base_amount=0, exclude_own_orders=True): """ Returns the BASE/QUOTE price for which [quote_amount] worth of QUOTE could be bought, enhanced with moving average or weighted moving average. @@ -654,11 +684,23 @@ def get_market_sell_price(self, quote_amount=0, base_amount=00): :param float | quote_amount: :param float | base_amount: + :param bool | exclude_own_orders: Exclude own orders when calculating a price :return: """ + market_sell_orders = [] + + # Exclude own orders from orderbook if needed + if exclude_own_orders: + market_sell_orders = self.get_market_sell_orders(depth=self.fetch_depth) + own_sell_orders_ids = [o['id'] for o in self.get_own_sell_orders()] + market_sell_orders = [o for o in market_sell_orders if o['id'] not in own_sell_orders_ids] + # In case amount is not given, return price of the lowest sell order on the market if quote_amount == 0 and base_amount == 0: - return self.ticker().get('lowestAsk') + if exclude_own_orders: + return float(market_sell_orders[0]['price']) + else: + return float(self.ticker().get('lowestAsk')) asset_amount = quote_amount @@ -671,7 +713,8 @@ def get_market_sell_price(self, quote_amount=0, base_amount=00): asset_amount = base_amount quote = False - market_sell_orders = self.get_market_sell_orders(depth=self.fetch_depth) + if not market_sell_orders: + market_sell_orders = self.get_market_sell_orders(depth=self.fetch_depth) market_fee = self.get_market_fee() target_amount = asset_amount * (1 + market_fee) @@ -711,8 +754,8 @@ def get_market_spread(self, quote_amount=0, base_amount=0): :param float | base_amount: :return: Market spread as float or None """ - ask = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount) - bid = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount) + ask = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount, exclude_own_orders=False) + bid = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount, exclude_own_orders=False) # Calculate market spread if ask == 0 or bid == 0: From 7b1a50f9ffde4879f60c995add758c2dfc06fb5c Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 5 Oct 2018 22:52:44 +0500 Subject: [PATCH 0836/1846] Use updated orders in get_limit_orders() This needed to properly handle partially filled orders in get_market_xxx_price() --- dexbot/strategies/base.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 25e5c4aca..0b58df801 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -652,16 +652,21 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, exclude_own_orders return base_amount / quote_amount - def get_limit_orders(self, depth=1): + def get_limit_orders(self, depth=1, updated=True): """ Returns orders from the current market. Orders are sorted by price. get_limit_orders() call does not have any depth limit. :param int | depth: Amount of orders per side will be fetched, default=1 + :param bool | updated: Return updated orders. "Updated" means partially filled orders will represent + remainders and not just initial amounts :return: Returns a list of orders or None """ orders = self.bitshares.rpc.get_limit_orders(self.market['base']['id'], self.market['quote']['id'], depth) - return [Order(o, bitshares_instance=self.bitshares) for o in orders] + if updated: + orders = [self.get_updated_limit_order(o) for o in orders] + orders = [Order(o, bitshares_instance=self.bitshares) for o in orders] + return orders def get_orderbook_orders(self, depth=1): """ Returns orders from the current market split in bids and asks. Orders are sorted by price. From 3e743cdad7cad073312b24175733167f9c449d38 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 6 Oct 2018 15:32:37 +0500 Subject: [PATCH 0837/1846] Refactor get_updated_order() to handle not only own orders --- dexbot/strategies/base.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 0b58df801..ca22a878f 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -876,25 +876,17 @@ def get_own_spread(self): return actual_spread def get_updated_order(self, order_id): - # Todo: This needed? """ Tries to get the updated order from the API. Returns None if the order doesn't exist - :param str|dict order_id: blockchain object id of the order can be an order dict with the id key in it + :param str|dict order: blockchain Order object or id of the order """ if isinstance(order_id, dict): order_id = order_id['id'] - - # Get the limited order by id - order = None - for limit_order in self.account['limit_orders']: - if order_id == limit_order['id']: - order = limit_order - break - else: - return order - - order = self.get_updated_limit_order(order) - return Order(order, bitshares_instance=self.bitshares) + # We are using direct rpc call here because passing an Order object to self.get_updated_limit_order() give us + # weird error "Object of type 'BitShares' is not JSON serializable" + order = self.bitshares.rpc.get_objects([order_id])[0] + updated_order = self.get_updated_limit_order(order) + return Order(updated_order, bitshares_instance=self.bitshares) def is_current_market(self, base_asset_id, quote_asset_id): """ Returns True if given asset id's are of the current market @@ -1211,10 +1203,8 @@ def get_updated_limit_order(limit_order): """ Returns a modified limit_order so that when passed to Order class, will return an Order object with updated amount values - :param limit_order: an item of Account['limit_orders'] + :param limit_order: an item of Account['limit_orders'] or bitshares.rpc.get_limit_orders() :return: Order - Todo: When would we not want an updated order? - Todo: If get_updated_order is removed, this can be removed as well. """ order = copy.deepcopy(limit_order) price = float(order['sell_price']['base']['amount']) / float(order['sell_price']['quote']['amount']) From b05953473fe67cccc21dc67977bc9977f75d5f30 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 6 Oct 2018 20:42:57 +0500 Subject: [PATCH 0838/1846] Handle empty markets when exclude_own_orders is active --- dexbot/strategies/base.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index ca22a878f..43a5a848d 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -602,7 +602,10 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, exclude_own_orders # In case amount is not given, return price of the highest buy order on the market if quote_amount == 0 and base_amount == 0: if exclude_own_orders: - return float(market_buy_orders[0]['price']) + if market_buy_orders: + return float(market_buy_orders[0]['price']) + else: + return '0.0' else: return float(self.ticker().get('highestBid')) @@ -703,7 +706,10 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, exclude_own_order # In case amount is not given, return price of the lowest sell order on the market if quote_amount == 0 and base_amount == 0: if exclude_own_orders: - return float(market_sell_orders[0]['price']) + if market_sell_orders: + return float(market_sell_orders[0]['price']) + else: + return '0.0' else: return float(self.ticker().get('lowestAsk')) From 8b423d8feda115cb698f6fc9fd7de79891ff38e5 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 6 Oct 2018 21:22:21 +0500 Subject: [PATCH 0839/1846] Implement methods is_xxx_order() --- dexbot/strategies/base.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 43a5a848d..bd503bc81 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -894,6 +894,18 @@ def get_updated_order(self, order_id): updated_order = self.get_updated_limit_order(order) return Order(updated_order, bitshares_instance=self.bitshares) + def is_buy_order(self, order): + """ Check whether an order is buy order + + :param dict | order: dict or Order object + :return bool + """ + # Check if the order is buy order, by comparing asset symbol of the order and the market + if order['base']['symbol'] == self.market['base']['symbol']: + return True + else: + return False + def is_current_market(self, base_asset_id, quote_asset_id): """ Returns True if given asset id's are of the current market @@ -912,6 +924,18 @@ def is_current_market(self, base_asset_id, quote_asset_id): return False + def is_sell_order(self, order): + """ Check whether an order is sell order + + :param dict | order: dict or Order object + :return bool + """ + # Check if the order is sell order, by comparing asset symbol of the order and the market + if order['base']['symbol'] == self.market['quote']['symbol']: + return True + else: + return False + def pause(self): """ Pause the worker From e4f11ed389b3e231f2616ac263224942ebb4ed76 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Mon, 8 Oct 2018 11:26:50 +0300 Subject: [PATCH 0840/1846] added some missing return statements --- dexbot/strategies/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 94060594e..073db4275 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -262,8 +262,10 @@ def _cancel_orders(self, orders): return False else: self.log.exception("Unable to cancel order") + return False except bitshares.exceptions.MissingKeyError: self.log.exception('Unable to cancel order(s), private key missing.') + return False return True From 903f10cf900aac7dcb487359607bd05cc3d8fa79 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 8 Oct 2018 15:18:26 +0300 Subject: [PATCH 0841/1846] Add get_data_directory() to helper.py --- dexbot/helper.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/dexbot/helper.py b/dexbot/helper.py index 832ddba6e..5f4e16ec2 100644 --- a/dexbot/helper.py +++ b/dexbot/helper.py @@ -43,10 +43,16 @@ def truncate(number, decimals): return math.floor(number * 10 ** decimals) / 10 ** decimals +def get_data_directory(): + """ Returns the data directory path which contains history, sql and logs + """ + return user_data_dir(APP_NAME, AUTHOR) + + def initialize_orders_log(): """ Creates .csv log file, adds the headers first time only """ - data_dir = user_data_dir(APP_NAME, AUTHOR) + data_dir = get_data_directory() filename = os.path.join(data_dir, 'history.csv') file = os.path.isfile(filename) From bbdfd90741098db07439ae61e112574a4ec757ca Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 8 Oct 2018 15:19:29 +0300 Subject: [PATCH 0842/1846] Remove unnecessary empty documentation in base.py --- dexbot/strategies/base.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 5d3df0cd7..5f7177353 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -277,12 +277,6 @@ def _callbackPlaceFillOrders(self, d): pass def _cancel_orders(self, orders): - """ - - :param orders: - :return: - """ - # Todo: Add documentation try: self.retry_action( self.bitshares.cancel, From ec5233cd7136cbd7d4c0d4387f0dc55ae9e00a66 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 8 Oct 2018 15:19:46 +0300 Subject: [PATCH 0843/1846] Remove test DetailElements from Staggered Orders --- dexbot/strategies/staggered_orders.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c388791f7..8444fd1a2 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -73,9 +73,7 @@ def configure(cls, return_base_config=True): @classmethod def configure_details(cls, include_default_tabs=True): - return StrategyBase.configure_details(include_default_tabs) + [ - DetailElement('text', 'Test', 'This is a test') - ] + return StrategyBase.configure_details(include_default_tabs) + [] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) From f681f5e461446363eae13b7c6722a495eee804f5 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 8 Oct 2018 15:20:29 +0300 Subject: [PATCH 0844/1846] Remove test tabs from worker_details.py --- dexbot/views/worker_details.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/dexbot/views/worker_details.py b/dexbot/views/worker_details.py index 7ac65e8bc..6e1bc425e 100644 --- a/dexbot/views/worker_details.py +++ b/dexbot/views/worker_details.py @@ -32,31 +32,6 @@ def __init__(self, worker_name, config): # Initialize other data to the dialog self.controller.initialize_worker_data() - # Testing that each tab works as intended, Todo: Remove these after dynamic creation works - self.tab_1 = QWidget(self) - self.tab_2 = QWidget(self) - self.tab_3 = QWidget(self) - self.tab_4 = QWidget(self) - - graph_tab = Ui_Graph_Tab() - table_tab = Ui_Table_Tab() - table_tab_2 = Ui_Table_Tab() - text_tab = Ui_Text_Tab() - - graph_tab.setupUi(self.tab_1) - table_tab.setupUi(self.tab_2) - table_tab_2.setupUi(self.tab_4) - text_tab.setupUi(self.tab_3) - - graph_tab.graph_wrap.setTitle('Profit estimate') - table_tab.table_wrap.setTitle('Worker\'s buy orders in the market') - table_tab_2.table_wrap.setTitle('Worker\'s sell orders in the market') - text_tab.text_wrap.setTitle('Local log') - - self.tabs_widget.addTab(self.tab_1, 'Profit') - self.tabs_widget.addTab(self.tab_2, 'Buy Orders') - self.tabs_widget.addTab(self.tab_4, 'Sell Orders') - self.tabs_widget.addTab(self.tab_3, 'Log') # Dialog controls self.button_box.rejected.connect(self.reject) From aa9bb65240884ba8af26d4fd2b87375490c34fe5 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 8 Oct 2018 15:20:57 +0300 Subject: [PATCH 0845/1846] WIP Add dynamic tab in worker_details.py --- dexbot/views/worker_details.py | 39 +++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/dexbot/views/worker_details.py b/dexbot/views/worker_details.py index 6e1bc425e..a4b06ceca 100644 --- a/dexbot/views/worker_details.py +++ b/dexbot/views/worker_details.py @@ -1,4 +1,5 @@ from dexbot.controllers.worker_details_controller import WorkerDetailsController +from dexbot.helper import * from dexbot.views.ui.worker_details_window_ui import Ui_details_dialog from dexbot.views.ui.tabs.graph_tab_ui import Ui_Graph_Tab from dexbot.views.ui.tabs.table_tab_ui import Ui_Table_Tab @@ -27,14 +28,46 @@ def __init__(self, worker_name, config): # Get strategy class from the config strategy_class = getattr(importlib.import_module(self.config.get('module')), 'Strategy') + + # DetailElements that are used to configure worker's detail view details = strategy_class.configure_details() # Initialize other data to the dialog self.controller.initialize_worker_data() + # Add tabs to the details view + # Todo: Make this prettier + for detail in details: + widget = QWidget(self) + + if detail.type == 'graph': + tab = Ui_Graph_Tab() + tab.setupUi(widget) + tab.graph_wrap.setTitle(detail.title) + + # Get image path + # Todo: Pass the image name from the strategy as well as the location + directory = get_data_directory() + '/graphs' + filename = os.path.join(directory, 'graph.jpg') + + # Create pixmap of the image + pixmap = QtGui.QPixmap(filename) + + # Set graph image to the label + tab.graph.setPixmap(pixmap) + + # Resize label to fit the image + # Todo: Resize the tab to fit the image nicely + elif detail.type == 'table': + tab = Ui_Table_Tab() + tab.setupUi(widget) + tab.table_wrap.setTitle(detail.title) + elif detail.type == 'text': + tab = Ui_Text_Tab() + tab.setupUi(widget) + tab.text_wrap.setTitle(detail.title) + + self.tabs_widget.addTab(widget, detail.name) # Dialog controls self.button_box.rejected.connect(self.reject) - - # Add tabs to the details view - # Todo: Continue from here From b944d26706d6846ca9caa243afd61f1427f2edfa Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 8 Oct 2018 21:14:05 +0500 Subject: [PATCH 0846/1846] Restore function name get_limit_orders -> get_market_orders --- dexbot/strategies/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index bd503bc81..06d9925be 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -479,7 +479,7 @@ def get_market_buy_orders(self, depth=10): :param int | depth: Amount of buy orders returned, Default=10 :return: List of market sell orders """ - orders = self.get_limit_orders(depth=depth) + orders = self.get_market_orders(depth=depth) buy_orders = self.filter_buy_orders(orders) return buy_orders @@ -489,7 +489,7 @@ def get_market_sell_orders(self, depth=10): :param int | depth: Amount of sell orders returned, Default=10 :return: List of market sell orders """ - orders = self.get_limit_orders(depth=depth) + orders = self.get_market_orders(depth=depth) sell_orders = self.filter_sell_orders(orders) return sell_orders @@ -655,10 +655,10 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, exclude_own_orders return base_amount / quote_amount - def get_limit_orders(self, depth=1, updated=True): + def get_market_orders(self, depth=1, updated=True): """ Returns orders from the current market. Orders are sorted by price. - get_limit_orders() call does not have any depth limit. + get_market_orders() call does not have any depth limit. :param int | depth: Amount of orders per side will be fetched, default=1 :param bool | updated: Return updated orders. "Updated" means partially filled orders will represent From ff09a507445ca6dec77a90dbb580b3299b03f164 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 9 Oct 2018 00:59:28 +0500 Subject: [PATCH 0847/1846] Use rpc.get_objects() only as a fallback When updating own orders, we do not want to generate excessive RPC calls, so try to iterate over own orders first. --- dexbot/strategies/base.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 06d9925be..a380ad7d2 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -888,9 +888,17 @@ def get_updated_order(self, order_id): """ if isinstance(order_id, dict): order_id = order_id['id'] - # We are using direct rpc call here because passing an Order object to self.get_updated_limit_order() give us - # weird error "Object of type 'BitShares' is not JSON serializable" - order = self.bitshares.rpc.get_objects([order_id])[0] + + # At first, try to look up own orders. This prevents RPC calls whether requested order is own order + order = None + for limit_order in self.account['limit_orders']: + if order_id == limit_order['id']: + order = limit_order + break + else: + # We are using direct rpc call here because passing an Order object to self.get_updated_limit_order() give us + # weird error "Object of type 'BitShares' is not JSON serializable" + order = self.bitshares.rpc.get_objects([order_id])[0] updated_order = self.get_updated_limit_order(order) return Order(updated_order, bitshares_instance=self.bitshares) From 5d053351634f732700c0adce270267c9c90c3b36 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 9 Oct 2018 00:24:58 +0500 Subject: [PATCH 0848/1846] Reduce calls of refresh_orders() When we are bundling operations threre is no sense to refresh orders after allocate_asset() because the actual orders are placed after that. This will speed up the strategy a bit, see #339 --- dexbot/strategies/staggered_orders.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 19063cbfd..35f961346 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -562,8 +562,9 @@ def allocate_asset(self, asset, asset_balance): elif asset == 'quote': self.place_highest_sell_order(asset_balance) - # Get latest orders - self.refresh_orders() + # Get latest orders only when we are not bundling operations + if self.returnOrderId: + self.refresh_orders() def increase_order_sizes(self, asset, asset_balance, orders): """ Checks which order should be increased in size and replaces it From d933ff4c682a67c79947a0222fe412041e77b8a8 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 9 Oct 2018 16:19:04 +0500 Subject: [PATCH 0849/1846] Get rid of unneeded refresh_orders() call We need to refresh_orders() only when we're actually removing orders outside boundaries and not eah time. Related to #339 --- dexbot/strategies/staggered_orders.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 35f961346..955346b95 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -186,10 +186,7 @@ def maintain_strategy(self, *args, **kwargs): # Remove orders that exceed boundaries success = self.remove_outside_orders(self.sell_orders, self.buy_orders) - if success: - # Refresh orders to prevent orders outside boundaries being in the future comparisons - self.refresh_orders() - else: + if not success: # Return back to beginning self.log_maintenance_time() return @@ -374,6 +371,8 @@ def remove_outside_orders(self, sell_orders, buy_orders): if orders_to_cancel: # We are trying to cancel all orders in one try success = self.cancel(orders_to_cancel, batch_only=True) + # Refresh orders to prevent orders outside boundaries being in the future comparisons + self.refresh_orders() # Batch cancel failed, repeat cancelling only one order if success: return True From 9aa8c1727dca72b7ad1b733f24e3554201a37591 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 9 Oct 2018 16:34:47 +0500 Subject: [PATCH 0850/1846] Make one of refresh_balances() call conditional When replacing a partially filled order refresh the balances only when we are not bundling operations Related to #339 --- 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 955346b95..8dd136557 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -549,7 +549,8 @@ def allocate_asset(self, asset, asset_balance): elif asset == 'quote': price = closest_own_order['price'] ** -1 self.market_sell(closest_own_order['base']['amount'], price) - self.refresh_balances() + if self.returnOrderId: + self.refresh_balances() else: self.log.debug('Not replacing partially filled order because there is not enough funds') else: From 0b8b25528f479f17ca9a567246ee803b3be80397 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 9 Oct 2018 16:38:48 +0500 Subject: [PATCH 0851/1846] Don't refresh total balances when not needed In some places we need only free available balance but not total, so to avoid excessive API calls implement an option to skip orders balance calculation. Related to #339 --- dexbot/strategies/staggered_orders.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 8dd136557..79279579b 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -220,7 +220,7 @@ def maintain_strategy(self, *args, **kwargs): # Maintain the history of free balances after maintenance runs. # Save exactly key values instead of full key because it may be modified later on. - self.refresh_balances() + self.refresh_balances(total_balances=False) self.base_balance_history.append(self.base_balance['amount']) self.quote_balance_history.append(self.quote_balance['amount']) if len(self.base_balance_history) > 3: @@ -303,8 +303,9 @@ def log_maintenance_time(self): delta = datetime.now() - self.start self.log.debug('Maintenance execution took: {:.2f} seconds'.format(delta.total_seconds())) - def refresh_balances(self): + def refresh_balances(self, total_balances=True): """ This function is used to refresh account balances + :param bool | total_balances: refresh total balance or skip it """ # Get current account balances account_balances = self.total_balance(order_ids=[], return_asset=True) @@ -330,6 +331,9 @@ def refresh_balances(self): elif self.fee_asset['id'] == self.market['quote']['id']: self.quote_balance['amount'] = self.quote_balance['amount'] - fee_reserve + if not total_balances: + return + # Balance per asset from orders order_ids = [order['id'] for order in self.orders] orders_balance = self.orders_balance(order_ids) @@ -550,7 +554,7 @@ def allocate_asset(self, asset, asset_balance): price = closest_own_order['price'] ** -1 self.market_sell(closest_own_order['base']['amount'], price) if self.returnOrderId: - self.refresh_balances() + self.refresh_balances(total_balances=False) else: self.log.debug('Not replacing partially filled order because there is not enough funds') else: From d38179913b41daa886572bfa3648763f2cadbc56 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 9 Oct 2018 16:50:22 +0500 Subject: [PATCH 0852/1846] Improve refresh_balances() by adding use_cached_orders kwarg To avoid API call, we can omit calling self.orders whether right before refresh_balances() we already called refresh_orders(). Related to #339 --- dexbot/strategies/staggered_orders.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 79279579b..0b34cdee3 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -123,6 +123,7 @@ def __init__(self, *args, **kwargs): # Initial balance history elements should not be equal to avoid immediate bootstrap turn off self.quote_balance_history = [1, 2, 3] self.base_balance_history = [1, 2, 3] + self.cached_orders = None # Order expiration time self.expiration = 60 * 60 * 24 * 365 * 5 @@ -171,8 +172,8 @@ def maintain_strategy(self, *args, **kwargs): self.log.warning('Cannot calculate center price on empty market, please set is manually') return - # Calculate balances - self.refresh_balances() + # Calculate balances, and use orders from previous call of self.refresh_orders() to reduce API calls + self.refresh_balances(use_cached_orders=True) # Calculate asset thresholds self.quote_asset_threshold = self.quote_total_balance / 20000 @@ -303,9 +304,10 @@ def log_maintenance_time(self): delta = datetime.now() - self.start self.log.debug('Maintenance execution took: {:.2f} seconds'.format(delta.total_seconds())) - def refresh_balances(self, total_balances=True): + def refresh_balances(self, total_balances=True, use_cached_orders=False): """ This function is used to refresh account balances :param bool | total_balances: refresh total balance or skip it + :param bool | use_cached_orders: when calculating orders balance, use cached orders from self.cached_orders """ # Get current account balances account_balances = self.total_balance(order_ids=[], return_asset=True) @@ -335,7 +337,11 @@ def refresh_balances(self, total_balances=True): return # Balance per asset from orders - order_ids = [order['id'] for order in self.orders] + if use_cached_orders and self.cached_orders: + orders = self.cached_orders + else: + orders = self.orders + order_ids = [order['id'] for order in orders] orders_balance = self.orders_balance(order_ids) # Total balance per asset (orders balance and available balance) @@ -346,6 +352,7 @@ def refresh_orders(self): """ Updates buy and sell orders """ orders = self.orders + self.cached_orders = orders # Sort orders so that order with index 0 is closest to the center price and -1 is furthers self.buy_orders = self.get_buy_orders('DESC', orders) From 2042e54b3c1965f26590ccdf57fb68ab2867c461 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Tue, 9 Oct 2018 19:15:39 +0300 Subject: [PATCH 0853/1846] allowed also owner key for workers --- dexbot/controllers/worker_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index 4d79e3211..20b1395eb 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -178,7 +178,7 @@ def validate_private_key_type(self, account, private_key): account = Account(account) pubkey = format(PrivateKey(private_key).pubkey, self.bitshares.prefix) key_type = self.bitshares.wallet.getKeyType(account, pubkey) - if key_type != 'active': + if key_type == 'memo': return False return True From f35fb1fd4e4fecca55b51307f11ccf586db6cded Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Tue, 9 Oct 2018 21:54:09 +0300 Subject: [PATCH 0854/1846] Update worker_controller.py --- dexbot/controllers/worker_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index 20b1395eb..a6d1fae34 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -178,7 +178,7 @@ def validate_private_key_type(self, account, private_key): account = Account(account) pubkey = format(PrivateKey(private_key).pubkey, self.bitshares.prefix) key_type = self.bitshares.wallet.getKeyType(account, pubkey) - if key_type == 'memo': + if key_type != 'active' and key_type != 'owner': return False return True From 2ea7e5aaecb63845a36ee95f52e30f18ebfc66e8 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 11 Oct 2018 15:07:41 +0300 Subject: [PATCH 0855/1846] Add get_user_data_directory() to helper.py --- dexbot/helper.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/dexbot/helper.py b/dexbot/helper.py index 5f4e16ec2..4f778b4b7 100644 --- a/dexbot/helper.py +++ b/dexbot/helper.py @@ -43,16 +43,24 @@ def truncate(number, decimals): return math.floor(number * 10 ** decimals) / 10 ** decimals -def get_data_directory(): - """ Returns the data directory path which contains history, sql and logs +def get_user_data_directory(): + """ Returns the user data directory path which contains history, sql and logs """ return user_data_dir(APP_NAME, AUTHOR) +def initialize_data_folders(): + """ Creates folders for strategies to store files """ + user_data_directory = get_user_data_directory() + mkdir(os.path.join(user_data_directory, 'graphs')) + mkdir(os.path.join(user_data_directory, 'data')) + mkdir(os.path.join(user_data_directory, 'logs')) + + def initialize_orders_log(): """ Creates .csv log file, adds the headers first time only """ - data_dir = get_data_directory() + data_dir = get_user_data_directory() filename = os.path.join(data_dir, 'history.csv') file = os.path.isfile(filename) From 968b387c2a4e0e2ee1daf8668750675cddbe88c9 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 11 Oct 2018 15:09:58 +0300 Subject: [PATCH 0856/1846] Add folders for graphs, data and logs --- dexbot/cli.py | 5 ++++- dexbot/controllers/main_controller.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index 5e49f90aa..1ff25a9a2 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -6,7 +6,7 @@ import sys from dexbot.config import Config, DEFAULT_CONFIG_FILE -from dexbot.helper import initialize_orders_log +from dexbot.helper import initialize_orders_log, initialize_data_folders from dexbot.ui import ( verbose, chain, @@ -35,6 +35,9 @@ # Configure orders logging initialize_orders_log() +# Initialize data folders +initialize_data_folders() + @click.group() @click.option( diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 29daf26b1..ebe422e5a 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -3,7 +3,7 @@ import sys from dexbot import VERSION, APP_NAME, AUTHOR -from dexbot.helper import initialize_orders_log +from dexbot.helper import initialize_orders_log, initialize_data_folders from dexbot.worker import WorkerInfrastructure from dexbot.views.errors import PyQtHandler @@ -38,6 +38,9 @@ def __init__(self, bitshares_instance, config): # Configure orders logging initialize_orders_log() + # Initialize folders + initialize_data_folders() + def set_info_handler(self, handler): self.pyqt_handler.set_info_handler(handler) From 44c3f0b0dc670ca3e59aa596e8b828f0afdc5aaf Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 11 Oct 2018 15:12:20 +0300 Subject: [PATCH 0857/1846] Remove default DetailElement from base.py --- dexbot/strategies/base.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 5f7177353..3f7353fcc 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -152,9 +152,7 @@ def configure_details(cls, include_default_tabs=True): """ # Common configs - details = [ - DetailElement('text', 'Log', 'Worker\'s log') - ] + details = [] if include_default_tabs: return details From 6cbbbc7866bbdf9390a90a3bacb44bc15082bfce Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 11 Oct 2018 15:12:34 +0300 Subject: [PATCH 0858/1846] Add file parameter to DetailElement --- dexbot/strategies/base.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 3f7353fcc..97839b61e 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -51,8 +51,21 @@ - Type: 'graph', 'text', 'table' - Name: The name of the tab, shows at the top - Title: The title is shown inside the tab + - File: Tabs can also show data from files, pass on the file name including the file extension + in strategy's `configure_details`. Below folders and representative file types that inside the folders. + + Location : File extensions + dexbot/graphs : .png, .jpg + dexbot/data : .csv + dexbot/logs : .log, .txt + + NOTE: To avoid conflicts with other custom strategies, when generating names for files, use slug or worker's + name when generating files. + + Example of graph tab: + `DetailElement('graph', 'Profit', 'Profit for the past month', 'profit_graph.jpg')` """ -DetailElement = collections.namedtuple('DetailTab', 'type name title') +DetailElement = collections.namedtuple('DetailTab', 'type name title file') class StrategyBase(BaseStrategy, Storage, StateMachine, Events): From 39e5039efaa8b75857bf9d59bfed0bb2dccc89d3 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 11 Oct 2018 15:13:10 +0300 Subject: [PATCH 0859/1846] Change text_tab.ui --- dexbot/views/ui/tabs/text_tab.ui | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/dexbot/views/ui/tabs/text_tab.ui b/dexbot/views/ui/tabs/text_tab.ui index 69b3f44d8..cb53f8d85 100644 --- a/dexbot/views/ui/tabs/text_tab.ui +++ b/dexbot/views/ui/tabs/text_tab.ui @@ -20,8 +20,30 @@ + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + - + + + + + + + + From dfd781b7f7b05840e93a542119fc18fa539ea4d0 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 11 Oct 2018 15:13:20 +0300 Subject: [PATCH 0860/1846] Change table_tab.ui --- dexbot/views/ui/tabs/table_tab.ui | 51 ++++++++++++++----------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/dexbot/views/ui/tabs/table_tab.ui b/dexbot/views/ui/tabs/table_tab.ui index 9d50242c9..84473f778 100644 --- a/dexbot/views/ui/tabs/table_tab.ui +++ b/dexbot/views/ui/tabs/table_tab.ui @@ -20,6 +20,21 @@ + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + @@ -35,38 +50,18 @@ true - 20 - - - 4 + 0 120 - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + From e4865a4678b231e4f9d9857600ff798cd46b5763 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 11 Oct 2018 15:14:05 +0300 Subject: [PATCH 0861/1846] Add csv file reading to details controller --- .../controllers/worker_details_controller.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/dexbot/controllers/worker_details_controller.py b/dexbot/controllers/worker_details_controller.py index 4089ace03..0abedadd8 100644 --- a/dexbot/controllers/worker_details_controller.py +++ b/dexbot/controllers/worker_details_controller.py @@ -1,3 +1,7 @@ +import csv + +from PyQt5.QtWidgets import QTableWidgetItem +from PyQt5.QtGui import QTextCursor class WorkerDetailsController: @@ -25,3 +29,42 @@ def initialize_worker_data(self): self.view.strategy_name.setText(self.config.get('module')) self.view.market.setText(self.config.get('market')) self.view.fee_asset.setText(self.config.get('fee_asset')) + + @staticmethod + def populate_table_from_csv(table, file, delimiter=';', first_item_header=True): + try: + with open(file, 'r') as csv_file: + file_reader = csv.reader(csv_file, delimiter=delimiter) + rows = list(file_reader) + except FileNotFoundError: + print('File {} not found'.format(file)) + + table.setColumnCount(len(rows[0])) + + # Set headers + if first_item_header: + headers = rows.pop(0) + for header_index, header in enumerate(headers): + item = QTableWidgetItem() + item.setText(header) + table.setHorizontalHeaderItem(header_index, item) + + # Set rows data + table.setRowCount(len(rows)) + for row_index, row in enumerate(rows): + for column_index, column in enumerate(row): + item = QTableWidgetItem() + item.setText(column) + table.setItem(row_index, column_index, item) + + return table + + @staticmethod + def populate_text_from_file(tab, file): + try: + tab.text.setPlainText(open(file).read()) + tab.text.moveCursor(QTextCursor.End) + + return tab + except FileNotFoundError: + tab.status_label.setText('File \'{}\' not found'.format(file)) From 9029a0679930a81d7af1231580b815828b642fba Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 11 Oct 2018 15:15:03 +0300 Subject: [PATCH 0862/1846] Change worker_details.py --- dexbot/views/ui/tabs/graph_tab.ui | 32 ++++++++++++- dexbot/views/worker_details.py | 74 ++++++++++++++++++++----------- 2 files changed, 79 insertions(+), 27 deletions(-) diff --git a/dexbot/views/ui/tabs/graph_tab.ui b/dexbot/views/ui/tabs/graph_tab.ui index cf157e2c9..77e336097 100644 --- a/dexbot/views/ui/tabs/graph_tab.ui +++ b/dexbot/views/ui/tabs/graph_tab.ui @@ -19,9 +19,37 @@ - + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + - + + + Qt::ScrollBarAlwaysOff + + + Qt::ScrollBarAlwaysOff + + + QAbstractScrollArea::AdjustToContents + + + + + diff --git a/dexbot/views/worker_details.py b/dexbot/views/worker_details.py index a4b06ceca..71f48874f 100644 --- a/dexbot/views/worker_details.py +++ b/dexbot/views/worker_details.py @@ -35,39 +35,63 @@ def __init__(self, worker_name, config): # Initialize other data to the dialog self.controller.initialize_worker_data() + # Add custom tabs + self.add_tabs(details) + + # Dialog controls + self.button_box.rejected.connect(self.reject) + + def add_tabs(self, details): # Add tabs to the details view - # Todo: Make this prettier for detail in details: widget = QWidget(self) if detail.type == 'graph': - tab = Ui_Graph_Tab() - tab.setupUi(widget) - tab.graph_wrap.setTitle(detail.title) + widget = self.add_graph_tab(detail, widget) + elif detail.type == 'table': + widget = self.add_table_tab(detail, widget) + elif detail.type == 'text': + widget = self.add_text_tab(detail, widget) - # Get image path - # Todo: Pass the image name from the strategy as well as the location - directory = get_data_directory() + '/graphs' - filename = os.path.join(directory, 'graph.jpg') + self.tabs_widget.addTab(widget, detail.name) - # Create pixmap of the image - pixmap = QtGui.QPixmap(filename) + @staticmethod + def add_graph_tab(detail, widget): + tab = Ui_Graph_Tab() + tab.setupUi(widget) + tab.graph_wrap.setTitle(detail.title) - # Set graph image to the label - tab.graph.setPixmap(pixmap) + # Get image path + directory = get_user_data_directory() + file = os.path.join(directory, 'graphs', detail.file) - # Resize label to fit the image - # Todo: Resize the tab to fit the image nicely - elif detail.type == 'table': - tab = Ui_Table_Tab() - tab.setupUi(widget) - tab.table_wrap.setTitle(detail.title) - elif detail.type == 'text': - tab = Ui_Text_Tab() - tab.setupUi(widget) - tab.text_wrap.setTitle(detail.title) + # Fixme: If there is better way to print an image and scale it, fix this + tab.graph.setHtml(''.format(file)) - self.tabs_widget.addTab(widget, detail.name) + return widget - # Dialog controls - self.button_box.rejected.connect(self.reject) + def add_table_tab(self, detail, widget): + tab = Ui_Table_Tab() + tab.setupUi(widget) + tab.table_wrap.setTitle(detail.title) + + if detail.file: + file = os.path.join(get_user_data_directory(), 'data', detail.file) + tab.table = self.controller.populate_table_from_csv(tab.table, file) + else: + tab.text.setText('File {} not found'.format(detail.file)) + + return widget + + def add_text_tab(self, detail, widget): + tab = Ui_Text_Tab() + tab.setupUi(widget) + tab.text_wrap.setTitle(detail.title) + + if detail.file: + file = os.path.join(get_user_data_directory(), 'logs', detail.file) + tab.text = self.controller.populate_text_from_file(tab, file) + else: + tab.text.setText('File {} not found'.format(detail.file)) + + return widget From 5b1395dc2fcc2357c2e641b09d24d12db1c113b6 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 11 Oct 2018 15:15:42 +0300 Subject: [PATCH 0863/1846] Remove default DetailElements from Relative Orders --- dexbot/strategies/relative_orders.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index e5cd7bc74..d2af9d777 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -56,10 +56,7 @@ def configure(cls, return_base_config=True): @classmethod def configure_details(cls, include_default_tabs=True): - return StrategyBase.configure_details(include_default_tabs) + [ - DetailElement('graph', 'Profit', 'Profit for the past month'), - DetailElement('table', 'Buy orders', 'Open buy orders') - ] + return StrategyBase.configure_details(include_default_tabs) + [] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) From 9b3ce3f560ff7fa5eff7904ce96d0e2045f5c25d Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 11 Oct 2018 15:18:30 +0300 Subject: [PATCH 0864/1846] Fix details crash if .csv file is not found --- dexbot/controllers/worker_details_controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dexbot/controllers/worker_details_controller.py b/dexbot/controllers/worker_details_controller.py index 0abedadd8..c7728eeb4 100644 --- a/dexbot/controllers/worker_details_controller.py +++ b/dexbot/controllers/worker_details_controller.py @@ -38,6 +38,7 @@ def populate_table_from_csv(table, file, delimiter=';', first_item_header=True): rows = list(file_reader) except FileNotFoundError: print('File {} not found'.format(file)) + return table.setColumnCount(len(rows[0])) From 36d2fa4ca8f7d64211377b93618bcb4e53cff0c8 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 12 Oct 2018 07:58:28 +0300 Subject: [PATCH 0865/1846] Change dexbot version number to 0.7.2 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 508996598..2d2e50351 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.1' +VERSION = '0.7.2' AUTHOR = 'Codaone Oy' __version__ = VERSION From 5f87ab6c3addbd1baaf8d5e52e7f1904dc85a101 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 12 Oct 2018 08:07:41 +0300 Subject: [PATCH 0866/1846] Change dexbot version number to 0.7.3 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 2d2e50351..744316c1f 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.2' +VERSION = '0.7.3' AUTHOR = 'Codaone Oy' __version__ = VERSION From 9b7c69a8a92056935098249dc3a7bcce525a4151 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 12 Oct 2018 09:08:02 +0300 Subject: [PATCH 0867/1846] Remove some empty comment blocks --- dexbot/strategies/base.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index a380ad7d2..c7b05e517 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -244,12 +244,6 @@ def _callbackPlaceFillOrders(self, d): pass def _cancel_orders(self, orders): - """ - - :param orders: - :return: - """ - # Todo: Add documentation try: self.retry_action( self.bitshares.cancel, @@ -315,14 +309,6 @@ def balance(self, asset, fee_reservation=0): return balance def calculate_order_data(self, order, amount, price): - """ - - :param order: - :param amount: - :param price: - :return: - """ - # Todo: Add documentation quote_asset = Amount(amount, self.market['quote']['symbol']) order['quote'] = quote_asset order['price'] = price From 0788f1ea4323bc85dbf0542f430247bf7587034a Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 12 Oct 2018 09:08:30 +0300 Subject: [PATCH 0868/1846] Change dexbot version number to 0.7.4 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 744316c1f..548b5956e 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.3' +VERSION = '0.7.4' AUTHOR = 'Codaone Oy' __version__ = VERSION From 5b5302b6bb3ffefdb29e6fc9d2df4b30b8e036c7 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 12 Oct 2018 09:29:21 +0300 Subject: [PATCH 0869/1846] Change dexbot version number to 0.7.5 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 548b5956e..ad7d8e60e 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.4' +VERSION = '0.7.5' AUTHOR = 'Codaone Oy' __version__ = VERSION From a909be5b2c62002fe16f133e0948ccf55dca1db6 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 12 Oct 2018 10:21:57 +0300 Subject: [PATCH 0870/1846] Change dexbot version number to 0.7.6 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index ad7d8e60e..dd5c4a93c 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.5' +VERSION = '0.7.6' AUTHOR = 'Codaone Oy' __version__ = VERSION From a7cd4908da5f3a3c687d9485326f8ad82257eb61 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 12 Oct 2018 12:12:02 +0300 Subject: [PATCH 0871/1846] Change dexbot version number to 0.7.7 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index dd5c4a93c..39a5f59d3 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.6' +VERSION = '0.7.7' AUTHOR = 'Codaone Oy' __version__ = VERSION From f27971a97756a5a1e0e892341e0603c26ed3f853 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 12 Oct 2018 13:59:16 +0300 Subject: [PATCH 0872/1846] Add DetailElement and example to strategy_template --- dexbot/strategies/strategy_template.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/strategy_template.py b/dexbot/strategies/strategy_template.py index 211223e53..a1701e3e7 100644 --- a/dexbot/strategies/strategy_template.py +++ b/dexbot/strategies/strategy_template.py @@ -2,7 +2,7 @@ import math # Project imports -from dexbot.strategies.base import StrategyBase, ConfigElement +from dexbot.strategies.base import StrategyBase, ConfigElement, DetailElement from dexbot.qt_queue.idle_queue import idle_add # Third party imports @@ -61,6 +61,21 @@ def configure(cls, return_base_config=True): (0, 10000000, 8, '')), ] + @classmethod + def configure_details(cls, include_default_tabs=True): + """ This function defines the tabs for detailed view of the worker. Further documentation is found in base.py + + :param include_default_tabs: If default tabs are included as well + :return: List of DetailElement(s) + + NOTE: Add files to user data folders to see how they behave as an example. + """ + return StrategyBase.configure_details(include_default_tabs) + [ + DetailElement('graph', 'Graph', 'Graph', 'graph.jpg'), + DetailElement('table', 'Orders', 'Data from csv file', 'example.csv'), + DetailElement('text', 'Log', 'Log data', 'example.log') + ] + def __init__(self, *args, **kwargs): # Initializes StrategyBase class super().__init__(*args, **kwargs) From 1668f09a959b42072c2079a299c74c5fedaa9bad Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 12 Oct 2018 14:00:28 +0300 Subject: [PATCH 0873/1846] Change DetailElement documentation in base.py --- dexbot/strategies/base.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 97839b61e..d8680d95f 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -52,18 +52,21 @@ - Name: The name of the tab, shows at the top - Title: The title is shown inside the tab - File: Tabs can also show data from files, pass on the file name including the file extension - in strategy's `configure_details`. Below folders and representative file types that inside the folders. + in strategy's `configure_details`. + + Below folders and representative file types that inside the folders. + + Location File extensions + --------------------------- + dexbot/graphs .png, .jpg + dexbot/data .csv + dexbot/logs .log, .txt (.csv, will print as raw data) - Location : File extensions - dexbot/graphs : .png, .jpg - dexbot/data : .csv - dexbot/logs : .log, .txt - NOTE: To avoid conflicts with other custom strategies, when generating names for files, use slug or worker's - name when generating files. + name when generating files or create custom folders. Add relative path to 'file' parameter if file is in + custom folder inside default folders. Like shown below: - Example of graph tab: - `DetailElement('graph', 'Profit', 'Profit for the past month', 'profit_graph.jpg')` + `DetailElement('log', 'Worker log', 'Log of worker's actions', 'my_custom_folder/example_worker.log')` """ DetailElement = collections.namedtuple('DetailTab', 'type name title file') From c5a2a9f68679086455fcbf67e9e5b9936a127b10 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 12 Oct 2018 14:00:59 +0300 Subject: [PATCH 0874/1846] Change text_tab status message color to red --- dexbot/views/ui/tabs/text_tab.ui | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/dexbot/views/ui/tabs/text_tab.ui b/dexbot/views/ui/tabs/text_tab.ui index cb53f8d85..b91d86362 100644 --- a/dexbot/views/ui/tabs/text_tab.ui +++ b/dexbot/views/ui/tabs/text_tab.ui @@ -20,26 +20,17 @@ - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - + + true + + + color: red; + From 3b63c2e24340745c92d12772498f3001e9ecb969 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 12 Oct 2018 14:32:43 +0300 Subject: [PATCH 0875/1846] Change detail tabs margins --- dexbot/views/ui/tabs/graph_tab.ui | 15 --------------- dexbot/views/ui/tabs/table_tab.ui | 15 --------------- dexbot/views/ui/tabs/text_tab.ui | 3 --- 3 files changed, 33 deletions(-) diff --git a/dexbot/views/ui/tabs/graph_tab.ui b/dexbot/views/ui/tabs/graph_tab.ui index 77e336097..ee9716f80 100644 --- a/dexbot/views/ui/tabs/graph_tab.ui +++ b/dexbot/views/ui/tabs/graph_tab.ui @@ -20,21 +20,6 @@ - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - diff --git a/dexbot/views/ui/tabs/table_tab.ui b/dexbot/views/ui/tabs/table_tab.ui index 84473f778..1084c5a21 100644 --- a/dexbot/views/ui/tabs/table_tab.ui +++ b/dexbot/views/ui/tabs/table_tab.ui @@ -20,21 +20,6 @@ - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - diff --git a/dexbot/views/ui/tabs/text_tab.ui b/dexbot/views/ui/tabs/text_tab.ui index b91d86362..c27ec5826 100644 --- a/dexbot/views/ui/tabs/text_tab.ui +++ b/dexbot/views/ui/tabs/text_tab.ui @@ -28,9 +28,6 @@ true - - color: red; - From 68e0126da680ceda1e341aad0b58017bb3cb6da9 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 12 Oct 2018 14:33:03 +0300 Subject: [PATCH 0876/1846] Add status messages to tab file loading --- .../controllers/worker_details_controller.py | 48 ++++++++++++++----- dexbot/views/worker_details.py | 18 ++----- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/dexbot/controllers/worker_details_controller.py b/dexbot/controllers/worker_details_controller.py index c7728eeb4..f6bfadda2 100644 --- a/dexbot/controllers/worker_details_controller.py +++ b/dexbot/controllers/worker_details_controller.py @@ -1,4 +1,5 @@ import csv +import os from PyQt5.QtWidgets import QTableWidgetItem from PyQt5.QtGui import QTextCursor @@ -30,17 +31,26 @@ def initialize_worker_data(self): self.view.market.setText(self.config.get('market')) self.view.fee_asset.setText(self.config.get('fee_asset')) - @staticmethod - def populate_table_from_csv(table, file, delimiter=';', first_item_header=True): + def add_graph(self, tab, file): + # Fixme: If there is better way to print an image and scale it, fix this + if os.path.isfile(file): + tab.graph.setHtml(''.format(file)) + self.status_file_loaded(tab, file) + else: + self.status_file_not_found(tab, file) + + return tab.graph + + def populate_table_from_csv(self, tab, file, delimiter=';', first_item_header=True): try: with open(file, 'r') as csv_file: file_reader = csv.reader(csv_file, delimiter=delimiter) rows = list(file_reader) except FileNotFoundError: - print('File {} not found'.format(file)) + self.status_file_not_found(tab, file) return - table.setColumnCount(len(rows[0])) + tab.table.setColumnCount(len(rows[0])) # Set headers if first_item_header: @@ -48,24 +58,36 @@ def populate_table_from_csv(table, file, delimiter=';', first_item_header=True): for header_index, header in enumerate(headers): item = QTableWidgetItem() item.setText(header) - table.setHorizontalHeaderItem(header_index, item) + tab.table.setHorizontalHeaderItem(header_index, item) # Set rows data - table.setRowCount(len(rows)) + tab.table.setRowCount(len(rows)) for row_index, row in enumerate(rows): for column_index, column in enumerate(row): item = QTableWidgetItem() item.setText(column) - table.setItem(row_index, column_index, item) + tab.table.setItem(row_index, column_index, item) - return table + self.status_file_loaded(tab, file) - @staticmethod - def populate_text_from_file(tab, file): + return tab.table + + def populate_text_from_file(self, tab, file): try: tab.text.setPlainText(open(file).read()) tab.text.moveCursor(QTextCursor.End) - - return tab + self.status_file_loaded(tab, file) + return tab.text except FileNotFoundError: - tab.status_label.setText('File \'{}\' not found'.format(file)) + self.status_file_not_found(tab, file) + return + + @staticmethod + def status_file_not_found(tab, file): + tab.status_label.setStyleSheet('color: red;') + return tab.status_label.setText('File \'{}\' not found'.format(file)) + + @staticmethod + def status_file_loaded(tab, file): + tab.status_label.setStyleSheet('') + return tab.status_label.setText('File \'{}\' loaded'.format(file)) diff --git a/dexbot/views/worker_details.py b/dexbot/views/worker_details.py index 71f48874f..5bb7c97ad 100644 --- a/dexbot/views/worker_details.py +++ b/dexbot/views/worker_details.py @@ -55,18 +55,14 @@ def add_tabs(self, details): self.tabs_widget.addTab(widget, detail.name) - @staticmethod - def add_graph_tab(detail, widget): + def add_graph_tab(self, detail, widget): tab = Ui_Graph_Tab() tab.setupUi(widget) tab.graph_wrap.setTitle(detail.title) - # Get image path - directory = get_user_data_directory() - file = os.path.join(directory, 'graphs', detail.file) - - # Fixme: If there is better way to print an image and scale it, fix this - tab.graph.setHtml(''.format(file)) + if detail.file: + file = os.path.join(get_user_data_directory(), 'graphs', detail.file) + tab.table = self.controller.add_graph(tab, file) return widget @@ -77,9 +73,7 @@ def add_table_tab(self, detail, widget): if detail.file: file = os.path.join(get_user_data_directory(), 'data', detail.file) - tab.table = self.controller.populate_table_from_csv(tab.table, file) - else: - tab.text.setText('File {} not found'.format(detail.file)) + tab.table = self.controller.populate_table_from_csv(tab, file) return widget @@ -91,7 +85,5 @@ def add_text_tab(self, detail, widget): if detail.file: file = os.path.join(get_user_data_directory(), 'logs', detail.file) tab.text = self.controller.populate_text_from_file(tab, file) - else: - tab.text.setText('File {} not found'.format(detail.file)) return widget From 1fdaa77ae7bfb4e89dcddbebfc8cae5b24cc8572 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 12 Oct 2018 14:50:29 +0300 Subject: [PATCH 0877/1846] Change dexbot version number to 0.7.8 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 39a5f59d3..3a31130f9 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.7' +VERSION = '0.7.8' AUTHOR = 'Codaone Oy' __version__ = VERSION From e88d5a50ef31aaa276e5ff1367d0b70a9720c7ca Mon Sep 17 00:00:00 2001 From: joelva Date: Sat, 13 Oct 2018 10:57:22 +0300 Subject: [PATCH 0878/1846] Fix windows build bug --- gui.spec | 4 ++++ pyuic.json | 1 + 2 files changed, 5 insertions(+) diff --git a/gui.spec b/gui.spec index 753169ffe..eaaa482ce 100644 --- a/gui.spec +++ b/gui.spec @@ -12,6 +12,10 @@ hiddenimports_strategies = [ 'dexbot.strategies.staggered_orders', 'dexbot.strategies.storagedemo', 'dexbot.strategies.walls', + 'dexbot.views.ui.tabs', + 'dexbot.views.ui.tabs.graph_tab.ui', + 'dexbot.views.ui.tabs.table_tab.ui', + 'dexbot.views.ui.tabs.text_tab.ui', 'dexbot.views.ui.forms', 'dexbot.views.ui.forms.relative_orders_widget_ui', 'dexbot.views.ui.forms.staggered_orders_widget_ui', diff --git a/pyuic.json b/pyuic.json index 09b83e80e..266116fda 100644 --- a/pyuic.json +++ b/pyuic.json @@ -1,6 +1,7 @@ { "files": [ ["dexbot/views/ui/*.ui", "dexbot/views/ui/"], + ["dexbot/views/ui/tabs/*.ui", "dexbot/views/ui/tabs"], ["dexbot/views/ui/forms/*.ui", "dexbot/views/ui/forms"], ["dexbot/resources/*.qrc", "dexbot/resources/"] ], From ee247c99089afc3aa63dbb4577d3e8a72fa6fed8 Mon Sep 17 00:00:00 2001 From: joelvai Date: Sat, 13 Oct 2018 11:12:50 +0300 Subject: [PATCH 0879/1846] Change dexbot version to 0.7.9 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 3a31130f9..e757db62d 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.8' +VERSION = '0.7.9' AUTHOR = 'Codaone Oy' __version__ = VERSION From 0a6a4d94578c93a59da20f1a9c39f20395dde3bd Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 15 Oct 2018 20:51:46 +0500 Subject: [PATCH 0880/1846] Fix precision in place_lowest_buy_order() Regression was introduced in d77b708da25f2f0b298fe257c1c7acbbc64b4a03 --- dexbot/strategies/staggered_orders.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 24e48fcbf..e7463c2dd 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1232,9 +1232,6 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p price = previous_price amount_quote = amount_base / price - precision = self.market['quote']['precision'] - amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) - elif self.mode == 'valley' or self.mode == 'buy_slope': orders_count = 0 while price >= self.lower_bound: @@ -1250,6 +1247,9 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p """ amount_quote = amount_quote / (1 + self.increment / 100) + precision = self.market['quote']['precision'] + amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) + if place_order: self.market_buy(amount_quote, price) else: From f5776a52ebd7033de5587a0457182f4505e51162 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 17 Oct 2018 15:04:59 +0500 Subject: [PATCH 0881/1846] Avoid passing None to get_updated_limit_order Fixes #348. --- dexbot/strategies/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index f4d8cc68c..c8869387a 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -934,6 +934,11 @@ def get_updated_order(self, order_id): # We are using direct rpc call here because passing an Order object to self.get_updated_limit_order() give us # weird error "Object of type 'BitShares' is not JSON serializable" order = self.bitshares.rpc.get_objects([order_id])[0] + + # Do not try to continue whether there is no order in the blockchain + if not order: + return None + updated_order = self.get_updated_limit_order(order) return Order(updated_order, bitshares_instance=self.bitshares) From 95ebc3dcc826d64ab59ad9d4e385a33ff10ff03d Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 17 Oct 2018 19:58:00 +0500 Subject: [PATCH 0882/1846] Add function to calculate order creation fee for an asset --- dexbot/strategies/staggered_orders.py | 37 +++++++++++++++++++-------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index e7463c2dd..19c0ef0f6 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta from bitshares.market import Market from bitshares.asset import Asset +from bitshares.dex import Dex from dexbot.basestrategy import BaseStrategy, ConfigElement from dexbot.strategies.base import StrategyBase, DetailElement @@ -130,6 +131,9 @@ def __init__(self, *args, **kwargs): self.base_balance_history = [1, 2, 3] self.cached_orders = None + # Dex instance used to get different fees for the market + self.dex = Dex(self.bitshares) + # Order expiration time self.expiration = 60 * 60 * 24 * 365 * 5 self.start = datetime.now() @@ -309,6 +313,25 @@ def log_maintenance_time(self): delta = datetime.now() - self.start self.log.debug('Maintenance execution took: {:.2f} seconds'.format(delta.total_seconds())) + def get_order_creation_fee(self, fee_asset): + """ Returns the cost of creating an order in the asset specified + + :param fee_asset: QUOTE, BASE, BTS, or any other + :return: + """ + # Get fee + fees = self.dex.returnFees() + limit_order_create = fees['limit_order_create'] + + if fee_asset['id'] == '1.3.0': + # Fee asset is BTS, so no further calculations are needed + return limit_order_create['fee'] + else: + # Determine how many fee_asset is needed for core-exchange + temp_market = Market(base=fee_asset, quote=Asset('1.3.0')) + core_exchange_rate = temp_market.ticker()['core_exchange_rate'] + return limit_order_create['fee'] * core_exchange_rate['base']['amount'] + def refresh_balances(self, total_balances=True, use_cached_orders=False): """ This function is used to refresh account balances :param bool | total_balances: refresh total balance or skip it @@ -320,17 +343,9 @@ def refresh_balances(self, total_balances=True, use_cached_orders=False): self.base_balance = account_balances['base'] self.quote_balance = account_balances['quote'] - # Todo: order_creation_fee(BTS) = 0.01 for now. Use OperationsFee in the feature. - # Reserve fee for 200 orders - fee_reserve = 0.01 * 200 - if self.fee_asset['id'] == '1.3.0': - # Fee asset is BTS, so no further calculations are needed - fee_reserve = fee_reserve - else: - # Determine how many fee_asset is needed for core-exchange - temp_market = Market(base=self.fee_asset, quote=Asset('1.3.0')) - core_exchange_rate = temp_market.ticker()['core_exchange_rate'] - fee_reserve = fee_reserve * core_exchange_rate['base']['amount'] + # Reserve fees for N orders + reserve_num_orders = 200 + fee_reserve = reserve_num_orders * self.get_order_creation_fee(self.fee_asset) # Finally, reserve only required asset if self.fee_asset['id'] == self.market['base']['id']: From 9b22fbdfac92af48b04547263e627f88cd164e29 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 17 Oct 2018 21:14:02 +0500 Subject: [PATCH 0883/1846] Adjust precision in log messages --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 19c0ef0f6..ce90a43d2 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1103,7 +1103,7 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente if price > self.upper_bound: self.log.info( 'Not placing highest sell order because price will exceed higher bound. Market center ' - 'price: {:.8f}, closest order price: {:.8f}, upper_bound: {}' + 'price: {:.8f}, closest order price: {:.8f}, upper_bound: {:.8f}' .format(market_center_price, price, self.upper_bound)) return @@ -1207,7 +1207,7 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p if price < self.lower_bound: self.log.info( 'Not placing lowest buy order because price will exceed lower bound. Market center price: ' - '{:.8f}, closest order price: {:.8f}, lower bound: {}' + '{:.8f}, closest order price: {:.8f}, lower bound: {:.8f}' .format(market_center_price, price, self.lower_bound)) return From b5622d494b42d5c7432d33c04299af003c954126 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 17 Oct 2018 21:26:34 +0500 Subject: [PATCH 0884/1846] Precisely calculate initial orders sizes Calculate orders sizes getting in mind further fees paid during orders placement. --- dexbot/strategies/staggered_orders.py | 80 ++++++++++++++++++--------- 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index ce90a43d2..510739e8f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1107,39 +1107,44 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente .format(market_center_price, price, self.upper_bound)) return + if self.fee_asset['id'] == self.market['quote']['id']: + fee = self.get_order_creation_fee(self.fee_asset) + buy_orders_count = self.calc_buy_orders_count(price=price) + sell_orders_count = self.calc_sell_orders_count(price=price) + # Exclude all further fees from avail balance + quote_balance = quote_balance - fee * (buy_orders_count + sell_orders_count) + amount_quote = 0 previous_price = 0 if self.mode == 'mountain' or self.mode == 'buy_slope': previous_price = price orders_sum = 0 amount = quote_balance['amount'] * self.increment - previous_amount = amount while price <= self.upper_bound: - orders_sum += previous_amount previous_price = price previous_amount = amount + orders_sum += previous_amount price = price * (1 + self.increment) amount = amount / (1 + self.increment) price = previous_price - amount_quote = previous_amount * (self.quote_total_balance / orders_sum) * (1 + self.increment * 0.75) + amount_quote = previous_amount * (quote_balance['amount'] / orders_sum) - if self.mode == 'neutral': + elif self.mode == 'neutral': previous_price = price orders_sum = 0 - amount = quote_balance['amount'] * math.sqrt(1 + self.increment) - previous_amount = amount + amount = quote_balance['amount'] * (math.sqrt(1 + self.increment) - 1) while price <= self.upper_bound: - orders_sum += previous_amount previous_price = price previous_amount = amount - price = price * math.sqrt(1 + self.increment) + orders_sum += previous_amount + price = price * (1 + self.increment) amount = amount / math.sqrt(1 + self.increment) price = previous_price - amount_quote = previous_amount * (self.quote_total_balance / orders_sum) * (1 + self.increment * 0.75) + amount_quote = previous_amount * (quote_balance['amount'] / orders_sum) elif self.mode == 'valley' or self.mode == 'sell_slope': orders_count = 0 @@ -1149,9 +1154,7 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente price = price * (1 + self.increment) price = previous_price - amount_quote = quote_balance / orders_count - # Slightly reduce order amount to avoid rounding issues - amount_quote = amount_quote / (1 + self.increment / 100) + amount_quote = quote_balance['amount'] / orders_count precision = self.market['quote']['precision'] amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) @@ -1211,39 +1214,44 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p .format(market_center_price, price, self.lower_bound)) return + if self.fee_asset['id'] == self.market['base']['id']: + fee = self.get_order_creation_fee(self.fee_asset) + buy_orders_count = self.calc_buy_orders_count(price=price) + sell_orders_count = self.calc_sell_orders_count(price=price) + # Exclude all further fees from avail balance + base_balance = base_balance - fee * (buy_orders_count + sell_orders_count) + amount_quote = 0 previous_price = 0 if self.mode == 'mountain' or self.mode == 'sell_slope': previous_price = price orders_sum = 0 amount = base_balance['amount'] * self.increment - previous_amount = amount while price >= self.lower_bound: - orders_sum += previous_amount previous_price = price previous_amount = amount + orders_sum += previous_amount price = price / (1 + self.increment) amount = amount / (1 + self.increment) - amount_base = previous_amount * (self.base_total_balance / orders_sum) * (1 + self.increment * 0.75) + amount_base = previous_amount * (base_balance['amount'] / orders_sum) price = previous_price amount_quote = amount_base / price elif self.mode == 'neutral': previous_price = price orders_sum = 0 - amount = base_balance['amount'] * sqrt(1 + self.increment) - previous_amount = amount + amount = base_balance['amount'] * (math.sqrt(1 + self.increment) - 1) while price >= self.lower_bound: - orders_sum += previous_amount previous_price = price previous_amount = amount - price = price / sqrt(1 + self.increment) - amount = amount / sqrt(1 + self.increment) + orders_sum += previous_amount + price = price / (1 + self.increment) + amount = amount / math.sqrt(1 + self.increment) - amount_base = previous_amount * (self.base_total_balance / orders_sum) * (1 + self.increment * 0.75) + amount_base = previous_amount * (base_balance['amount'] / orders_sum) price = previous_price amount_quote = amount_base / price @@ -1255,12 +1263,8 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p orders_count += 1 price = previous_price - amount_base = self.base_total_balance / orders_count + amount_base = base_balance['amount'] / orders_count amount_quote = amount_base / price - """ Slightly reduce order amount to avoid rounding issues AND to leave some free balance after initial - allocation to not turn bootstrap off prematurely - """ - amount_quote = amount_quote / (1 + self.increment / 100) precision = self.market['quote']['precision'] amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) @@ -1270,6 +1274,30 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p else: return {"amount": amount_quote, "price": price} + def calc_buy_orders_count(self, price=None): + """ Calculate number of buy orders to place between lower_bound and specified price + + :param float | price: Highest buy price bound + :return int | count: Returns number of orders + """ + orders_count = 0 + while price >= self.lower_bound: + orders_count += 1 + price = price / (1 + self.increment) + return orders_count + + def calc_sell_orders_count(self, price=None): + """ Calculate number of sell orders to place between upper_bound and specified price + + :param float | price: Lowest sell price bound + :return int | count: Returns number of orders + """ + orders_count = 0 + while price <= self.upper_bound: + orders_count += 1 + price = price * (1 + self.increment) + return orders_count + def error(self, *args, **kwargs): self.disabled = True From c6a0b1f65b0e2b9513cb8700d8f11896d9b9e2dd Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 17 Oct 2018 21:28:07 +0500 Subject: [PATCH 0885/1846] Remove unneeded bootstrap turn off switch This switch is not needed anymore because we are calculating orders sized precisely. --- dexbot/strategies/staggered_orders.py | 29 +-------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 510739e8f..757a118c1 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -480,34 +480,7 @@ def allocate_asset(self, asset, asset_balance): which side order (buy or sell) should be placed first. So, when placing two closer orders from both sides, spread will be no less than `target_spread - increment`, thus not making any loss. """ - if opposite_balance <= opposite_threshold and self.bootstrapping and opposite_orders: - """ During the bootstrap we're fist placing orders of some amounts, than we are reaching target - spread and then turning bootstrap flag off and starting to allocate remaining balance by - gradually increasing order sizes. After bootstrap is complete and following order size - increase is complete too, we will not have available balance. - - When we have a different amount of assets (for example, 100 USD for base and 1 BTC for - quote), the orders on the one size will be bigger than at the opposite. - - During the bootstrap we are not allowing to place orders with limited amount by opposite - order. Bootstrap is designed to place orders of the same size. But, when the bootstrap is - done, we are beginning to limit new orders by opposite side orders. We need this to stay in - game when orders on the lower side gets filled. Because they are less than big-side orders, - we cannot just place another big order on the big side. So we are limiting the big-side - order to amount of a low-side one! - - Normally we are turning bootstrap off after initial allocation is done and we're beginning - to distribute remaining funds. But, whether we will restart the bot after size increase was - done, we have no chance to know if bootstrap was done or not. This is where this check comes - in! The situation when the target spread is not reached, but we have some available balance - on the one side and not have any free balance of the other side, clearly says to us that an - order from lower-side was filled! Thus, we can safely turn bootstrap off and thus place an - order limited in size by opposite-side order. - """ - self.log.debug('Turning bootstrapping off: actual_spread > target_spread, and not having ' - 'opposite-side balance') - self.bootstrapping = False - elif (self.bootstrapping and + if (self.bootstrapping and self.base_balance_history[2] == self.base_balance_history[0] and self.quote_balance_history[2] == self.quote_balance_history[0]): # Turn off bootstrap mode whether we're didn't allocated assets during previous 3 maintenance From fcaa073db142ce9fe93cb5f95ce64152cc04b6d6 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 17 Oct 2018 21:31:02 +0500 Subject: [PATCH 0886/1846] Update comment --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 757a118c1..d64908147 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1148,13 +1148,13 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p Mountain: For asset to be allocated (base for buy and quote for sell orders) - First order = balance * increment + First order (furthest) = balance * increment Next order = previous order / (1 + increment) Repeat until last order. Neutral: For asset to be allocated (base for buy and quote for sell orders) - First order = balance * (sqrt(1 + increment) - 1) + First order (furthest) = balance * (sqrt(1 + increment) - 1) Next order = previous order / sqrt(1 + increment) Repeat until last order From ac3b923a2a59342b16aa09a54068e51367f0922a Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 17 Oct 2018 23:24:11 +0500 Subject: [PATCH 0887/1846] Implement missing parts for neutral mode --- dexbot/strategies/staggered_orders.py | 108 +++++++++++--------------- 1 file changed, 47 insertions(+), 61 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index d64908147..c5753616c 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -21,7 +21,8 @@ def configure(cls, return_base_config=True): - Sell orders same BASE Neutral: - - All orders lower_order_quote / sqrt(1 + increment) + - Buy orders lower_order_base * sqrt(1 + increment) + - Sell orders higher_order_quote * sqrt(1 + increment) Valley: - Buy orders same BASE @@ -36,7 +37,7 @@ def configure(cls, return_base_config=True): # Todo: - Add other modes modes = [ ('mountain', 'Mountain'), - # ('neutral', 'Neutral'), + ('neutral', 'Neutral'), ('valley', 'Valley'), ('buy_slope', 'Buy Slope'), ('sell_slope', 'Sell Slope') @@ -503,8 +504,10 @@ def allocate_asset(self, asset, asset_balance): self.log.debug('Limiting {} order by opposite order: {} {}' .format(order_type, own_asset_limit, symbol)) elif self.mode == 'neutral': - # todo: implement - pass + opposite_asset_limit = closest_opposite_order['base']['amount'] * math.sqrt(1 + self.increment) + own_asset_limit = None + self.log.debug('Limiting {} order by opposite order: {} {}'.format( + order_type, opposite_asset_limit, symbol)) elif (self.mode == 'valley' or (self.mode == 'buy_slope' and asset == 'quote') or (self.mode == 'sell_slope' and asset == 'base')): @@ -785,7 +788,6 @@ def increase_order_sizes(self, asset, asset_balance, orders): return elif self.mode == 'neutral': - # todo: convert to neutral if asset == 'quote': total_balance = self.quote_total_balance order_type = 'sell' @@ -793,81 +795,63 @@ def increase_order_sizes(self, asset, asset_balance, orders): total_balance = self.base_total_balance order_type = 'buy' - # Get orders and amounts to be compared. Note: orders are sorted from low price to high + orders_count = len(orders) + orders = list(reversed(orders)) + for order in orders: order_index = orders.index(order) order_amount = order['base']['amount'] - # This check prevents choosing order with index lower than the list length - if order_index == 0: - # In case checking the first order, use the same order, but increased by 1 increment - # This allows our closest order amount exceed highest opposite-side order amount - closer_order = order - closer_bound = closer_order['base']['amount'] * (1 + self.increment) + if order_index + 1 < orders_count: + # Closer order is an order which one-step closer to the center + closer_order = orders[order_index + 1] + closer_order_bound = closer_order['base']['amount'] / math.sqrt(1 + self.increment) else: - closer_order = orders[order_index - 1] - closer_bound = closer_order['base']['amount'] + closer_order_bound = order_amount * math.sqrt(1 + self.increment) - # This check prevents choosing order with index higher than the list length - if order_index + 1 < len(orders): - # Current order is a not furthest order - further_order = orders[order_index + 1] - is_least_order = False - else: - # Current order is furthest order - further_order = orders[order_index] - is_least_order = True + new_orders_sum = 0 + amount = order_amount + for o in orders: + new_orders_sum += amount + amount = amount / math.sqrt(1 + self.increment) + new_amount = order_amount * (total_balance / new_orders_sum) - further_bound = further_order['base']['amount'] * (1 + self.increment) + if new_amount > closer_order_bound: + # Maximize order up to max possible amount if we can + closer_order_bound = new_amount - if (further_bound > order_amount * (1 + self.increment / 10) < closer_bound and - further_bound - order_amount >= order_amount * self.increment / 2): - # Calculate new order size and place the order to the market - new_order_amount = further_bound + self.log.debug('order amount: {:.8f}, closer_order_bound: {:.8f}'.format(order_amount, closer_order_bound)) + self.log.debug('diff: {:.8f}, half of increase: {:.8f}'.format(closer_order_bound - order_amount, order_amount * (math.sqrt(1 + self.increment) - 1) / 2)) - if is_least_order: - new_orders_sum = 0 - amount = order_amount - for o in orders: - amount = amount * (1 + self.increment) - new_orders_sum += amount - # To reduce allocation rounds, increase furthest order more - new_order_amount = order_amount * (total_balance / new_orders_sum) \ - * (1 + self.increment * 0.75) + if (order_amount * (1 + self.increment / 10) < closer_order_bound and + closer_order_bound - order_amount >= order_amount * (math.sqrt(1 + self.increment) - 1) / 2): - if new_order_amount < closer_bound: - """ This is for situations when calculated new_order_amount is not big enough to - allocate all funds. Use partial-increment increase, so we'll got at least one full - increase round. Whether we will just use `new_order_amount = further_bound`, we will - get less than one full allocation round, thus leaving closest-to-center order not - increased. - """ - new_order_amount = closer_bound / (1 + self.increment * 0.2) + amount_base = closer_order_bound + + # Limit order to available balance + if asset_balance < amount_base - order_amount: + amount_base = order_amount + asset_balance['amount'] + self.log.info('Limiting new order to avail asset balance: {:.8f} {}' + .format(amount_base, asset_balance['symbol'])) - # Limit sell order to available balance - if asset_balance < new_order_amount - order_amount: - new_order_amount = order_amount + asset_balance['amount'] - self.log.info('Limiting new {} order to avail asset balance: {:.8f} {}' - .format(order_type, new_order_amount, asset_balance['symbol'])) - quote_amount = 0 price = 0 if asset == 'quote': price = (order['price'] ** -1) - quote_amount = new_order_amount elif asset == 'base': price = order['price'] - quote_amount = new_order_amount / price - - self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}, amount: {}, price: {:.8f}' + self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}, amount: {:.8f}, price: {:.8f}' .format(order_type, self.mode, order_amount, price)) self.cancel(order) + if asset == 'quote': - self.market_sell(quote_amount, price) + self.market_sell(amount_base, price) elif asset == 'base': - self.market_buy(quote_amount, price) - # Only one increase at a time. This prevents running more than one increment round simultaneously + amount_quote = amount_base / price + self.market_buy(amount_quote, price) + # One increase at a time. This prevents running more than one increment round simultaneously. return + return None def check_partial_fill(self, order): @@ -947,7 +931,8 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False own_asset_amount = order['base']['amount'] opposite_asset_amount = own_asset_amount / price elif self.mode == 'neutral': - # todo: implement + own_asset_amount = order['base']['amount'] * math.sqrt(1 + self.increment) + opposite_asset_amount = own_asset_amount / price # Apply limits. Limit order only whether passed limit is less than expected order size if own_asset_limit and own_asset_limit < own_asset_amount: @@ -1021,11 +1006,12 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals if self.mode == 'mountain' or self.mode == 'buy_slope': opposite_asset_amount = order['quote']['amount'] own_asset_amount = opposite_asset_amount * price - if self.mode == 'neutral': - # todo: implement elif self.mode == 'valley' or self.mode == 'buy_slope': own_asset_amount = order['base']['amount'] opposite_asset_amount = own_asset_amount / price + elif self.mode == 'neutral': + own_asset_amount = order['base']['amount'] / math.sqrt(1 + self.increment) + opposite_asset_amount = own_asset_amount / price limiter = 0 quote_amount = 0 From 3c01ba3667521b41d93e93b58401cf1b0c12cb73 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 18 Oct 2018 00:35:10 +0500 Subject: [PATCH 0888/1846] Add check for partiall fill in increase_order_sizes() Fixes #347 --- dexbot/strategies/staggered_orders.py | 63 +++++++++++++++++++-------- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c5753616c..dea0bbd8c 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -99,6 +99,7 @@ def __init__(self, *args, **kwargs): self.increment = self.worker['increment'] / 100 self.upper_bound = self.worker['upper_bound'] self.lower_bound = self.worker['lower_bound'] + # This fill threshold prevents too often orders replacements draining fee_asset self.partial_fill_threshold = self.increment / 10 self.is_instant_fill_enabled = self.worker.get('instant_fill', True) self.is_center_price_dynamic = self.worker['center_price_dynamic'] @@ -549,20 +550,7 @@ def allocate_asset(self, asset, asset_balance): self.log.debug('Placing further order than current furthest {} order'.format(order_type)) self.place_further_order(asset, furthest_own_order, allow_partial=True) else: - # Make sure we have enough balance to replace partially filled order - if asset_balance + closest_own_order['for_sale']['amount'] >= closest_own_order['base']['amount']: - # Cancel closest order and immediately replace it with new one. - self.log.info('Replacing partially filled {} order'.format(order_type)) - self.cancel(closest_own_order) - if asset == 'base': - self.market_buy(closest_own_order['quote']['amount'], closest_own_order['price']) - elif asset == 'quote': - price = closest_own_order['price'] ** -1 - self.market_sell(closest_own_order['base']['amount'], price) - if self.returnOrderId: - self.refresh_balances(total_balances=False) - else: - self.log.debug('Not replacing partially filled order because there is not enough funds') + self.replace_partially_filled_order(closest_own_order) else: # Place first buy order as close to the lower bound as possible self.bootstrapping = True @@ -609,6 +597,12 @@ def increase_order_sizes(self, asset, asset_balance, orders): total_balance = 0 order_type = '' + # First of all, make sure all orders are not partially filled + for order in orders: + if not self.check_partial_fill(order, fill_threshold=0): + self.replace_partially_filled_order(order) + return + # Mountain mode: if (self.mode == 'mountain' or (self.mode == 'buy_slope' and asset == 'quote') or @@ -854,22 +848,57 @@ def increase_order_sizes(self, asset, asset_balance, orders): return None - def check_partial_fill(self, order): + def check_partial_fill(self, order, fill_threshold=None): """ Checks whether order was partially filled it needs to be replaced - :param order: Order closest to the center price from buy or sell side + :param dict | order: Order closest to the center price from buy or sell side + :param float | fill_threshold: Order fill threshold, relative :return: bool | True = Order is correct size or within the threshold False = Order is not right size """ + if fill_threshold == None: + fill_threshold = self.partial_fill_threshold + if order['for_sale']['amount'] != order['base']['amount']: diff_abs = order['base']['amount'] - order['for_sale']['amount'] diff_rel = diff_abs / order['base']['amount'] - if diff_rel >= self.partial_fill_threshold: + if diff_rel > fill_threshold: self.log.debug('Partially filled order: {} @ {:.8f}, filled: {:.2%}'.format( order['base']['amount'], order['price'], diff_rel)) return False return True + def replace_partially_filled_order(self, order): + """ Replace partially filled order + + :param order: Order instance + """ + order_type = '' + asset_balance = None + + if order['base']['symbol'] == self.market['base']['symbol']: + asset_balance = self.base_balance + order_type = 'buy' + else: + asset_balance = self.quote_balance + order_type = 'sell' + + # Make sure we have enough balance to replace partially filled order + if asset_balance + order['for_sale']['amount'] >= order['base']['amount']: + # Cancel closest order and immediately replace it with new one. + self.log.info('Replacing partially filled {} order'.format(order_type)) + self.cancel(order) + if order_type == 'buy': + self.market_buy(order['quote']['amount'], order['price']) + elif order_type == 'sell': + price = order['price'] ** -1 + self.market_sell(order['base']['amount'], price) + if self.returnOrderId: + self.refresh_balances(total_balances=False) + else: + self.log.debug('Not replacing partially filled {} order because there is not enough funds' + .format(order_type)) + def place_closer_order(self, asset, order, place_order=True, allow_partial=False, own_asset_limit=None, opposite_asset_limit=None): """ Place order closer to the center From efd5d616010ec32d4dc71a99c503524c763b18bc Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 18 Oct 2018 00:48:09 +0500 Subject: [PATCH 0889/1846] Fix price arg in calc_xxx_orders_count() --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index dea0bbd8c..3c6f8ee80 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1262,7 +1262,7 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p else: return {"amount": amount_quote, "price": price} - def calc_buy_orders_count(self, price=None): + def calc_buy_orders_count(self, price): """ Calculate number of buy orders to place between lower_bound and specified price :param float | price: Highest buy price bound @@ -1274,7 +1274,7 @@ def calc_buy_orders_count(self, price=None): price = price / (1 + self.increment) return orders_count - def calc_sell_orders_count(self, price=None): + def calc_sell_orders_count(self, price): """ Calculate number of sell orders to place between upper_bound and specified price :param float | price: Lowest sell price bound From 39adb953615bbde89051daef351db54ff9be3cf6 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 18 Oct 2018 13:16:18 +0300 Subject: [PATCH 0890/1846] Change commenting in base.py --- dexbot/strategies/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index c8869387a..3a4f65a4a 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -919,7 +919,7 @@ def get_own_spread(self): def get_updated_order(self, order_id): """ Tries to get the updated order from the API. Returns None if the order doesn't exist - :param str|dict order: blockchain Order object or id of the order + :param str|dict order_id: blockchain Order object or id of the order """ if isinstance(order_id, dict): order_id = order_id['id'] @@ -931,8 +931,8 @@ def get_updated_order(self, order_id): order = limit_order break else: - # We are using direct rpc call here because passing an Order object to self.get_updated_limit_order() give us - # weird error "Object of type 'BitShares' is not JSON serializable" + # We are using direct rpc call here because passing an Order object to self.get_updated_limit_order() give + # us weird error "Object of type 'BitShares' is not JSON serializable" order = self.bitshares.rpc.get_objects([order_id])[0] # Do not try to continue whether there is no order in the blockchain From c51ab098f241b4dd903801c9e179819565cc9775 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 18 Oct 2018 13:16:36 +0300 Subject: [PATCH 0891/1846] Add NoneType fix to BaseStrategy as well --- dexbot/basestrategy.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 5e0f10047..3457506bb 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -401,6 +401,10 @@ def get_updated_order(self, order_id): else: return order + # Do not try to continue whether there is no order in the blockchain + if not order: + return None + order = self.get_updated_limit_order(order) return Order(order, bitshares_instance=self.bitshares) From b62aa653231a961aed353d0443bbf7f826bf666b Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 18 Oct 2018 13:17:24 +0300 Subject: [PATCH 0892/1846] Change dexbot version number to 0.7.10 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index e757db62d..4fc7a3997 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.9' +VERSION = '0.7.10' AUTHOR = 'Codaone Oy' __version__ = VERSION From 75b61ee5af215fb3c3f467bf7f306dc23a9f0733 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 18 Oct 2018 14:12:37 +0300 Subject: [PATCH 0893/1846] Update pybitshares version to 0.2.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f02964afd..685aa047b 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ cmd_class = {} console_scripts = ['dexbot-cli = dexbot.cli:main'] install_requires = [ - 'bitshares==0.1.22', + 'bitshares==0.2.0', 'uptick>=0.1.9', 'click', 'sqlalchemy', From fff897645223d97fbb0c55b51d3d755531a3f567 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 18 Oct 2018 17:00:15 +0500 Subject: [PATCH 0894/1846] Move fee calculations into base strategies Fixes #351 --- dexbot/basestrategy.py | 45 +++++++++++++++++++++++++++ dexbot/strategies/base.py | 31 ++++++++++++++---- dexbot/strategies/staggered_orders.py | 19 ----------- 3 files changed, 70 insertions(+), 25 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 3457506bb..865640999 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -197,6 +197,9 @@ def __init__( # buy/sell actions will return order id by default self.returnOrderId = 'head' + # CER cache + self.core_exchange_rate = None + # A private logger that adds worker identify data to the LogRecord self.log = logging.LoggerAdapter( logging.getLogger('dexbot.per_worker'), @@ -364,6 +367,28 @@ def sort_orders(orders, sort='DESC'): # Sort orders by price return sorted(orders, key=lambda order: order['price'], reverse=reverse) + def get_order_cancellation_fee(self, fee_asset): + """ Returns the order cancellation fee in the specified asset. + + :param string | fee_asset: Asset in which the fee is wanted + :return: Cancellation fee as fee asset + """ + # Get fee + fees = self.dex.returnFees() + limit_order_cancel = fees['limit_order_cancel'] + return self.convert_fee(limit_order_cancel['fee'], fee_asset) + + def get_order_creation_fee(self, fee_asset): + """ Returns the cost of creating an order in the asset specified + + :param fee_asset: QUOTE, BASE, BTS, or any other + :return: + """ + # Get fee + fees = self.dex.returnFees() + limit_order_create = fees['limit_order_create'] + return self.convert_fee(limit_order_create['fee'], fee_asset) + @staticmethod def get_order(order_id, return_none=True): """ Returns the Order object for the order_id @@ -756,6 +781,26 @@ def convert_asset(from_value, from_asset, to_asset): latest_price = ticker.get('latest', {}).get('price', None) return from_value * latest_price + def convert_fee(self, fee_amount, fee_asset): + """ Convert fee amount in BTS to fee in fee_asset + + :param float | fee_amount: fee amount paid in BTS + :param Asset | fee_asset: fee asset to pay fee in + :return: float | amount of fee_asset to pay fee + """ + if isinstance(fee_asset, str): + fee_asset = Asset(fee_asset) + + if fee_asset['id'] == '1.3.0': + # Fee asset is BTS, so no further calculations are needed + return fee_amount + else: + if not self.core_exchange_rate: + # Determine how many fee_asset is needed for core-exchange + temp_market = Market(base=fee_asset, quote=Asset('1.3.0')) + self.core_exchange_rate = temp_market.ticker()['core_exchange_rate'] + return fee_amount * self.core_exchange_rate['base']['amount'] + def orders_balance(self, order_ids, return_asset=False): if not order_ids: order_ids = [] diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 3a4f65a4a..89eb282e4 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -248,6 +248,9 @@ def __init__(self, # If there is no fee asset, use BTS self.fee_asset = Asset('1.3.0') + # CER cache + self.core_exchange_rate = None + # Ticker self.ticker = self.market.ticker @@ -818,9 +821,7 @@ def get_order_cancellation_fee(self, fee_asset): # Get fee fees = self.dex.returnFees() limit_order_cancel = fees['limit_order_cancel'] - - # Convert fee - return self.convert_asset(limit_order_cancel['fee'], 'BTS', fee_asset) + return self.convert_fee(limit_order_cancel['fee'], fee_asset) def get_order_creation_fee(self, fee_asset): """ Returns the cost of creating an order in the asset specified @@ -831,9 +832,7 @@ def get_order_creation_fee(self, fee_asset): # Get fee fees = self.dex.returnFees() limit_order_create = fees['limit_order_create'] - - # Convert fee - return self.convert_asset(limit_order_create['fee'], 'BTS', fee_asset) + return self.convert_fee(limit_order_create['fee'], fee_asset) def filter_buy_orders(self, orders, sort=None): """ Return own buy orders from list of orders. Can be used to pick buy orders from a list @@ -1259,6 +1258,26 @@ def convert_asset(from_value, from_asset, to_asset): return truncate((from_value * latest_price), precision) + def convert_fee(self, fee_amount, fee_asset): + """ Convert fee amount in BTS to fee in fee_asset + + :param float | fee_amount: fee amount paid in BTS + :param Asset | fee_asset: fee asset to pay fee in + :return: float | amount of fee_asset to pay fee + """ + if isinstance(fee_asset, str): + fee_asset = Asset(fee_asset) + + if fee_asset['id'] == '1.3.0': + # Fee asset is BTS, so no further calculations are needed + return fee_amount + else: + if not self.core_exchange_rate: + # Determine how many fee_asset is needed for core-exchange + temp_market = Market(base=fee_asset, quote=Asset('1.3.0')) + self.core_exchange_rate = temp_market.ticker()['core_exchange_rate'] + return fee_amount * self.core_exchange_rate['base']['amount'] + @staticmethod def get_order(order_id, return_none=True): """ Get Order object with order_id diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 3c6f8ee80..04001a6d0 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -315,25 +315,6 @@ def log_maintenance_time(self): delta = datetime.now() - self.start self.log.debug('Maintenance execution took: {:.2f} seconds'.format(delta.total_seconds())) - def get_order_creation_fee(self, fee_asset): - """ Returns the cost of creating an order in the asset specified - - :param fee_asset: QUOTE, BASE, BTS, or any other - :return: - """ - # Get fee - fees = self.dex.returnFees() - limit_order_create = fees['limit_order_create'] - - if fee_asset['id'] == '1.3.0': - # Fee asset is BTS, so no further calculations are needed - return limit_order_create['fee'] - else: - # Determine how many fee_asset is needed for core-exchange - temp_market = Market(base=fee_asset, quote=Asset('1.3.0')) - core_exchange_rate = temp_market.ticker()['core_exchange_rate'] - return limit_order_create['fee'] * core_exchange_rate['base']['amount'] - def refresh_balances(self, total_balances=True, use_cached_orders=False): """ This function is used to refresh account balances :param bool | total_balances: refresh total balance or skip it From c875a711de87870875bfc56a4a2cc47a62d41cd2 Mon Sep 17 00:00:00 2001 From: slade991 Date: Fri, 19 Oct 2018 14:50:39 +0900 Subject: [PATCH 0895/1846] Added runservice to run dexbot-cli as a service: dexbot-cli runservice --- dexbot/cli.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index 1ff25a9a2..420596d22 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -6,6 +6,7 @@ import sys from dexbot.config import Config, DEFAULT_CONFIG_FILE +from dexbot.cli_conf import * from dexbot.helper import initialize_orders_log, initialize_data_folders from dexbot.ui import ( verbose, @@ -94,7 +95,7 @@ def run(ctx): # 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") - if ctx.obj['systemd']: + if ctx.obj['systemd'] or ctx.obj['d']: try: import sdnotify # A soft dependency on sdnotify -- don't crash on non-systemd systems n = sdnotify.SystemdNotifier() @@ -109,6 +110,24 @@ def run(ctx): helper.remove(ctx.obj['pidfile']) +@main.command() +@click.pass_context +@configfile +@chain +@unlock +def runservice(ctx): + """ Continuously run the worker as a service + """ + if dexbot_service_running(): + click.echo("Stopping dexbot daemon") + os.system('systemctl --user stop dexbot') + + if not os.path.exists(SYSTEMD_SERVICE_NAME): + setup_systemd(get_whiptail('DEXBot configure'), {}) + + click.echo("Starting dexbot daemon") + os.system("systemctl --user start dexbot") + @main.command() @click.pass_context @configfile From 92ccfd36bffa731e848feb985fa2dc88842afd90 Mon Sep 17 00:00:00 2001 From: slade991 Date: Fri, 19 Oct 2018 16:00:04 +0900 Subject: [PATCH 0896/1846] fix bug in run by adding ["d"] --- dexbot/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index 420596d22..2e4285825 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -95,7 +95,7 @@ def run(ctx): # 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") - if ctx.obj['systemd'] or ctx.obj['d']: + if ctx.obj['systemd']: try: import sdnotify # A soft dependency on sdnotify -- don't crash on non-systemd systems n = sdnotify.SystemdNotifier() From a86105864d165b2f7e00c8fe3f0d0803056bf269 Mon Sep 17 00:00:00 2001 From: slade991 Date: Fri, 19 Oct 2018 16:15:21 +0900 Subject: [PATCH 0897/1846] More specific import --- dexbot/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index 2e4285825..e778f4644 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -6,7 +6,7 @@ import sys from dexbot.config import Config, DEFAULT_CONFIG_FILE -from dexbot.cli_conf import * +from dexbot.cli_conf import SYSTEMD_SERVICE_NAME, get_whiptail, setup_systemd from dexbot.helper import initialize_orders_log, initialize_data_folders from dexbot.ui import ( verbose, From fc0dbb55206f7f9b0e034205539bf7a083d3d119 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 19 Oct 2018 12:23:03 +0300 Subject: [PATCH 0898/1846] Remove unused imports --- dexbot/strategies/staggered_orders.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 04001a6d0..5940f5e89 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1,7 +1,5 @@ import math from datetime import datetime, timedelta -from bitshares.market import Market -from bitshares.asset import Asset from bitshares.dex import Dex from dexbot.basestrategy import BaseStrategy, ConfigElement From 9b5e386aa488878de4899e596fe995957b4f706a Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 19 Oct 2018 12:23:27 +0300 Subject: [PATCH 0899/1846] Remove todo comment --- dexbot/strategies/staggered_orders.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 5940f5e89..6ad8c4f59 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -32,7 +32,6 @@ def configure(cls, return_base_config=True): Sell slope: - All orders same QUOTE (profit made in BASE) """ - # Todo: - Add other modes modes = [ ('mountain', 'Mountain'), ('neutral', 'Neutral'), From d24a772bc5007eb176211c2500659acf4e9563b3 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 19 Oct 2018 12:23:46 +0300 Subject: [PATCH 0900/1846] Remove unused variables --- dexbot/strategies/staggered_orders.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 6ad8c4f59..b19cf6aec 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -208,19 +208,13 @@ def maintain_strategy(self, *args, **kwargs): # BASE asset check if self.base_balance > self.base_asset_threshold: - base_allocated = False # Allocate available BASE funds self.allocate_asset('base', self.base_balance) - else: - base_allocated = True # QUOTE asset check if self.quote_balance > self.quote_asset_threshold: - quote_allocated = False # Allocate available QUOTE funds self.allocate_asset('quote', self.quote_balance) - else: - quote_allocated = True # Send pending operations if not self.bitshares.txbuffer.is_empty(): @@ -405,8 +399,6 @@ def allocate_asset(self, asset, asset_balance): closest_opposite_order = None opposite_asset_limit = None opposite_orders = [] - opposite_balance = None - opposite_threshold = 0.0 order_type = '' own_asset_limit = None own_orders = [] @@ -418,16 +410,12 @@ def allocate_asset(self, asset, asset_balance): symbol = self.base_balance['symbol'] own_orders = self.buy_orders opposite_orders = self.sell_orders - opposite_balance = self.quote_balance - opposite_threshold = self.quote_asset_threshold own_threshold = self.base_asset_threshold elif asset == 'quote': order_type = 'sell' symbol = self.quote_balance['symbol'] own_orders = self.sell_orders opposite_orders = self.buy_orders - opposite_balance = self.base_balance - opposite_threshold = self.base_asset_threshold own_threshold = self.quote_asset_threshold if own_orders: @@ -851,8 +839,6 @@ def replace_partially_filled_order(self, order): :param order: Order instance """ - order_type = '' - asset_balance = None if order['base']['symbol'] == self.market['base']['symbol']: asset_balance = self.base_balance From baf3555e166f8465b34cf3bdec4227bfdd0c1b18 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 19 Oct 2018 12:23:59 +0300 Subject: [PATCH 0901/1846] Fix code to follow PEP guidelines on line length --- dexbot/strategies/staggered_orders.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index b19cf6aec..c9caa627a 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -449,8 +449,8 @@ def allocate_asset(self, asset, asset_balance): both sides, spread will be no less than `target_spread - increment`, thus not making any loss. """ if (self.bootstrapping and - self.base_balance_history[2] == self.base_balance_history[0] and - self.quote_balance_history[2] == self.quote_balance_history[0]): + self.base_balance_history[2] == self.base_balance_history[0] and + self.quote_balance_history[2] == self.quote_balance_history[0]): # Turn off bootstrap mode whether we're didn't allocated assets during previous 3 maintenance self.log.debug('Turning bootstrapping off: actual_spread > target_spread, we have free ' 'balances and cannot allocate them normally 3 times in a row') @@ -471,7 +471,8 @@ def allocate_asset(self, asset, asset_balance): self.log.debug('Limiting {} order by opposite order: {} {}' .format(order_type, own_asset_limit, symbol)) elif self.mode == 'neutral': - opposite_asset_limit = closest_opposite_order['base']['amount'] * math.sqrt(1 + self.increment) + opposite_asset_limit = closest_opposite_order['base']['amount'] * \ + math.sqrt(1 + self.increment) own_asset_limit = None self.log.debug('Limiting {} order by opposite order: {} {}'.format( order_type, opposite_asset_limit, symbol)) @@ -780,8 +781,11 @@ def increase_order_sizes(self, asset, asset_balance, orders): # Maximize order up to max possible amount if we can closer_order_bound = new_amount - self.log.debug('order amount: {:.8f}, closer_order_bound: {:.8f}'.format(order_amount, closer_order_bound)) - self.log.debug('diff: {:.8f}, half of increase: {:.8f}'.format(closer_order_bound - order_amount, order_amount * (math.sqrt(1 + self.increment) - 1) / 2)) + self.log.debug('order amount: {:.8f}, closer_order_bound: {:.8f}' + .format(order_amount, closer_order_bound)) + self.log.debug('diff: {:.8f}, half of increase: {:.8f}' + .format(closer_order_bound - order_amount, + order_amount * (math.sqrt(1 + self.increment) - 1) / 2)) if (order_amount * (1 + self.increment / 10) < closer_order_bound and closer_order_bound - order_amount >= order_amount * (math.sqrt(1 + self.increment) - 1) / 2): @@ -800,7 +804,8 @@ def increase_order_sizes(self, asset, asset_balance, orders): price = (order['price'] ** -1) elif asset == 'base': price = order['price'] - self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}, amount: {:.8f}, price: {:.8f}' + self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}' + ', amount: {:.8f}, price: {:.8f}' .format(order_type, self.mode, order_amount, price)) self.cancel(order) From bba691f6cc36ad9d584f3fb28f376f3318f18791 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 19 Oct 2018 12:24:44 +0300 Subject: [PATCH 0902/1846] Add values to variables to suppress warnings --- dexbot/strategies/staggered_orders.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c9caa627a..0a4dd8e93 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1071,8 +1071,10 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente # Exclude all further fees from avail balance quote_balance = quote_balance - fee * (buy_orders_count + sell_orders_count) + # Initialize local variables amount_quote = 0 previous_price = 0 + previous_amount = 0 if self.mode == 'mountain' or self.mode == 'buy_slope': previous_price = price orders_sum = 0 @@ -1178,8 +1180,10 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p # Exclude all further fees from avail balance base_balance = base_balance - fee * (buy_orders_count + sell_orders_count) + # Initialize local variables amount_quote = 0 previous_price = 0 + previous_amount = 0 if self.mode == 'mountain' or self.mode == 'sell_slope': previous_price = price orders_sum = 0 From af6e550264e5799280d8e35d8aff0fb5b6653b3d Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 19 Oct 2018 12:25:59 +0300 Subject: [PATCH 0903/1846] Refactor if statement to use "is" instead "==" --- 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 0a4dd8e93..baf7500c3 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -827,7 +827,7 @@ def check_partial_fill(self, order, fill_threshold=None): :return: bool | True = Order is correct size or within the threshold False = Order is not right size """ - if fill_threshold == None: + if fill_threshold is None: fill_threshold = self.partial_fill_threshold if order['for_sale']['amount'] != order['base']['amount']: From abee318e042f000fbb770b809d8c53346f3c4305 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 19 Oct 2018 13:14:41 +0300 Subject: [PATCH 0904/1846] Change dexbot version number to 0.7.11 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 4fc7a3997..679206a8f 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.10' +VERSION = '0.7.11' AUTHOR = 'Codaone Oy' __version__ = VERSION From 9746e6fc2cb7654db4be706c0146f6f29e316c00 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 19 Oct 2018 15:26:21 +0300 Subject: [PATCH 0905/1846] Update websocket-client to version 0.53.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 685aa047b..c3290dd8f 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ 'sdnotify', 'appdirs>=1.4.3', 'pycryptodomex==3.6.4', - 'websocket-client==0.48.0' + 'websocket-client==0.53.0' ] From 9e1716afa4b71d9bc2a7f3df0ebe6ea955dc1dc5 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 22 Oct 2018 15:06:50 +0300 Subject: [PATCH 0906/1846] Revert "Update websocket-client to version 0.53.0" This reverts commit 9746e6fc2cb7654db4be706c0146f6f29e316c00. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c3290dd8f..685aa047b 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ 'sdnotify', 'appdirs>=1.4.3', 'pycryptodomex==3.6.4', - 'websocket-client==0.53.0' + 'websocket-client==0.48.0' ] From 676b08a0710552e8b52036013a28d7082bdbafef Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 22 Oct 2018 15:08:30 +0300 Subject: [PATCH 0907/1846] Update websocket-client to version 0.53.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 685aa047b..c3290dd8f 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ 'sdnotify', 'appdirs>=1.4.3', 'pycryptodomex==3.6.4', - 'websocket-client==0.48.0' + 'websocket-client==0.53.0' ] From 99dbcdac273c4220818992a6776d4fc5d952af2a Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 23 Oct 2018 09:05:17 +0300 Subject: [PATCH 0908/1846] Change dexbot version number to 0.7.12 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 679206a8f..0e6a3b93e 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.11' +VERSION = '0.7.12' AUTHOR = 'Codaone Oy' __version__ = VERSION From 01ab5f55c83f46b07a053adb6025438064c6bf27 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 23 Oct 2018 12:18:38 +0300 Subject: [PATCH 0909/1846] Fix adding twice to num_of_workers --- dexbot/views/worker_list.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 8b606facc..beba741ac 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -43,7 +43,6 @@ def __init__(self, main_ctrl): 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.add_worker_button.setEnabled(False) break From 2c1a48d9b32513e7d1856426cfa836b442c152ad Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 23 Oct 2018 15:04:21 +0300 Subject: [PATCH 0910/1846] Change .travis.yml to only update brew --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b97cdc390..ca5347673 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,8 +7,7 @@ matrix: - os: osx language: generic before_install: - - python --version - - brew upgrade python + - brew update install: - make install script: From 7dd664f08f633d6e17622cf44d0dbecf43f5e0b2 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 23 Oct 2018 15:16:04 +0300 Subject: [PATCH 0911/1846] Change dexbot version number to 0.7.13 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 0e6a3b93e..cf9fada7e 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.12' +VERSION = '0.7.13' AUTHOR = 'Codaone Oy' __version__ = VERSION From 680b433e7bdab02cf0ca171898fe478f621d7e0b Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 25 Oct 2018 14:23:52 +0300 Subject: [PATCH 0912/1846] Fix an error when there is no market center price --- dexbot/strategies/relative_orders.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index d2af9d777..dcdf09dfe 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -119,6 +119,16 @@ def __init__(self, *args, **kwargs): self.disabled = True return + # Check if market has center price when using dynamic center price + if self.is_center_price_dynamic: + + # Try getting center price from the market + self.center_price = self.get_market_center_price(suppress_errors=True) + + if self.center_price is None: + self.log.info('Waiting until market center price can be estimated') + return + # Check old orders from previous run (from force-interruption) only whether we are not using # "Reset orders on center price change" option if self.is_reset_on_price_change: From c9bfe009f5693c81bbd451b4d4f20e5bbd5557da Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 26 Oct 2018 14:21:47 +0300 Subject: [PATCH 0913/1846] Fix editing paused worker doesn't turn off another --- dexbot/worker.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/dexbot/worker.py b/dexbot/worker.py index 6af9f742f..f5f440378 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -179,13 +179,18 @@ def run(self): def stop(self, worker_name=None, pause=False): """ Used to stop the worker(s) + :param str worker_name: name of the worker to stop - :param bool pause: optional argument which tells worker if it was - stopped or just paused + :param bool pause: optional argument which tells worker if it was stopped or just paused """ - if worker_name and len(self.workers) > 1: - # Kill only the specified worker - self.remove_market(worker_name) + if worker_name: + try: + # Kill only the specified worker + self.remove_market(worker_name) + except KeyError: + # Worker was not found meaning it does not exist or it is paused already + return + with self.config_lock: account = self.config['workers'][worker_name]['account'] self.config['workers'].pop(worker_name) @@ -194,14 +199,18 @@ def stop(self, worker_name=None, pause=False): if pause: self.workers[worker_name].pause() self.workers.pop(worker_name, None) - self.update_notify() else: # Kill all of the workers if pause: for worker in self.workers: self.workers[worker].pause() - if self.notify: - self.notify.websocket.close() + + # Update other workers + if len(self.workers) > 0: + self.update_notify() + else: + # No workers left, close websocket + self.notify.websocket.close() def remove_worker(self, worker_name=None): if worker_name: From b5436dc3b5cb1b9ed86898a6c0c3656f2b1ce8af Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 26 Oct 2018 14:23:19 +0300 Subject: [PATCH 0914/1846] Fix Relative Orders error when market empty --- dexbot/strategies/relative_orders.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index dcdf09dfe..3463a5815 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -74,6 +74,13 @@ def __init__(self, *args, **kwargs): self.error_onMarketUpdate = self.error self.error_onAccount = self.error + # Market status + self.market_center_price = self.get_market_center_price(suppress_errors=True) + self.empty_market = False + + if not self.market_center_price: + self.empty_market = True + # Worker parameters self.is_center_price_dynamic = self.worker['center_price_dynamic'] if self.is_center_price_dynamic: @@ -120,14 +127,9 @@ def __init__(self, *args, **kwargs): return # Check if market has center price when using dynamic center price - if self.is_center_price_dynamic: - - # Try getting center price from the market - self.center_price = self.get_market_center_price(suppress_errors=True) - - if self.center_price is None: - self.log.info('Waiting until market center price can be estimated') - return + if self.empty_market and (self.is_center_price_dynamic or self.dynamic_spread): + self.log.info('Market is empty and using dynamic market parameters. Waiting for market change...') + return # Check old orders from previous run (from force-interruption) only whether we are not using # "Reset orders on center price change" option From 644b02aab64126bcdbd5980b3fe31eec146b798d Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 26 Oct 2018 17:05:07 +0300 Subject: [PATCH 0915/1846] WIP Fix CLI config saving --- dexbot/cli_conf.py | 69 +++++++++++++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index e3260b480..3919a7fcb 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -89,13 +89,15 @@ def process_config_element(elem, whiptail, config): title, config.get( elem.key, elem.default)) config[elem.key] = txt + if elem.type == "bool": value = config.get(elem.key, elem.default) value = 'yes' if value else 'no' config[elem.key] = whiptail.confirm(title, value) + if elem.type in ("float", "int"): - txt = whiptail.prompt(title, str(config.get(elem.key, elem.default))) while True: + txt = whiptail.prompt(title, str(config.get(elem.key, elem.default))) try: if elem.type == "int": val = int(txt) @@ -109,8 +111,8 @@ def process_config_element(elem, whiptail, config): break except ValueError: whiptail.alert("Not a valid value") - txt = whiptail.prompt(title, str(config.get(elem.key, elem.default))) config[elem.key] = val + if elem.type == "choice": config[elem.key] = whiptail.radiolist(title, select_choice( config.get(elem.key, elem.default), elem.extra)) @@ -165,33 +167,56 @@ def setup_systemd(whiptail, config): config['systemd_status'] = 'enabled' -def configure_worker(whiptail, worker): - default_strategy = worker.get('module', 'dexbot.strategies.relative_orders') - for i in STRATEGIES: - if default_strategy == i['class']: - default_strategy = i['tag'] +def configure_worker(whiptail, worker_config): + default_strategy = worker_config.get('module', 'dexbot.strategies.relative_orders') + strategy_list = [] + + for strategy in STRATEGIES: + if default_strategy == strategy['class']: + default_strategy = strategy['tag'] + + # Add strategy tag and name pairs to a list + strategy_list.append([strategy['tag'], strategy['name']]) + + worker_config['module'] = whiptail.radiolist( + "Choose a worker strategy", + select_choice(default_strategy, strategy_list) + ) + + for strategy in STRATEGIES: + if strategy['tag'] == worker_config['module']: + worker_config['module'] = strategy['class'] - worker['module'] = whiptail.radiolist( - "Choose a worker strategy", select_choice( - default_strategy, [(i['tag'], i['name']) for i in STRATEGIES])) - for i in STRATEGIES: - if i['tag'] == worker['module']: - worker['module'] = i['class'] # Import the strategy class but we don't __init__ it here strategy_class = getattr( - importlib.import_module(worker["module"]), + importlib.import_module(worker_config["module"]), 'Strategy' ) + + # print(worker_config) + + # if default_strategy != worker_config['module']: + from dexbot.strategies.base import StrategyBase + + new_worker_config = {} + + for config_item in StrategyBase.configure(): + key = config_item[0] + new_worker_config[key] = worker_config[key] + + print(new_worker_config) + # Use class metadata for per-worker configuration - configs = strategy_class.configure() - if configs: - for c in configs: - process_config_element(c, whiptail, worker) + config_elems = strategy_class.configure() + if config_elems: + # Strategy options + for elem in config_elems: + process_config_element(elem, whiptail, worker_config) else: whiptail.alert( "This worker type does not have configuration information. " "You will have to check the worker code and add configuration values to config.yml if required") - return worker + return worker_config def configure_dexbot(config, ctx): @@ -214,13 +239,13 @@ def configure_dexbot(config, ctx): ('CONF', 'Redo general config')]) if action == 'EDIT': - worker_name = whiptail.menu("Select worker to edit", [(i, i) for i in workers]) + worker_name = whiptail.menu("Select worker to edit", [(index, index) for index in workers]) config['workers'][worker_name] = configure_worker(whiptail, config['workers'][worker_name]) strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) strategy.clear_all_worker_data() elif action == 'DEL': - worker_name = whiptail.menu("Select worker to delete", [(i, i) for i in workers]) + worker_name = whiptail.menu("Select worker to delete", [(index, index) for index in workers]) del config['workers'][worker_name] strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) @@ -231,7 +256,7 @@ def configure_dexbot(config, ctx): elif action == 'CONF': choice = whiptail.node_radiolist( msg="Choose node", - items=select_choice(config['node'][0], [(i, i) for i in config['node']]) + items=select_choice(config['node'][0], [(index, index) for index in config['node']]) ) # Move selected node as first item in the config file's node list config['node'].remove(choice) From 11e4fb3abc0f26f9452114c05cc06d015e1e54b0 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 27 Oct 2018 23:21:07 +0500 Subject: [PATCH 0916/1846] Fix order size in place_further_order() --- dexbot/strategies/staggered_orders.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index baf7500c3..31107d093 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1001,10 +1001,14 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals # Calculate new order amounts depending on mode opposite_asset_amount = 0 own_asset_amount = 0 - if self.mode == 'mountain' or self.mode == 'buy_slope': + if (self.mode == 'mountain' or + (self.mode == 'buy_slope' and asset == 'quote') or + (self.mode == 'sell_slope' and asset == 'base')): opposite_asset_amount = order['quote']['amount'] own_asset_amount = opposite_asset_amount * price - elif self.mode == 'valley' or self.mode == 'buy_slope': + elif (self.mode == 'valley' or + (self.mode == 'buy_slope' and asset == 'base') or + (self.mode == 'sell_slope' and asset == 'quote')): own_asset_amount = order['base']['amount'] opposite_asset_amount = own_asset_amount / price elif self.mode == 'neutral': From 67dcdf0d32285738aa47030f923ab5dc070a5bca Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 29 Oct 2018 08:48:02 +0200 Subject: [PATCH 0917/1846] Change dexbot version number to 0.7.14 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index cf9fada7e..0a4520cb7 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.13' +VERSION = '0.7.14' AUTHOR = 'Codaone Oy' __version__ = VERSION From bddd01985010635b80fa9c1ef7fe96b0726d69f0 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 29 Oct 2018 08:53:53 +0200 Subject: [PATCH 0918/1846] Change dexbot version number to 0.7.15 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 0a4520cb7..807d30f41 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.14' +VERSION = '0.7.15' AUTHOR = 'Codaone Oy' __version__ = VERSION From 39eca13b6512292a3296dfea45a00710bcf8009a Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 29 Oct 2018 09:41:22 +0200 Subject: [PATCH 0919/1846] Change dexbot version number to 0.7.16 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 807d30f41..2bd4fd6a0 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.15' +VERSION = '0.7.16' AUTHOR = 'Codaone Oy' __version__ = VERSION From 918c397bf918cbd09241482ad244ab9dd80355b4 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 29 Oct 2018 13:40:41 +0200 Subject: [PATCH 0920/1846] Change dexbot version number to 0.7.17 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 2bd4fd6a0..ec038219d 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.16' +VERSION = '0.7.17' AUTHOR = 'Codaone Oy' __version__ = VERSION From 892c24aca13402d37f5b3286393d185e96bda602 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 29 Oct 2018 15:29:19 +0200 Subject: [PATCH 0921/1846] Fix CLI configuration saving --- dexbot/cli_conf.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 3919a7fcb..37293786a 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -167,6 +167,13 @@ def setup_systemd(whiptail, config): config['systemd_status'] = 'enabled' +def get_strategy_tag(strategy_class): + for strategy in STRATEGIES: + if strategy_class == strategy['class']: + return strategy['tag'] + return None + + def configure_worker(whiptail, worker_config): default_strategy = worker_config.get('module', 'dexbot.strategies.relative_orders') strategy_list = [] @@ -193,18 +200,18 @@ def configure_worker(whiptail, worker_config): 'Strategy' ) - # print(worker_config) - - # if default_strategy != worker_config['module']: - from dexbot.strategies.base import StrategyBase - - new_worker_config = {} + # Check if strategy has changed + if default_strategy != get_strategy_tag(worker_config['module']): + new_worker_config = {} - for config_item in StrategyBase.configure(): - key = config_item[0] - new_worker_config[key] = worker_config[key] + # If strategy has changed, create new config where base elements stay the same + for config_item in StrategyBase.configure(): + key = config_item[0] + new_worker_config[key] = worker_config[key] - print(new_worker_config) + # Add module separately to the config + new_worker_config['module'] = worker_config['module'] + worker_config = new_worker_config # Use class metadata for per-worker configuration config_elems = strategy_class.configure() From 90505ae835e00f28ab3474961f38aec1760719e0 Mon Sep 17 00:00:00 2001 From: "hapax.io" Date: Mon, 29 Oct 2018 18:12:43 -0700 Subject: [PATCH 0922/1846] add requirements for external feeds and test --- requirements.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e3eab6ed6..a8a61da3b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,8 @@ pyqt5==5.10 pyqt-distutils==0.7.3 pyinstaller==3.3.1 click-datetime==0.2 -cryptography==2.3 \ No newline at end of file +cryptography==2.3 +aiohttp==3.4.4 +requests>=2.20.0 +yarl==1.1.0 +ccxt==1.17.434 From 0964b06127b7ac492c756f4345fbf55fdb20993f Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 30 Oct 2018 08:57:09 +0200 Subject: [PATCH 0923/1846] Fix CLI new worker creation --- dexbot/cli_conf.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 37293786a..5056d0d65 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -175,6 +175,12 @@ def get_strategy_tag(strategy_class): def configure_worker(whiptail, worker_config): + # By default always editing + editing = True + + if not worker_config: + editing = False + default_strategy = worker_config.get('module', 'dexbot.strategies.relative_orders') strategy_list = [] @@ -185,6 +191,7 @@ def configure_worker(whiptail, worker_config): # Add strategy tag and name pairs to a list strategy_list.append([strategy['tag'], strategy['name']]) + # Strategy selection worker_config['module'] = whiptail.radiolist( "Choose a worker strategy", select_choice(default_strategy, strategy_list) @@ -200,8 +207,8 @@ def configure_worker(whiptail, worker_config): 'Strategy' ) - # Check if strategy has changed - if default_strategy != get_strategy_tag(worker_config['module']): + # Check if strategy has changed and editing existing worker + if editing and default_strategy != get_strategy_tag(worker_config['module']): new_worker_config = {} # If strategy has changed, create new config where base elements stay the same From 7b76c518f4b2f8ed05fde9fd881e19791a6d969c Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 30 Oct 2018 09:13:31 +0200 Subject: [PATCH 0924/1846] Change dexbot version number to 0.7.18 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index ec038219d..22c4c4df6 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.17' +VERSION = '0.7.18' AUTHOR = 'Codaone Oy' __version__ = VERSION From dc27bb597aa6f21510d6f457905b6bb7c26b7feb Mon Sep 17 00:00:00 2001 From: "hapax.io" Date: Tue, 30 Oct 2018 10:49:28 -0700 Subject: [PATCH 0925/1846] add external feed files --- dexbot/strategies/external_feeds/ccxt_feed.py | 119 ++++++++++++++++++ .../strategies/external_feeds/gecko_feed.py | 114 +++++++++++++++++ .../strategies/external_feeds/process_pair.py | 85 +++++++++++++ dexbot/strategies/external_feeds/styles.py | 45 +++++++ .../external_feeds/tests/async_feeds_test.py | 35 ++++++ 5 files changed, 398 insertions(+) create mode 100644 dexbot/strategies/external_feeds/ccxt_feed.py create mode 100644 dexbot/strategies/external_feeds/gecko_feed.py create mode 100644 dexbot/strategies/external_feeds/process_pair.py create mode 100644 dexbot/strategies/external_feeds/styles.py create mode 100644 dexbot/strategies/external_feeds/tests/async_feeds_test.py diff --git a/dexbot/strategies/external_feeds/ccxt_feed.py b/dexbot/strategies/external_feeds/ccxt_feed.py new file mode 100644 index 000000000..bb4d20f82 --- /dev/null +++ b/dexbot/strategies/external_feeds/ccxt_feed.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +import os, sys, time, math +import click +import ccxt # noqa: E402 +from pprint import pprint +from styles import * +from process_pair import * + + +def get_exch_symbols(exchange): + return exchange.symbols + + +def get_exchanges(): + return ccxt.exchanges + + +def get_ticker(exchange, symbol): + try: + # get raw json data + ticker = exchange.fetch_ticker(symbol.upper()) + + except ccxt.DDoSProtection as e: + print(type(e).__name__, e.args, 'DDoS Protection (ignoring)') + except ccxt.RequestTimeout as e: + print(type(e).__name__, e.args, 'Request Timeout (ignoring)') + except ccxt.ExchangeNotAvailable as e: + print(type(e).__name__, e.args, 'Exchange Not Available due to downtime or maintenance (ignoring)') + except ccxt.AuthenticationError as e: + print(type(e).__name__, e.args, 'Authentication Error (missing API keys, ignoring)') + + return ticker + + + +###### unit tests ###### +@click.group() +def main(): + pass + + +@main.command() +@click.argument('exchange') +@click.argument('symbol') +def test_feed(exchange, symbol): + ''' + Usage: exchange [symbol] + Symbol is required, for example: + python ccxt_feed.py test_feed gdax BTC/USD + ''' + usage = "Usage: python ccxt_feed.py id [symbol]\nSymbol is required, for example: python ccxt_feed.py gdax BTC/USD" + + try: + id = exchange # get exchange id from command line arguments + + # check if the exchange is supported by ccxt + exchange_found = id in ccxt.exchanges + + if exchange_found: + print_args('Instantiating', green(id)) + + # instantiate the exchange by id + exch = getattr(ccxt, id)() + + # load all markets from the exchange + markets = exch.load_markets() + + sym = symbol + if sym: + ticker = get_ticker(exch, sym) + print_args( + green(exch.id), + yellow(sym), + 'ticker', + ticker['datetime'], + 'high: ' + str(ticker['high']), + 'low: ' + str(ticker['low']), + 'bid: ' + str(ticker['bid']), + 'ask: ' + str(ticker['ask']), + 'volume: ' + str(ticker['quoteVolume'])) + else: + print_args('Symbol not found') + print_exch_symbols(exch) + print(usage) + + else: + print_args('Exchange ' + red(id) + ' not found') + print(usage) + except Exception as e: + print(type(e).__name__, e.args, str(e)) + print(usage) + + +@main.command() +def test_exch_list(): + ''' + gets a list of supported exchanges + ''' + supported_exchanges = get_exchanges() + exch_list = ', '.join(str(name) for name in supported_exchanges) + print(bold(underline('Supported exchanges: '))) + pprint(exch_list, width=80) + + +@main.command() +@click.argument('exchange') +def test_exch_sym(exchange): + ''' + print all symbols from an exchange + ''' + # output all symbols + print_args(green(id), 'has', len(exchange.symbols), 'symbols:', yellow(', '.join(exchange.symbols))) + + + +if __name__ == '__main__': + main() + + diff --git a/dexbot/strategies/external_feeds/gecko_feed.py b/dexbot/strategies/external_feeds/gecko_feed.py new file mode 100644 index 000000000..ff6ac6cff --- /dev/null +++ b/dexbot/strategies/external_feeds/gecko_feed.py @@ -0,0 +1,114 @@ +# Python imports +import requests, json, sys +import click +from styles import * +from process_pair import * + +GECKO_COINS_URL = 'https://api.coingecko.com/api/v3/coins/' + +"""To use Gecko API, first get coinlist to search for base/quote individually +gecko does not provide pairs by default. for base/quote one must be listed as ticker +and the other lsited as fullname, i.e. BTCUSD is vs_currency = usd , ids = bitcoin +https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin +""" + +isDebug = True + +def debug(*args): + if isDebug: + print(' '.join([str(arg) for arg in args])) + + +def print_usage(): + print("Usage: python3 gecko_feed.py", + yellow('[symbol]'), + "Symbol is required, for example:", + yellow('BTC/USD'), sep='') + + +def get_gecko_json(url): + r = requests.get(url) + json_obj = r.json() + return json_obj + + +def check_gecko_symbol_exists(coinlist, symbol): + try: + symbol_name = [obj for obj in coinlist if obj['symbol']==symbol][0]['id'] + return symbol_name + except IndexError: + return None + + +def get_gecko_market_price(base, quote): + + try: + coin_list = get_gecko_json(GECKO_COINS_URL+'list') + quote_name = check_gecko_symbol_exists(coin_list, quote.lower()) + + lookup_pair = "?vs_currency="+base.lower()+"&ids="+quote_name + market_url = GECKO_COINS_URL+'markets'+lookup_pair + debug(market_url) + + ticker = get_gecko_json(market_url) + + for entry in ticker: + current_price = entry['current_price'] + high_24h = entry['high_24h'] + low_24h = entry['low_24h'] + total_volume = entry['total_volume'] + + return current_price + + except TypeError: + return None + + +### Unit tests +@click.group() +def main(): + pass + +@main.command() +@click.argument('symbol') +def test_feed(symbol): + ''' + [symbol] Symbol example: btc/usd or btc:usd + + base currency for coin gecko is in USD,EUR,JPY, CAD, etc, + see entire list here: https://api.coingecko.com/api/v3/global + + Gecko Example of no market = BTC/USDT + Gecko Example of working market BTC/EUR or BTC/USD + ''' + try: + pair = split_pair(symbol) # pair = [quote, base] + filtered_pair = [filter_bit_symbol(j) for j in [filter_prefix_symbol(i) for i in pair]] + debug(filtered_pair) + + new_quote = filtered_pair[0] + new_base = filtered_pair[1] + + current_price = get_gecko_market_price(new_base, new_quote) + debug(current_price) + + if current_price is None: + # try inverted version + debug(" Trying pair inversion...") + current_price = get_gecko_market_price(new_quote, new_base) + # invert price + debug(new_base+"/"+new_quote+ ":"+ str(current_price)) + if current_price is not None: + actual_price = 1/current_price + debug(new_quote+"/"+new_base+ ":"+ str(actual_price)) + + except Exception as e: + print(type(e).__name__, e.args, str(e)) + print_usage() + + + + +if __name__ == '__main__': + main() + diff --git a/dexbot/strategies/external_feeds/process_pair.py b/dexbot/strategies/external_feeds/process_pair.py new file mode 100644 index 000000000..d9c543161 --- /dev/null +++ b/dexbot/strategies/external_feeds/process_pair.py @@ -0,0 +1,85 @@ +# Python imports +import re + +def print_args(*args): + print(' '.join([str(arg) for arg in args])) + + +def filter_prefix_symbol(symbol): + # example open.USD or bridge.USD, remove leading bit up to . + base = '' + if re.match(r'^[a-zA-Z](.*)\.(.*)', symbol): + base = re.sub('(.*)\.', '', symbol) + else: + base = symbol + return base + + +def filter_bit_symbol(symbol): + # if matches bitUSD or bitusd any bit prefix, strip + base = '' + if re.match(r'bit[a-zA-Z]{3}' , symbol): + base = re.sub("bit", "", symbol) + else: + base = symbol + return base + + +def split_pair(symbol): + pair = re.split(':|/', symbol) + return pair + + +def get_consolidated_pair(base, quote): + # split into two USD pairs, STEEM/BTS = (BTS/USD * USD/STEEM) + pair1 = [base,'USD'] # BTS/USD pair = [quote, base] + pair2 = ['USD', quote] # USD/STEEM + return pair1, pair2 + + +## Unit Tests + +def test_consolidated_pair(): + symbol = 'STEEM:BTS' # pair = 'STEEM:BTS' or STEEM/BTS' + pair = split_pair(symbol) + pair1, pair2 = get_consolidated_pair(pair[1], pair[0]) + print(symbol, '=', pair1, pair2, sep=' ') + + + +def test_split_symbol(): + try: + group = ['BTC:USD', 'STEEM/USD'] + pair = [split_pair(symbol) for symbol in group] + print('original:', group, 'result:', pair, sep=' ') + except Exception as e: + pass + + +def test_filters(): + test_symbols = ['USDT', 'bridge.USD', 'Rudex.USD', 'open.USD', + 'GDEX.USD', 'Spark.USD', 'bridge.BTC', 'BTC', 'LTC', + 'bitUSD', 'bitEUR', 'bitHKD'] + + print("Test Symbols", test_symbols, sep=":") + + r = [filter_prefix_symbol(i) for i in test_symbols] + print("Filter prefix symbol", r, sep=":") + + r2 = [filter_bit_symbol(i) for i in r] + print("Apply to result, Filter bit symbol", r2, sep=":") + + + +if __name__ == '__main__': + + print("testing consolidate pair") + test_consolidated_pair() + print("\n") + + print("testing split symbol") + test_split_symbol() + print("\n") + + print("testing filters") + test_filters() diff --git a/dexbot/strategies/external_feeds/styles.py b/dexbot/strategies/external_feeds/styles.py new file mode 100644 index 000000000..2efe86dde --- /dev/null +++ b/dexbot/strategies/external_feeds/styles.py @@ -0,0 +1,45 @@ +import os + +def style(s, style): + return style + s + '\033[0m' + + +def green(s): + return style(s, '\033[92m') + + +def blue(s): + return style(s, '\033[94m') + + +def yellow(s): + return style(s, '\033[93m') + + +def red(s): + return style(s, '\033[91m') + + +def pink(s): + return style(s, '\033[95m') + + +def bold(s): + return style(s, '\033[1m') + + +def underline(s): + return style(s, '\033[4m') + + +if __name__ == '__main__': + + # unit test + + print(green("green style test")) + print(blue("blue style test")) + print(yellow("yellow style test")) + print(red("red style test")) + print(pink("pink style test")) + print(bold("bold style test")) + print(underline("underline test")) diff --git a/dexbot/strategies/external_feeds/tests/async_feeds_test.py b/dexbot/strategies/external_feeds/tests/async_feeds_test.py new file mode 100644 index 000000000..4b5c5a9a5 --- /dev/null +++ b/dexbot/strategies/external_feeds/tests/async_feeds_test.py @@ -0,0 +1,35 @@ +import asyncio +import aiohttp + +# docs +# Gecko https://www.coingecko.com/api_doc/3.html +# CCXT https://github.com/ccxt/ccxt +# Waves https://marketdata.wavesplatform.com/ + +# Cryptowat.ch https://cryptowat.ch/docs/api#pairs-index +# Asset: Returns a single asset. Lists all markets which have this asset as a base or quote. +# Example: https://api.cryptowat.ch/assets/btc + +#Index: Returns all assets (in no particular order). +# Example: https://api.cryptowat.ch/assets + +gecko_coins_url = 'https://api.coingecko.com/api/v3/coins/' +waves_symbols = 'http://marketdata.wavesplatform.com/api/symbols' +cwatch_assets='https://api.cryptowat.ch/assets' + +urls = [cwatch_assets, waves_symbols, gecko_coins_url] + +@asyncio.coroutine +def call_url(url): + print('Starting {}'.format(url)) + response = yield from aiohttp.ClientSession().get(url) + data = yield from response.text() + print('url: {} bytes: {}'.format(url, len(data))) +# print('{}: {} bytes: {}'.format(url, len(data), data)) + return data + + + +futures = [call_url(url) for url in urls] + +asyncio.run(asyncio.wait(futures)) From 6d30fb83c5f7ad0aaf82ff6e5071808fc61361df Mon Sep 17 00:00:00 2001 From: "hapax.io" Date: Tue, 30 Oct 2018 22:23:08 -0700 Subject: [PATCH 0926/1846] add to cli options for external feeds --- dexbot/strategies/base.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 89eb282e4..0c7c868b7 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -135,7 +135,8 @@ def configure(cls, return_base_config=True): :param return_base_config: bool: :return: Returns a list of config elements """ - + exchs = [('gecko', 'coingecko'), ('ccxt-kraken', 'kraken'), ('ccxt-bitfinex', 'bitfinex'), ('ccxt-gdax', 'gdax')] + # Common configs base_config = [ ConfigElement('account', 'string', '', 'Account', @@ -144,7 +145,14 @@ def configure(cls, return_base_config=True): ConfigElement('market', 'string', 'USD:BTS', 'Market', 'BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"', r'[A-Z\.]+[:\/][A-Z\.]+'), - ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset', + ConfigElement('external_center_price', 'bool', True, + 'Use External center price (if not avail, defaults to manual center price)', + 'External center price expressed in base asset: base/quote', None), + ConfigElement('external_center_price_source', 'choice', 'gecko', 'External Source', + 'External Price Source, select one', exchs), + + + ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset', 'Asset to be used to pay transaction fees', r'[A-Z\.]+') ] @@ -593,6 +601,18 @@ def get_lowest_own_sell_order(self, orders=None): except IndexError: return None + def get_external_market_center_price(self): +# def get_external_market_center_price(self, exchange='gecko', market='BTC/USD', suppress_errors=False): + """ Returns the center price of market including own orders. + :param bool | suppress_errors: + :return: Market center price as float + """ + print("getting market center price for BTC/USD at 6363.00") + ticker_price = 6363.00 # dummy price + return ticker_price + + + def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors=False): """ Returns the center price of market including own orders. From 6ea95952b3af26cf816987421a428df153e42982 Mon Sep 17 00:00:00 2001 From: "hapax.io" Date: Tue, 30 Oct 2018 22:48:58 -0700 Subject: [PATCH 0927/1846] add async --- dexbot/strategies/external_feeds/ccxt_feed.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/dexbot/strategies/external_feeds/ccxt_feed.py b/dexbot/strategies/external_feeds/ccxt_feed.py index bb4d20f82..7a70af2e5 100644 --- a/dexbot/strategies/external_feeds/ccxt_feed.py +++ b/dexbot/strategies/external_feeds/ccxt_feed.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- import os, sys, time, math import click +import asyncio, functools +import ccxt.async_support as accxt import ccxt # noqa: E402 from pprint import pprint from styles import * @@ -33,12 +35,60 @@ def get_ticker(exchange, symbol): +async def fetch_ticker(exchange, symbol): + ticker = await exchange.fetchTicker(symbol) + print(exchange.id, symbol, ticker) + return ticker + + +async def fetch_tickers(exchange): + await exchange.load_markets() + print(exchange.id, 'fetching all tickers by simultaneous multiple concurrent requests') + symbols_to_load = get_active_symbols(exchange) + input_coroutines = [fetch_ticker(exchange, symbol) for symbol in symbols_to_load] + tickers = await asyncio.gather(*input_coroutines, return_exceptions=True) + for ticker, symbol in zip(tickers, symbols_to_load): + if not isinstance(ticker, dict): + print(exchange.id, symbol, 'error') + else: + print(exchange.id, symbol, 'ok') + print(exchange.id, 'fetched', len(list(tickers)), 'tickers') + + +async def print_ticker(symbol, id): + # verbose mode will show the order of execution to verify concurrency + exchange = getattr(accxt, id)({'verbose': True}) + print(await exchange.fetch_ticker(symbol)) + + ###### unit tests ###### @click.group() def main(): pass +@main.command() +def test_async(): + + symbol = 'ETH/BTC' + print_ethbtc_ticker = functools.partial(print_ticker, symbol) + [asyncio.ensure_future(print_ethbtc_ticker(id)) for id in [ + 'bitfinex', + 'binance', + 'kraken', + 'gdax', + 'bittrex', + ]] + pending = asyncio.Task.all_tasks() + loop = asyncio.get_event_loop() + loop.run_until_complete(asyncio.gather(*pending)) + +# asyncio.get_event_loop().run_until_complete(fetch_tickers(accxt.bitfinex({ +# 'enableRateLimit': True, # this option enables the built-in rate limiter +# }))) + + + @main.command() @click.argument('exchange') @click.argument('symbol') @@ -117,3 +167,4 @@ def test_exch_sym(exchange): main() + From 9e9022288e083a171eba38db33d44caeae0b10a5 Mon Sep 17 00:00:00 2001 From: "hapax.io" Date: Tue, 30 Oct 2018 23:08:26 -0700 Subject: [PATCH 0928/1846] update external center price --- dexbot/strategies/base.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 0c7c868b7..56e967180 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -135,7 +135,9 @@ def configure(cls, return_base_config=True): :param return_base_config: bool: :return: Returns a list of config elements """ - exchs = [('gecko', 'coingecko'), ('ccxt-kraken', 'kraken'), ('ccxt-bitfinex', 'bitfinex'), ('ccxt-gdax', 'gdax')] + + exchs = [('gecko', 'coingecko'), ('ccxt-kraken', 'kraken'), + ('ccxt-bitfinex', 'bitfinex'), ('ccxt-gdax', 'gdax'), ('ccxt-binance', 'binance')] # Common configs base_config = [ @@ -149,10 +151,8 @@ def configure(cls, return_base_config=True): 'Use External center price (if not avail, defaults to manual center price)', 'External center price expressed in base asset: base/quote', None), ConfigElement('external_center_price_source', 'choice', 'gecko', 'External Source', - 'External Price Source, select one', exchs), - - - ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset', + 'External Price Source, select one', exchs), + ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset', 'Asset to be used to pay transaction fees', r'[A-Z\.]+') ] @@ -243,6 +243,11 @@ def __init__(self, # Count of orders to be fetched from the API self.fetch_depth = 8 + + # set external price source + if self.worker.get('external_center_price', False): + external_price_source = self.worker.get('external_center_price_source') + print("external_price_source", external_price_source, sep=":") # Set fee asset fee_asset_symbol = self.worker.get('fee_asset') From 511092b5780af33c94c72cddf200685a9b125c27 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 31 Oct 2018 14:35:38 +0200 Subject: [PATCH 0929/1846] Update uptick to 0.2.0 and bitshares to 0.2.1 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index c3290dd8f..d74e9afbe 100755 --- a/setup.py +++ b/setup.py @@ -7,8 +7,8 @@ cmd_class = {} console_scripts = ['dexbot-cli = dexbot.cli:main'] install_requires = [ - 'bitshares==0.2.0', - 'uptick>=0.1.9', + 'bitshares==0.2.1', + 'uptick>=0.2.0', 'click', 'sqlalchemy', 'ruamel.yaml>=0.15.37', From e6d220c39a13f35cc9daaac18cd469ed974b3a8c Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 31 Oct 2018 15:03:52 +0200 Subject: [PATCH 0930/1846] Change uptick to stay in 0.2.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d74e9afbe..f8bff3898 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ console_scripts = ['dexbot-cli = dexbot.cli:main'] install_requires = [ 'bitshares==0.2.1', - 'uptick>=0.2.0', + 'uptick==0.2.0', 'click', 'sqlalchemy', 'ruamel.yaml>=0.15.37', From 881f610074a4eda53bdba955b5601d351cf4125a Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 31 Oct 2018 15:10:07 +0200 Subject: [PATCH 0931/1846] Change dexbot version number to 0.7.19 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 22c4c4df6..874b56260 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.18' +VERSION = '0.7.19' AUTHOR = 'Codaone Oy' __version__ = VERSION From 92bb061063ec61b8697d93cfa451f847263a5846 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 1 Nov 2018 12:59:35 +0200 Subject: [PATCH 0932/1846] Change ValueError to KeyAlreadyInStoreException --- dexbot/controllers/worker_controller.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index a6d1fae34..5c14def92 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -12,6 +12,7 @@ from bitshares.instance import shared_bitshares_instance from bitshares.asset import Asset from bitshares.account import Account +from bitshares.exceptions import KeyAlreadyInStoreException from bitsharesbase.account import PrivateKey from PyQt5 import QtGui @@ -57,7 +58,7 @@ def add_private_key(self, private_key): wallet = self.bitshares.wallet try: wallet.addPrivateKey(private_key) - except ValueError: + except KeyAlreadyInStoreException: # Private key already added pass @@ -240,6 +241,7 @@ def handle_save(self): if self.mode == 'add': # Add the private key to the database private_key = self.view.private_key_input.text() + if private_key: self.add_private_key(private_key) From 6afc4896c3bfc5f03d0dfcb46fd8353e5cb82114 Mon Sep 17 00:00:00 2001 From: The Hapax <39811582+thehapax@users.noreply.github.com> Date: Thu, 1 Nov 2018 16:42:33 -0700 Subject: [PATCH 0933/1846] Update async_feeds_test.py updated spacing. --- .../external_feeds/tests/async_feeds_test.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/dexbot/strategies/external_feeds/tests/async_feeds_test.py b/dexbot/strategies/external_feeds/tests/async_feeds_test.py index 4b5c5a9a5..cea0abe51 100644 --- a/dexbot/strategies/external_feeds/tests/async_feeds_test.py +++ b/dexbot/strategies/external_feeds/tests/async_feeds_test.py @@ -1,22 +1,9 @@ import asyncio import aiohttp -# docs -# Gecko https://www.coingecko.com/api_doc/3.html -# CCXT https://github.com/ccxt/ccxt -# Waves https://marketdata.wavesplatform.com/ - -# Cryptowat.ch https://cryptowat.ch/docs/api#pairs-index -# Asset: Returns a single asset. Lists all markets which have this asset as a base or quote. -# Example: https://api.cryptowat.ch/assets/btc - -#Index: Returns all assets (in no particular order). -# Example: https://api.cryptowat.ch/assets - gecko_coins_url = 'https://api.coingecko.com/api/v3/coins/' waves_symbols = 'http://marketdata.wavesplatform.com/api/symbols' cwatch_assets='https://api.cryptowat.ch/assets' - urls = [cwatch_assets, waves_symbols, gecko_coins_url] @asyncio.coroutine @@ -25,11 +12,7 @@ def call_url(url): response = yield from aiohttp.ClientSession().get(url) data = yield from response.text() print('url: {} bytes: {}'.format(url, len(data))) -# print('{}: {} bytes: {}'.format(url, len(data), data)) return data - - futures = [call_url(url) for url in urls] - asyncio.run(asyncio.wait(futures)) From 4caf0e0e3b4dadd91ab46404573b225f7563ec61 Mon Sep 17 00:00:00 2001 From: The Hapax <39811582+thehapax@users.noreply.github.com> Date: Thu, 1 Nov 2018 16:51:31 -0700 Subject: [PATCH 0934/1846] update with PEP8 checked with http://pep8online.com/ --- dexbot/strategies/external_feeds/ccxt_feed.py | 120 +++++++----------- 1 file changed, 44 insertions(+), 76 deletions(-) diff --git a/dexbot/strategies/external_feeds/ccxt_feed.py b/dexbot/strategies/external_feeds/ccxt_feed.py index 7a70af2e5..1ecd38f77 100644 --- a/dexbot/strategies/external_feeds/ccxt_feed.py +++ b/dexbot/strategies/external_feeds/ccxt_feed.py @@ -1,123 +1,106 @@ # -*- coding: utf-8 -*- -import os, sys, time, math import click -import asyncio, functools +import asyncio +import functools import ccxt.async_support as accxt import ccxt # noqa: E402 from pprint import pprint -from styles import * -from process_pair import * +from styles import green, yellow, bold, underline +from process_pair import print_args -def get_exch_symbols(exchange): - return exchange.symbols - - def get_exchanges(): return ccxt.exchanges def get_ticker(exchange, symbol): - try: - # get raw json data - ticker = exchange.fetch_ticker(symbol.upper()) - + try: + ticker = exchange.fetch_ticker(symbol.upper()) except ccxt.DDoSProtection as e: print(type(e).__name__, e.args, 'DDoS Protection (ignoring)') except ccxt.RequestTimeout as e: - print(type(e).__name__, e.args, 'Request Timeout (ignoring)') + print(type(e).__name__, e.args, + 'Request Timeout (ignoring)') except ccxt.ExchangeNotAvailable as e: - print(type(e).__name__, e.args, 'Exchange Not Available due to downtime or maintenance (ignoring)') + print(type(e).__name__, e.args, + 'Exchange Not Available due to downtime or maintenance (ignoring)') except ccxt.AuthenticationError as e: - print(type(e).__name__, e.args, 'Authentication Error (missing API keys, ignoring)') - + print(type(e).__name__, e.args, + 'Authentication Error (missing API keys, ignoring)') return ticker - async def fetch_ticker(exchange, symbol): ticker = await exchange.fetchTicker(symbol) - print(exchange.id, symbol, ticker) + await exchange.close() return ticker - -async def fetch_tickers(exchange): - await exchange.load_markets() - print(exchange.id, 'fetching all tickers by simultaneous multiple concurrent requests') - symbols_to_load = get_active_symbols(exchange) - input_coroutines = [fetch_ticker(exchange, symbol) for symbol in symbols_to_load] - tickers = await asyncio.gather(*input_coroutines, return_exceptions=True) - for ticker, symbol in zip(tickers, symbols_to_load): - if not isinstance(ticker, dict): - print(exchange.id, symbol, 'error') - else: - print(exchange.id, symbol, 'ok') - print(exchange.id, 'fetched', len(list(tickers)), 'tickers') - - async def print_ticker(symbol, id): # verbose mode will show the order of execution to verify concurrency exchange = getattr(accxt, id)({'verbose': True}) print(await exchange.fetch_ticker(symbol)) + await exchange.close() -###### unit tests ###### +# unit tests @click.group() def main(): pass @main.command() -def test_async(): - +def test_async2(): + """ + get all tickers from multiple exchanges using async + """ symbol = 'ETH/BTC' print_ethbtc_ticker = functools.partial(print_ticker, symbol) [asyncio.ensure_future(print_ethbtc_ticker(id)) for id in [ - 'bitfinex', - 'binance', - 'kraken', - 'gdax', - 'bittrex', - ]] + 'bitfinex', + 'binance', + 'kraken', + 'gdax', + 'bittrex', + ]] pending = asyncio.Task.all_tasks() loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.gather(*pending)) -# asyncio.get_event_loop().run_until_complete(fetch_tickers(accxt.bitfinex({ -# 'enableRateLimit': True, # this option enables the built-in rate limiter -# }))) +@main.command() +def test_async(): + """ + get ticker for bitfinex using async + """ + bitfinex = accxt.bitfinex({'enableRateLimit': True, }) + ticker = asyncio.get_event_loop().run_until_complete( + fetch_ticker(bitfinex, 'BTC/USDT')) + print(ticker) @main.command() @click.argument('exchange') @click.argument('symbol') def test_feed(exchange, symbol): - ''' - Usage: exchange [symbol] + """ + Usage: exchange [symbol] Symbol is required, for example: python ccxt_feed.py test_feed gdax BTC/USD - ''' - usage = "Usage: python ccxt_feed.py id [symbol]\nSymbol is required, for example: python ccxt_feed.py gdax BTC/USD" - + """ + usage = "Usage: python ccxt_feed.py id [symbol]" try: id = exchange # get exchange id from command line arguments - - # check if the exchange is supported by ccxt exchange_found = id in ccxt.exchanges - + # check if the exchange is supported by ccxt if exchange_found: print_args('Instantiating', green(id)) - # instantiate the exchange by id exch = getattr(ccxt, id)() - # load all markets from the exchange markets = exch.load_markets() - sym = symbol if sym: - ticker = get_ticker(exch, sym) + ticker = get_ticker(exch, sym) print_args( green(exch.id), yellow(sym), @@ -128,11 +111,10 @@ def test_feed(exchange, symbol): 'bid: ' + str(ticker['bid']), 'ask: ' + str(ticker['ask']), 'volume: ' + str(ticker['quoteVolume'])) - else: + else: print_args('Symbol not found') - print_exch_symbols(exch) + print_exchange_symbols(exch) print(usage) - else: print_args('Exchange ' + red(id) + ' not found') print(usage) @@ -143,28 +125,14 @@ def test_feed(exchange, symbol): @main.command() def test_exch_list(): - ''' + """ gets a list of supported exchanges - ''' + """ supported_exchanges = get_exchanges() exch_list = ', '.join(str(name) for name in supported_exchanges) print(bold(underline('Supported exchanges: '))) pprint(exch_list, width=80) -@main.command() -@click.argument('exchange') -def test_exch_sym(exchange): - ''' - print all symbols from an exchange - ''' - # output all symbols - print_args(green(id), 'has', len(exchange.symbols), 'symbols:', yellow(', '.join(exchange.symbols))) - - - if __name__ == '__main__': main() - - - From 2c0a00910ab4310a9cfa0cd8e38e7742ec2a4069 Mon Sep 17 00:00:00 2001 From: The Hapax <39811582+thehapax@users.noreply.github.com> Date: Thu, 1 Nov 2018 17:05:03 -0700 Subject: [PATCH 0935/1846] PEP8 update according to http://pep8online.com --- .../strategies/external_feeds/process_pair.py | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/dexbot/strategies/external_feeds/process_pair.py b/dexbot/strategies/external_feeds/process_pair.py index d9c543161..c5fb18a0d 100644 --- a/dexbot/strategies/external_feeds/process_pair.py +++ b/dexbot/strategies/external_feeds/process_pair.py @@ -1,6 +1,7 @@ # Python imports import re + def print_args(*args): print(' '.join([str(arg) for arg in args])) @@ -16,9 +17,9 @@ def filter_prefix_symbol(symbol): def filter_bit_symbol(symbol): - # if matches bitUSD or bitusd any bit prefix, strip + # if matches bitUSD or bitusd any bit prefix, strip base = '' - if re.match(r'bit[a-zA-Z]{3}' , symbol): + if re.match(r'bit[a-zA-Z]{3}', symbol): base = re.sub("bit", "", symbol) else: base = symbol @@ -26,25 +27,24 @@ def filter_bit_symbol(symbol): def split_pair(symbol): - pair = re.split(':|/', symbol) + pair = re.split(':|/', symbol) return pair def get_consolidated_pair(base, quote): - # split into two USD pairs, STEEM/BTS = (BTS/USD * USD/STEEM) - pair1 = [base,'USD'] # BTS/USD pair = [quote, base] - pair2 = ['USD', quote] # USD/STEEM + # split into two USD pairs, STEEM/BTS=(BTS/USD * USD/STEEM) + pair1 = [base, 'USD'] # BTS/USD pair=[quote, base] + pair2 = ['USD', quote] return pair1, pair2 -## Unit Tests +# Unit Tests def test_consolidated_pair(): - symbol = 'STEEM:BTS' # pair = 'STEEM:BTS' or STEEM/BTS' + symbol = 'STEEM:BTS' # pair = 'STEEM:BTS' or STEEM/BTS' pair = split_pair(symbol) pair1, pair2 = get_consolidated_pair(pair[1], pair[0]) print(symbol, '=', pair1, pair2, sep=' ') - def test_split_symbol(): @@ -57,29 +57,20 @@ def test_split_symbol(): def test_filters(): - test_symbols = ['USDT', 'bridge.USD', 'Rudex.USD', 'open.USD', - 'GDEX.USD', 'Spark.USD', 'bridge.BTC', 'BTC', 'LTC', + test_symbols = ['USDT', 'bridge.USD', 'Rudex.USD', 'open.USD', + 'GDEX.USD', 'Spark.USD', 'bridge.BTC', 'BTC', 'LTC', 'bitUSD', 'bitEUR', 'bitHKD'] - print("Test Symbols", test_symbols, sep=":") - r = [filter_prefix_symbol(i) for i in test_symbols] print("Filter prefix symbol", r, sep=":") - - r2 = [filter_bit_symbol(i) for i in r] + r2 = [filter_bit_symbol(i) for i in r] print("Apply to result, Filter bit symbol", r2, sep=":") - if __name__ == '__main__': - print("testing consolidate pair") test_consolidated_pair() - print("\n") - - print("testing split symbol") + print("\ntesting split symbol") test_split_symbol() - print("\n") - - print("testing filters") + print("\ntesting filters") test_filters() From dc3e48765aba85cd078560a229bbf5cdcbbb44cf Mon Sep 17 00:00:00 2001 From: The Hapax <39811582+thehapax@users.noreply.github.com> Date: Thu, 1 Nov 2018 17:06:55 -0700 Subject: [PATCH 0936/1846] PEP8 update --- dexbot/strategies/external_feeds/styles.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dexbot/strategies/external_feeds/styles.py b/dexbot/strategies/external_feeds/styles.py index 2efe86dde..76035b8d6 100644 --- a/dexbot/strategies/external_feeds/styles.py +++ b/dexbot/strategies/external_feeds/styles.py @@ -1,5 +1,6 @@ import os + def style(s, style): return style + s + '\033[0m' @@ -33,9 +34,7 @@ def underline(s): if __name__ == '__main__': - # unit test - print(green("green style test")) print(blue("blue style test")) print(yellow("yellow style test")) From 7da04ce7fca338caa6bd5e7c0be00afb8a078ebc Mon Sep 17 00:00:00 2001 From: The Hapax <39811582+thehapax@users.noreply.github.com> Date: Thu, 1 Nov 2018 17:45:57 -0700 Subject: [PATCH 0937/1846] PEP8 --- .../strategies/external_feeds/gecko_feed.py | 85 +++++++------------ 1 file changed, 33 insertions(+), 52 deletions(-) diff --git a/dexbot/strategies/external_feeds/gecko_feed.py b/dexbot/strategies/external_feeds/gecko_feed.py index ff6ac6cff..d77035523 100644 --- a/dexbot/strategies/external_feeds/gecko_feed.py +++ b/dexbot/strategies/external_feeds/gecko_feed.py @@ -1,18 +1,19 @@ # Python imports -import requests, json, sys +import requests +import json +import sys import click -from styles import * -from process_pair import * - -GECKO_COINS_URL = 'https://api.coingecko.com/api/v3/coins/' - -"""To use Gecko API, first get coinlist to search for base/quote individually -gecko does not provide pairs by default. for base/quote one must be listed as ticker -and the other lsited as fullname, i.e. BTCUSD is vs_currency = usd , ids = bitcoin -https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin +from styles import yellow +from process_pair import split_pair, filter_prefix_symbol, filter_bit_symbol """ +To use Gecko API, note that gecko does not provide pairs by default. +For base/quote one must be listed as ticker and the other as fullname, +i.e. BTCUSD is vs_currency = usd , ids = bitcoin +https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin +""" +GECKO_COINS_URL = 'https://api.coingecko.com/api/v3/coins/' +isDebug = False -isDebug = True def debug(*args): if isDebug: @@ -20,95 +21,75 @@ def debug(*args): def print_usage(): - print("Usage: python3 gecko_feed.py", - yellow('[symbol]'), - "Symbol is required, for example:", - yellow('BTC/USD'), sep='') + print("Usage: python3 gecko_feed.py", yellow('[symbol]'), + "Symbol is required, for example:", yellow('BTC/USD'), sep='') -def get_gecko_json(url): +def get_gecko_json(url): r = requests.get(url) json_obj = r.json() return json_obj - -def check_gecko_symbol_exists(coinlist, symbol): + +def check_gecko_symbol_exists(coinlist, symbol): try: - symbol_name = [obj for obj in coinlist if obj['symbol']==symbol][0]['id'] + symbol_name = [obj for obj in coinlist if obj['symbol'] == symbol][0]['id'] return symbol_name except IndexError: return None - -def get_gecko_market_price(base, quote): - try: +def get_gecko_market_price(base, quote): + try: coin_list = get_gecko_json(GECKO_COINS_URL+'list') quote_name = check_gecko_symbol_exists(coin_list, quote.lower()) - lookup_pair = "?vs_currency="+base.lower()+"&ids="+quote_name market_url = GECKO_COINS_URL+'markets'+lookup_pair debug(market_url) - ticker = get_gecko_json(market_url) - + current_price = None for entry in ticker: current_price = entry['current_price'] high_24h = entry['high_24h'] low_24h = entry['low_24h'] total_volume = entry['total_volume'] - return current_price - except TypeError: return None -### Unit tests +# Unit tests @click.group() def main(): pass + @main.command() @click.argument('symbol') def test_feed(symbol): - ''' + """ [symbol] Symbol example: btc/usd or btc:usd - - base currency for coin gecko is in USD,EUR,JPY, CAD, etc, - see entire list here: https://api.coingecko.com/api/v3/global - - Gecko Example of no market = BTC/USDT - Gecko Example of working market BTC/EUR or BTC/USD - ''' + """ try: - pair = split_pair(symbol) # pair = [quote, base] - filtered_pair = [filter_bit_symbol(j) for j in [filter_prefix_symbol(i) for i in pair]] + pair = split_pair(symbol) # pair=[quote, base] + filtered_pair = [filter_bit_symbol(j) for j in [filter_prefix_symbol(i) for i in pair]] debug(filtered_pair) - new_quote = filtered_pair[0] new_base = filtered_pair[1] - current_price = get_gecko_market_price(new_base, new_quote) - debug(current_price) - - if current_price is None: - # try inverted version + if current_price is None: # try inverted version debug(" Trying pair inversion...") current_price = get_gecko_market_price(new_quote, new_base) - # invert price - debug(new_base+"/"+new_quote+ ":"+ str(current_price)) - if current_price is not None: + print(new_base+"/"+new_quote, str(current_price), sep=':') + if current_price is not None: # re-invert price actual_price = 1/current_price - debug(new_quote+"/"+new_base+ ":"+ str(actual_price)) - + print(new_quote+"/"+new_base, str(actual_price), sep=':') + else: + print(symbol, current_price, sep=':') except Exception as e: print(type(e).__name__, e.args, str(e)) print_usage() - - if __name__ == '__main__': main() - From f89695c014738a748a754a8ed8f6f65e23adab82 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 4 Nov 2018 23:43:56 +0500 Subject: [PATCH 0938/1846] Fix exiting after signal Fixes #382 --- dexbot/worker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dexbot/worker.py b/dexbot/worker.py index f5f440378..47c5e3ea5 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -204,6 +204,7 @@ def stop(self, worker_name=None, pause=False): if pause: for worker in self.workers: self.workers[worker].pause() + self.workers = [] # Update other workers if len(self.workers) > 0: From a9919fa606c046226acdafe61202284f737dc1ec Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 4 Nov 2018 23:09:44 +0500 Subject: [PATCH 0939/1846] Use human-readable notation in log messages --- dexbot/basestrategy.py | 8 ++++---- dexbot/strategies/base.py | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 865640999..0f1105485 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -588,8 +588,8 @@ def market_buy(self, quote_amount, price, return_none=False, *args, **kwargs): return None self.log.info( - 'Placing a buy order for {} {} @ {:.8f}'.format( - base_amount, symbol, price) + 'Placing a buy order for {:.{prec}} {} @ {:.8f}'.format( + base_amount, symbol, price, prec=precision) ) # Place the order @@ -639,8 +639,8 @@ def market_sell(self, quote_amount, price, return_none=False, *args, **kwargs): return None self.log.info( - 'Placing a sell order for {} {} @ {:.8f}'.format( - quote_amount, symbol, price) + 'Placing a sell order for {:.{prec}f} {} @ {:.8f}'.format( + quote_amount, symbol, price, prec=precision) ) # Place the order diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 89eb282e4..33e815beb 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1032,7 +1032,8 @@ def place_market_buy_order(self, amount, price, return_none=False, *args, **kwar self.disabled = True return None - self.log.info('Placing a buy order for {} {} @ {:.8f}'.format(base_amount, symbol, price)) + self.log.info('Placing a buy order for {:.{prec}f} {} @ {:.8f}' + .format(base_amount, symbol, price, prec=precision)) # Place the order buy_transaction = self.retry_action( @@ -1085,7 +1086,8 @@ def place_market_sell_order(self, amount, price, return_none=False, *args, **kwa self.disabled = True return None - self.log.info('Placing a sell order for {} {} @ {:.8f}'.format(quote_amount, symbol, price)) + self.log.info('Placing a sell order for {:.{prec}f} {} @ {:.8f}' + .format(quote_amount, symbol, price, prec=precision)) # Place the order sell_transaction = self.retry_action( From d3a57e26a7806a9ed27c7ff4cbd4d685a83ccc8e Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 5 Nov 2018 12:57:09 +0500 Subject: [PATCH 0940/1846] Fix notation in cli conf Fixes #201 --- dexbot/cli_conf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 5056d0d65..8ac8d210e 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -97,7 +97,11 @@ def process_config_element(elem, whiptail, config): if elem.type in ("float", "int"): while True: - txt = whiptail.prompt(title, str(config.get(elem.key, elem.default))) + if elem.type == 'int': + template = '{}' + else: + template = '{:.8f}' + txt = whiptail.prompt(title, template.format(config.get(elem.key, elem.default))) try: if elem.type == "int": val = int(txt) From e619bbceb38c6a78d2ffc60c500dd84a7b78b310 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 5 Nov 2018 10:39:54 +0200 Subject: [PATCH 0941/1846] Add backwards compatibility to basestrategy.py --- dexbot/basestrategy.py | 32 ++++++++++++++++++++++++++----- dexbot/strategies/base.py | 40 +++++++++++++++++++-------------------- 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py index 865640999..c8561f7c3 100644 --- a/dexbot/basestrategy.py +++ b/dexbot/basestrategy.py @@ -100,13 +100,31 @@ def configure(cls, return_base_config=True): NOTE: when overriding you almost certainly will want to call the ancestor and then add your config values to the list. """ + + # External exchanges used to calculate center price + exchanges = [ + ('gecko', 'coingecko'), + ('ccxt-kraken', 'kraken'), + ('ccxt-bitfinex', 'bitfinex'), + ('ccxt-gdax', 'gdax'), + ('ccxt-binance', 'binance') + ] + # These configs are common to all bots base_config = [ - ConfigElement("account", "string", "", "Account", "BitShares account name for the bot to operate with", ""), - ConfigElement("market", "string", "USD:BTS", "Market", - "BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"", - r"[A-Z\.]+[:\/][A-Z\.]+"), - ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset', 'Asset to be used to pay transaction fees', + ConfigElement('account', 'string', '', 'Account', + 'BitShares account name for the bot to operate with', + ''), + ConfigElement('market', 'string', 'USD:BTS', 'Market', + 'BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"', + r'[A-Z\.]+[:\/][A-Z\.]+'), + ConfigElement('external_center_price', 'bool', True, + 'Use External center price (if not available, defaults to manual center price)', + 'External center price expressed in base asset: BASE/QUOTE', None), + ConfigElement('external_center_price_source', 'choice', 'gecko', 'External Source', + 'External Price Source, select one', exchanges), + ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset', + 'Asset to be used to pay transaction fees', r'[A-Z\.]+') ] if return_base_config: @@ -174,6 +192,10 @@ def __init__( # Recheck flag - Tell the strategy to check for updated orders self.recheck_orders = False + # Set external price source + if self.worker.get('external_center_price', False): + self.external_price_source = self.worker.get('external_center_price_source') + # Set fee asset fee_asset_symbol = self.worker.get('fee_asset') if fee_asset_symbol: diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 56e967180..fcde580b5 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -136,8 +136,14 @@ def configure(cls, return_base_config=True): :return: Returns a list of config elements """ - exchs = [('gecko', 'coingecko'), ('ccxt-kraken', 'kraken'), - ('ccxt-bitfinex', 'bitfinex'), ('ccxt-gdax', 'gdax'), ('ccxt-binance', 'binance')] + # External exchanges used to calculate center price + exchanges = [ + ('gecko', 'coingecko'), + ('ccxt-kraken', 'kraken'), + ('ccxt-bitfinex', 'bitfinex'), + ('ccxt-gdax', 'gdax'), + ('ccxt-binance', 'binance') + ] # Common configs base_config = [ @@ -148,10 +154,10 @@ def configure(cls, return_base_config=True): 'BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"', r'[A-Z\.]+[:\/][A-Z\.]+'), ConfigElement('external_center_price', 'bool', True, - 'Use External center price (if not avail, defaults to manual center price)', - 'External center price expressed in base asset: base/quote', None), + 'Use External center price (if not available, defaults to manual center price)', + 'External center price expressed in base asset: BASE/QUOTE', None), ConfigElement('external_center_price_source', 'choice', 'gecko', 'External Source', - 'External Price Source, select one', exchs), + 'External Price Source, select one', exchanges), ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset', 'Asset to be used to pay transaction fees', r'[A-Z\.]+') @@ -244,10 +250,9 @@ def __init__(self, # Count of orders to be fetched from the API self.fetch_depth = 8 - # set external price source + # Set external price source if self.worker.get('external_center_price', False): - external_price_source = self.worker.get('external_center_price_source') - print("external_price_source", external_price_source, sep=":") + self.external_price_source = self.worker.get('external_center_price_source') # Set fee asset fee_asset_symbol = self.worker.get('fee_asset') @@ -607,16 +612,9 @@ def get_lowest_own_sell_order(self, orders=None): return None def get_external_market_center_price(self): -# def get_external_market_center_price(self, exchange='gecko', market='BTC/USD', suppress_errors=False): - """ Returns the center price of market including own orders. - :param bool | suppress_errors: - :return: Market center price as float - """ - print("getting market center price for BTC/USD at 6363.00") - ticker_price = 6363.00 # dummy price - return ticker_price - - + # Todo: Work in progress + ticker_price = 6363.00 # Dummy price + return ticker_price def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors=False): """ Returns the center price of market including own orders. @@ -627,8 +625,10 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors :return: Market center price as float """ - buy_price = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount, exclude_own_orders=False) - sell_price = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount, exclude_own_orders=False) + buy_price = self.get_market_buy_price(quote_amount=quote_amount, + base_amount=base_amount, exclude_own_orders=False) + sell_price = self.get_market_sell_price(quote_amount=quote_amount, + base_amount=base_amount, exclude_own_orders=False) if buy_price is None or buy_price == 0.0: if not suppress_errors: From c6441f613725d14688d25cc25f4dfa59b48de00e Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 5 Nov 2018 10:40:50 +0200 Subject: [PATCH 0942/1846] Add missing import for style red --- dexbot/strategies/external_feeds/ccxt_feed.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/external_feeds/ccxt_feed.py b/dexbot/strategies/external_feeds/ccxt_feed.py index 1ecd38f77..96850ea21 100644 --- a/dexbot/strategies/external_feeds/ccxt_feed.py +++ b/dexbot/strategies/external_feeds/ccxt_feed.py @@ -5,8 +5,9 @@ import ccxt.async_support as accxt import ccxt # noqa: E402 from pprint import pprint -from styles import green, yellow, bold, underline -from process_pair import print_args + +from dexbot.strategies.external_feeds.styles import red, green, yellow, bold, underline +from dexbot.strategies.external_feeds.process_pair import print_args def get_exchanges(): From 830350e63ad0157a23759a82d8f6d90f362fe630 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 5 Nov 2018 10:41:20 +0200 Subject: [PATCH 0943/1846] Refactor code style --- dexbot/strategies/external_feeds/ccxt_feed.py | 49 ++++++++----------- .../strategies/external_feeds/gecko_feed.py | 36 +++++++------- .../strategies/external_feeds/process_pair.py | 7 ++- dexbot/strategies/external_feeds/styles.py | 3 +- .../external_feeds/tests/async_feeds_test.py | 4 +- 5 files changed, 47 insertions(+), 52 deletions(-) diff --git a/dexbot/strategies/external_feeds/ccxt_feed.py b/dexbot/strategies/external_feeds/ccxt_feed.py index 96850ea21..cd8b657a0 100644 --- a/dexbot/strategies/external_feeds/ccxt_feed.py +++ b/dexbot/strategies/external_feeds/ccxt_feed.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import click import asyncio import functools @@ -20,14 +19,11 @@ def get_ticker(exchange, symbol): except ccxt.DDoSProtection as e: print(type(e).__name__, e.args, 'DDoS Protection (ignoring)') except ccxt.RequestTimeout as e: - print(type(e).__name__, e.args, - 'Request Timeout (ignoring)') + print(type(e).__name__, e.args, 'Request Timeout (ignoring)') except ccxt.ExchangeNotAvailable as e: - print(type(e).__name__, e.args, - 'Exchange Not Available due to downtime or maintenance (ignoring)') + print(type(e).__name__, e.args, 'Exchange Not Available due to downtime or maintenance (ignoring)') except ccxt.AuthenticationError as e: - print(type(e).__name__, e.args, - 'Authentication Error (missing API keys, ignoring)') + print(type(e).__name__, e.args, 'Authentication Error (missing API keys, ignoring)') return ticker @@ -36,14 +32,16 @@ async def fetch_ticker(exchange, symbol): await exchange.close() return ticker + async def print_ticker(symbol, id): - # verbose mode will show the order of execution to verify concurrency + # Verbose mode will show the order of execution to verify concurrency exchange = getattr(accxt, id)({'verbose': True}) print(await exchange.fetch_ticker(symbol)) await exchange.close() -# unit tests +# Unit tests +# Todo: Move tests to own files @click.group() def main(): pass @@ -51,9 +49,7 @@ def main(): @main.command() def test_async2(): - """ - get all tickers from multiple exchanges using async - """ + """ Get all tickers from multiple exchanges using async """ symbol = 'ETH/BTC' print_ethbtc_ticker = functools.partial(print_ticker, symbol) [asyncio.ensure_future(print_ethbtc_ticker(id)) for id in [ @@ -70,9 +66,7 @@ def test_async2(): @main.command() def test_async(): - """ - get ticker for bitfinex using async - """ + """ Get ticker for bitfinex using async """ bitfinex = accxt.bitfinex({'enableRateLimit': True, }) ticker = asyncio.get_event_loop().run_until_complete( fetch_ticker(bitfinex, 'BTC/USDT')) @@ -83,27 +77,26 @@ def test_async(): @click.argument('exchange') @click.argument('symbol') def test_feed(exchange, symbol): - """ - Usage: exchange [symbol] - Symbol is required, for example: - python ccxt_feed.py test_feed gdax BTC/USD + """ Usage: exchange [symbol] + Symbol is required, for example: + python ccxt_feed.py test_feed gdax BTC/USD """ usage = "Usage: python ccxt_feed.py id [symbol]" try: - id = exchange # get exchange id from command line arguments + id = exchange # Get exchange id from command line arguments exchange_found = id in ccxt.exchanges - # check if the exchange is supported by ccxt + # Check if the exchange is supported by ccxt if exchange_found: print_args('Instantiating', green(id)) # instantiate the exchange by id - exch = getattr(ccxt, id)() + exchange = getattr(ccxt, id)() # load all markets from the exchange - markets = exch.load_markets() + markets = exchange.load_markets() sym = symbol if sym: - ticker = get_ticker(exch, sym) + ticker = get_ticker(exchange, sym) print_args( - green(exch.id), + green(exchange.id), yellow(sym), 'ticker', ticker['datetime'], @@ -114,7 +107,7 @@ def test_feed(exchange, symbol): 'volume: ' + str(ticker['quoteVolume'])) else: print_args('Symbol not found') - print_exchange_symbols(exch) + print_exchange_symbols(exchange) print(usage) else: print_args('Exchange ' + red(id) + ' not found') @@ -130,9 +123,9 @@ def test_exch_list(): gets a list of supported exchanges """ supported_exchanges = get_exchanges() - exch_list = ', '.join(str(name) for name in supported_exchanges) + exchange_list = ', '.join(str(name) for name in supported_exchanges) print(bold(underline('Supported exchanges: '))) - pprint(exch_list, width=80) + pprint(exchange_list, width=80) if __name__ == '__main__': diff --git a/dexbot/strategies/external_feeds/gecko_feed.py b/dexbot/strategies/external_feeds/gecko_feed.py index d77035523..4d38fb02e 100644 --- a/dexbot/strategies/external_feeds/gecko_feed.py +++ b/dexbot/strategies/external_feeds/gecko_feed.py @@ -1,15 +1,13 @@ -# Python imports -import requests -import json -import sys import click -from styles import yellow -from process_pair import split_pair, filter_prefix_symbol, filter_bit_symbol +import requests + +from dexbot.strategies.external_feeds.styles import yellow +from dexbot.strategies.external_feeds.process_pair import split_pair, filter_prefix_symbol, filter_bit_symbol """ -To use Gecko API, note that gecko does not provide pairs by default. -For base/quote one must be listed as ticker and the other as fullname, -i.e. BTCUSD is vs_currency = usd , ids = bitcoin -https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin + To use Gecko API, note that gecko does not provide pairs by default. + For base/quote one must be listed as ticker and the other as fullname, + i.e. BTCUSD is vs_currency = usd , ids = bitcoin + https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin """ GECKO_COINS_URL = 'https://api.coingecko.com/api/v3/coins/' isDebug = False @@ -22,7 +20,7 @@ def debug(*args): def print_usage(): print("Usage: python3 gecko_feed.py", yellow('[symbol]'), - "Symbol is required, for example:", yellow('BTC/USD'), sep='') + "Symbol is required, for example:", yellow('BTC/USD'), sep='') def get_gecko_json(url): @@ -31,9 +29,9 @@ def get_gecko_json(url): return json_obj -def check_gecko_symbol_exists(coinlist, symbol): +def check_gecko_symbol_exists(coin_list, symbol): try: - symbol_name = [obj for obj in coinlist if obj['symbol'] == symbol][0]['id'] + symbol_name = [obj for obj in coin_list if obj['symbol'] == symbol][0]['id'] return symbol_name except IndexError: return None @@ -59,6 +57,7 @@ def get_gecko_market_price(base, quote): # Unit tests +# Todo: Move tests to own files @click.group() def main(): pass @@ -68,7 +67,7 @@ def main(): @click.argument('symbol') def test_feed(symbol): """ - [symbol] Symbol example: btc/usd or btc:usd + [symbol] Symbol example: btc/usd or btc:usd """ try: pair = split_pair(symbol) # pair=[quote, base] @@ -77,13 +76,14 @@ def test_feed(symbol): new_quote = filtered_pair[0] new_base = filtered_pair[1] current_price = get_gecko_market_price(new_base, new_quote) - if current_price is None: # try inverted version + + if current_price is None: # Try inverted version debug(" Trying pair inversion...") current_price = get_gecko_market_price(new_quote, new_base) - print(new_base+"/"+new_quote, str(current_price), sep=':') - if current_price is not None: # re-invert price + print(new_base + '/' + new_quote, str(current_price), sep=':') + if current_price is not None: # Re-invert price actual_price = 1/current_price - print(new_quote+"/"+new_base, str(actual_price), sep=':') + print(new_quote + '/' + new_base, str(actual_price), sep=':') else: print(symbol, current_price, sep=':') except Exception as e: diff --git a/dexbot/strategies/external_feeds/process_pair.py b/dexbot/strategies/external_feeds/process_pair.py index c5fb18a0d..ba6a4739a 100644 --- a/dexbot/strategies/external_feeds/process_pair.py +++ b/dexbot/strategies/external_feeds/process_pair.py @@ -1,4 +1,3 @@ -# Python imports import re @@ -7,7 +6,7 @@ def print_args(*args): def filter_prefix_symbol(symbol): - # example open.USD or bridge.USD, remove leading bit up to . + # Example open.USD or bridge.USD, remove leading bit up to . base = '' if re.match(r'^[a-zA-Z](.*)\.(.*)', symbol): base = re.sub('(.*)\.', '', symbol) @@ -32,14 +31,14 @@ def split_pair(symbol): def get_consolidated_pair(base, quote): - # split into two USD pairs, STEEM/BTS=(BTS/USD * USD/STEEM) + # Split into two USD pairs, STEEM/BTS=(BTS/USD * USD/STEEM) pair1 = [base, 'USD'] # BTS/USD pair=[quote, base] pair2 = ['USD', quote] return pair1, pair2 # Unit Tests - +# Todo: Move tests to own files def test_consolidated_pair(): symbol = 'STEEM:BTS' # pair = 'STEEM:BTS' or STEEM/BTS' pair = split_pair(symbol) diff --git a/dexbot/strategies/external_feeds/styles.py b/dexbot/strategies/external_feeds/styles.py index 76035b8d6..ca5d0e300 100644 --- a/dexbot/strategies/external_feeds/styles.py +++ b/dexbot/strategies/external_feeds/styles.py @@ -34,7 +34,8 @@ def underline(s): if __name__ == '__main__': - # unit test + # Unit test + # Todo: Move tests to own files print(green("green style test")) print(blue("blue style test")) print(yellow("yellow style test")) diff --git a/dexbot/strategies/external_feeds/tests/async_feeds_test.py b/dexbot/strategies/external_feeds/tests/async_feeds_test.py index cea0abe51..1c4339a3c 100644 --- a/dexbot/strategies/external_feeds/tests/async_feeds_test.py +++ b/dexbot/strategies/external_feeds/tests/async_feeds_test.py @@ -3,9 +3,10 @@ gecko_coins_url = 'https://api.coingecko.com/api/v3/coins/' waves_symbols = 'http://marketdata.wavesplatform.com/api/symbols' -cwatch_assets='https://api.cryptowat.ch/assets' +cwatch_assets = 'https://api.cryptowat.ch/assets' urls = [cwatch_assets, waves_symbols, gecko_coins_url] + @asyncio.coroutine def call_url(url): print('Starting {}'.format(url)) @@ -14,5 +15,6 @@ def call_url(url): print('url: {} bytes: {}'.format(url, len(data))) return data + futures = [call_url(url) for url in urls] asyncio.run(asyncio.wait(futures)) From e5ddfe9022f268da2a143bedaa04b17f1381794f Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 5 Nov 2018 10:42:02 +0200 Subject: [PATCH 0944/1846] Remove aiohttp forced version 3.4.4 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a8a61da3b..22fb689dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pyqt-distutils==0.7.3 pyinstaller==3.3.1 click-datetime==0.2 cryptography==2.3 -aiohttp==3.4.4 +aiohttp requests>=2.20.0 yarl==1.1.0 ccxt==1.17.434 From b016e8418facfd63c398b73abb3870272cd5252a Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 7 Nov 2018 15:11:38 +0200 Subject: [PATCH 0945/1846] Refactor gui.py variables --- dexbot/gui.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/dexbot/gui.py b/dexbot/gui.py index d1543f70b..e0fe5f081 100644 --- a/dexbot/gui.py +++ b/dexbot/gui.py @@ -2,16 +2,17 @@ from dexbot.config import Config from dexbot.controllers.main_controller import MainController -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 +from dexbot.views.unlock_wallet import UnlockWalletView +from dexbot.views.worker_list import MainView from PyQt5.Qt import QApplication from bitshares import BitShares class App(QApplication): + def __init__(self, sys_argv): super(App, self).__init__(sys_argv) @@ -19,16 +20,16 @@ def __init__(self, sys_argv): bitshares_instance = BitShares(config['node'], num_retries=-1) # Wallet unlock - unlock_ctrl = WalletController(bitshares_instance) - if unlock_ctrl.wallet_created(): - unlock_view = UnlockWalletView(unlock_ctrl) + wallet_controller = WalletController(bitshares_instance) + if wallet_controller.wallet_created(): + unlock_view = UnlockWalletView(wallet_controller) else: - unlock_view = CreateWalletView(unlock_ctrl) + unlock_view = CreateWalletView(wallet_controller) if unlock_view.exec_(): - bitshares_instance = unlock_ctrl.bitshares - self.main_ctrl = MainController(bitshares_instance, config) - self.main_view = MainView(self.main_ctrl) + bitshares_instance = wallet_controller.bitshares + self.main_controller = MainController(bitshares_instance, config) + self.main_view = MainView(self.main_controller) self.main_view.show() else: sys.exit() From c88450fd18afe125cf80280921609e6a13855bf3 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 7 Nov 2018 15:15:47 +0200 Subject: [PATCH 0946/1846] Refactor config.py --- dexbot/cli.py | 2 +- dexbot/config.py | 37 ++++++++++++++++------- dexbot/controllers/settings_controller.py | 2 +- dexbot/views/worker_item.py | 2 +- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index e778f4644..c05d10a64 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -143,7 +143,7 @@ def configure(ctx): config = Config(path=ctx.obj['configfile']) configure_dexbot(config, ctx) - config.save_config() + config.save() click.echo("New configuration saved") if config.get('systemd_status', 'disabled') == 'enabled': diff --git a/dexbot/config.py b/dexbot/config.py index b5ac113d2..3e6aeb874 100644 --- a/dexbot/config.py +++ b/dexbot/config.py @@ -20,6 +20,8 @@ def __init__(self, config=None, path=None): :param str path: path to the config file """ super().__init__() + + # Check if using custom path for the config if path: self.config_dir = os.path.dirname(path) self.config_file = path @@ -28,18 +30,21 @@ def __init__(self, config=None, path=None): self.config_file = DEFAULT_CONFIG_FILE if config: - self.create_config(config, self.config_file) - self._config = self.load_config(self.config_file) + self.create(config, self.config_file) else: if not os.path.isfile(self.config_file): - self.create_config(self.default_data, self.config_file) - self._config = self.load_config(self.config_file) + # Config file was not found, creating default config+ + self.create(self.default_data, self.config_file) + + # Load config to cache from the configuration file + self._config = self.load(self.config_file) # In case there is not a list of nodes in the config file, # the node will be replaced by a list of pre-defined nodes. + # This prevents deleting worker config in a case where user has deleted nodes but left worker if isinstance(self._config['node'], str): self._config['node'] = self.node_list - self.save_config() + self.save() def __setitem__(self, key, value): self._config[key] = value @@ -58,7 +63,13 @@ def get(self, key, default=None): @property def default_data(self): - return {'node': self.node_list, 'workers': {}} + """ Default data used to create an empty configuration file + :return: dict: node list and workers dict + """ + return { + 'node': self.node_list, + 'workers': {} + } @property def workers_data(self): @@ -72,7 +83,7 @@ def dict(self): return self._config @staticmethod - def create_config(config, path=None): + def create(config, path=None): if not path: config_dir = DEFAULT_CONFIG_DIR config_file = DEFAULT_CONFIG_FILE @@ -87,19 +98,19 @@ def create_config(config, path=None): yaml.dump(config, f, default_flow_style=False) @staticmethod - def load_config(path=None): + def load(path=None): if not path: path = DEFAULT_CONFIG_FILE with open(path, 'r') as f: return Config.ordered_load(f, loader=yaml.SafeLoader) - def save_config(self): + def save(self): with open(self.config_file, 'w') as f: yaml.dump(self._config, f, default_flow_style=False) def refresh_config(self): - self._config = self.load_config(self.config_file) + self._config = self.load(self.config_file) @staticmethod def get_worker_config_file(worker_name, path=None): @@ -123,7 +134,11 @@ def get_worker_config(self, worker_name): config['workers'] = OrderedDict({worker_name: config['workers'][worker_name]}) return config - def remove_worker_config(self, worker_name): + # Return only the config items matching the worker_name + # config = self.workers_data.get(worker_name) + # return config + + def remove_worker(self, worker_name): self._config['workers'].pop(worker_name, None) with open(self.config_file, 'w') as f: diff --git a/dexbot/controllers/settings_controller.py b/dexbot/controllers/settings_controller.py index 374419afd..ff6269c17 100644 --- a/dexbot/controllers/settings_controller.py +++ b/dexbot/controllers/settings_controller.py @@ -107,7 +107,7 @@ def save_nodes_to_config(self, nodes): nodes = self.remove_empty_items(nodes) self.config['node'] = nodes - self.config.save_config() + self.config.save() # Update status self.view.notification_label.setText('Settings successfully saved!') diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 217aa0218..5415cbcb7 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -143,7 +143,7 @@ def remove_widget_dialog(self): def remove_widget(self): self.main_ctrl.remove_worker(self.worker_name) self.view.remove_worker_widget(self.worker_name) - self.main_ctrl.config.remove_worker_config(self.worker_name) + self.main_controller.config.remove_worker(self.worker_name) self.deleteLater() def reload_widget(self, worker_name): From ec112796ef183dcb413d2266ee1d84eedf91a7c0 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 7 Nov 2018 15:16:04 +0200 Subject: [PATCH 0947/1846] Fix typo in worker_controller.py --- dexbot/controllers/worker_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index a6d1fae34..33c9256a7 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -84,8 +84,8 @@ def get_strategy_mode(worker_data): return worker_data['mode'] @staticmethod - def get_allow_instant_fill(worder_data): - return worder_data['allow_instant_fill'] + def get_allow_instant_fill(worker_data): + return worker_data['allow_instant_fill'] @staticmethod def get_assets(worker_data): From 21af52ca9814722d79e2c04298239bc5a46ed25b Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 7 Nov 2018 15:16:34 +0200 Subject: [PATCH 0948/1846] Change create_worker() in main_controller.py --- dexbot/controllers/main_controller.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index ebe422e5a..8620b5c3d 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -6,6 +6,7 @@ from dexbot.helper import initialize_orders_log, initialize_data_folders from dexbot.worker import WorkerInfrastructure from dexbot.views.errors import PyQtHandler +from dexbot.storage import Storage from appdirs import user_data_dir from bitshares.instance import set_shared_bitshares_instance @@ -16,6 +17,8 @@ class MainController: def __init__(self, bitshares_instance, config): self.bitshares_instance = bitshares_instance set_shared_bitshares_instance(bitshares_instance) + + # Global configuration which includes all the workers self.config = config self.worker_manager = None @@ -77,5 +80,10 @@ def remove_worker(self, worker_name): @staticmethod def create_worker(worker_name): - # Deletes old worker's data + # Todo: Rename this function to something better + # In case worker is deleted only from config file, there are still information with the name in the database + # This function removes all that data and cancels orders so that new worker can take the name in it's use WorkerInfrastructure.remove_offline_worker_data(worker_name) + + # Wound't this be just shortcut to achieve the same thing?? + # Storage.clear_worker_data(worker_name) From c3b1e392838aaeeddf4c091d1cb04bab107b8b8d Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 7 Nov 2018 15:19:08 +0200 Subject: [PATCH 0949/1846] Remove config_lock --- dexbot/worker.py | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/dexbot/worker.py b/dexbot/worker.py index f5f440378..8e87072f2 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -36,7 +36,6 @@ def __init__( self.view = view self.jobs = set() self.notify = None - self.config_lock = threading.RLock() self.workers = {} self.accounts = set() @@ -50,7 +49,6 @@ 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={ @@ -82,7 +80,6 @@ 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']: @@ -114,7 +111,6 @@ def on_block(self, data): 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 @@ -126,13 +122,11 @@ def on_block(self, data): 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): 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)) @@ -146,10 +140,8 @@ def on_market(self, data): 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): - self.config_lock.acquire() account = account_update.account for worker_name, worker in self.config["workers"].items(): if self.workers[worker_name].disabled: @@ -164,12 +156,10 @@ def on_account(self, account_update): 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): - with self.config_lock: - self.config['workers'][worker_name] = config['workers'][worker_name] - self.init_workers(config) + self.config['workers'][worker_name] = config['workers'][worker_name] + self.init_workers(config) self.update_notify() def run(self): @@ -191,9 +181,9 @@ def stop(self, worker_name=None, pause=False): # Worker was not found meaning it does not exist or it is paused already return - with self.config_lock: - account = self.config['workers'][worker_name]['account'] - self.config['workers'].pop(worker_name) + # with self.config_lock: + account = self.config['workers'][worker_name]['account'] + self.config['workers'].pop(worker_name) self.accounts.remove(account) if pause: @@ -222,23 +212,25 @@ def remove_worker(self, worker_name=None): 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) + # 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, bitshares_instance): # Initialize the base strategy to get control over the data strategy = StrategyBase(worker_name, config, bitshares_instance=bitshares_instance) + # Purge all worker data and cancel orders strategy.purge() @staticmethod def remove_offline_worker_data(worker_name): + # Remove all worker data, but don't cancel orders StrategyBase.purge_all_local_worker_data(worker_name) def do_next_tick(self, job): From 83b612886a909b0ffd23b2b66fb4d96f30175601 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 7 Nov 2018 15:19:36 +0200 Subject: [PATCH 0950/1846] Remove unused import --- dexbot/worker.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dexbot/worker.py b/dexbot/worker.py index 8e87072f2..7f38e7bcf 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -8,7 +8,6 @@ import dexbot.errors as errors from dexbot.strategies.base import StrategyBase -from bitshares import BitShares from bitshares.notify import Notify from bitshares.instance import shared_bitshares_instance From 7e51719f8681ce18882fee7ef4e60bfe8c9b9541 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 7 Nov 2018 15:20:27 +0200 Subject: [PATCH 0951/1846] WIP Refactor names and Change config --- dexbot/views/worker_item.py | 27 +++++++++-------- dexbot/views/worker_list.py | 60 ++++++++++++++++++++++++------------- dexbot/worker.py | 18 ++++++----- 3 files changed, 64 insertions(+), 41 deletions(-) diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 5415cbcb7..974f56541 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -13,14 +13,14 @@ class WorkerItemWidget(QtWidgets.QWidget, Ui_widget): - def __init__(self, worker_name, config, main_ctrl, view): + def __init__(self, worker_name, config, main_controller, view): super().__init__() - self.main_ctrl = main_ctrl - self.running = False self.worker_name = worker_name - self.worker_config = self.main_ctrl.config.get_worker_config(worker_name) + self.worker_config = config + self.main_controller = main_controller self.view = view + self.running = False self.setupUi(self) @@ -73,8 +73,9 @@ def _toggle_worker(self, toggle_label_text, toggle_alignment): @gui_error def start_worker(self): self.set_status("Starting worker") + # thread.start() HERE self._start_worker() - self.main_ctrl.start_worker(self.worker_name, self.worker_config, self.view) + self.main_controller.start_worker(self.worker_name, self.worker_config, self.view) def _start_worker(self): self.running = True @@ -84,7 +85,7 @@ def _start_worker(self): def pause_worker(self): self.set_status("Pausing worker") self._pause_worker() - self.main_ctrl.pause_worker(self.worker_name) + self.main_controller.pause_worker(self.worker_name) def _pause_worker(self): self.running = False @@ -141,7 +142,7 @@ def remove_widget_dialog(self): self.remove_widget() def remove_widget(self): - self.main_ctrl.remove_worker(self.worker_name) + self.main_controller.remove_worker(self.worker_name) self.view.remove_worker_widget(self.worker_name) self.main_controller.config.remove_worker(self.worker_name) self.deleteLater() @@ -149,7 +150,7 @@ def remove_widget(self): def reload_widget(self, worker_name): """ Reload the data of the widget """ - self.worker_config = self.main_ctrl.config.get_worker_config(worker_name) + self.worker_config = self.main_controller.config.get_worker_config(worker_name) self.setup_ui_data(self.worker_config) self._pause_worker() @@ -159,7 +160,7 @@ def handle_open_details(self): @gui_error def handle_edit_worker(self): - edit_worker_dialog = EditWorkerView(self, self.main_ctrl.bitshares_instance, + edit_worker_dialog = EditWorkerView(self, self.main_controller.bitshares_instance, self.worker_name, self.worker_config) return_value = edit_worker_dialog.exec_() @@ -167,10 +168,10 @@ def handle_edit_worker(self): if return_value: new_worker_name = edit_worker_dialog.worker_name self.view.change_worker_widget_name(self.worker_name, new_worker_name) - self.main_ctrl.pause_worker(self.worker_name, config=self.worker_config) - self.main_ctrl.config.replace_worker_config(self.worker_name, - new_worker_name, - edit_worker_dialog.worker_data) + self.main_controller.pause_worker(self.worker_name, config=self.worker_config) + self.main_controller.config.replace_worker_config(self.worker_name, + new_worker_name, + edit_worker_dialog.worker_data) self.worker_name = new_worker_name self.reload_widget(new_worker_name) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index beba741ac..ec16463ef 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -12,38 +12,56 @@ from .errors import gui_error from .layouts.flow_layout import FlowLayout -from PyQt5 import QtGui, QtWidgets +from PyQt5 import QtGui +from PyQt5.QtCore import QThreadPool +from PyQt5.QtWidgets import QMainWindow from bitsharesapi.bitsharesnoderpc import BitSharesNodeRPC -class MainView(QtWidgets.QMainWindow, Ui_MainWindow): +class MainView(QMainWindow, Ui_MainWindow): - def __init__(self, main_ctrl): + def __init__(self, main_controller): super().__init__() self.setupUi(self) - self.main_ctrl = main_ctrl + self.main_controller = main_controller - self.config = main_ctrl.config + # Global configuration + self.config = self.main_controller.config + + # View settings self.max_workers = 10 - self.num_of_workers = 0 + + # Number of active workers + self.num_of_active_workers = 0 + + # Worker item widgets on the main view self.worker_widgets = {} + self.closing = False self.statusbar_updater = None self.statusbar_updater_first_run = True - self.main_ctrl.set_info_handler(self.set_worker_status) + self.main_controller.set_info_handler(self.set_worker_status) self.layout = FlowLayout(self.scrollAreaContent) + # View events self.add_worker_button.clicked.connect(lambda: self.handle_add_worker()) self.settings_button.clicked.connect(lambda: self.handle_open_settings()) self.help_button.clicked.connect(lambda: self.handle_open_documentation()) + # Configure threading + self.thread_pool = QThreadPool() + # Create thread pool size of the maximum number of workers. + self.thread_pool.setMaxThreadCount(self.max_workers) + print('Multi threading with max {} threads'.format(self.thread_pool.maxThreadCount())) + # Load worker widgets from config file - workers = self.config.workers_data - for worker_name in workers: + # Todo: THREADING HERE + # Assign thread for each worker item widget and then set it up so that start worker button starts the thread + for worker_name in self.config.workers_data: self.add_worker_widget(worker_name) # Limit the max amount of workers so that the performance isn't greatly affected - if self.num_of_workers >= self.max_workers: + if self.num_of_active_workers >= self.max_workers: self.add_worker_button.setEnabled(False) break @@ -51,31 +69,31 @@ def __init__(self, main_ctrl): self.dispatcher = ThreadDispatcher(self) self.dispatcher.start() + # Statusbar updater self.status_bar.showMessage("ver {} - Node delay: - ms".format(__version__)) - self.statusbar_updater = Thread( - target=self._update_statusbar_message - ) + self.statusbar_updater = Thread(target=self._update_statusbar_message) self.statusbar_updater.start() QtGui.QFontDatabase.addApplicationFont(":/bot_widget/font/SourceSansPro-Bold.ttf") def add_worker_widget(self, worker_name): - config = self.config.get_worker_config(worker_name) - widget = WorkerItemWidget(worker_name, config, self.main_ctrl, self) + config = self.main_controller.config.get_worker_config(worker_name) + + widget = WorkerItemWidget(worker_name, config, self.main_controller, view=self) widget.setFixedSize(widget.frameSize()) self.layout.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.num_of_active_workers += 1 + if self.num_of_active_workers >= self.max_workers: self.add_worker_button.setEnabled(False) def remove_worker_widget(self, worker_name): self.worker_widgets.pop(worker_name, None) - self.num_of_workers -= 1 - if self.num_of_workers < self.max_workers: + self.num_of_active_workers -= 1 + if self.num_of_active_workers < self.max_workers: self.add_worker_button.setEnabled(True) def change_worker_widget_name(self, old_worker_name, new_worker_name): @@ -84,13 +102,13 @@ def change_worker_widget_name(self, old_worker_name, new_worker_name): @gui_error def handle_add_worker(self): - create_worker_dialog = CreateWorkerView(self.main_ctrl.bitshares_instance) + create_worker_dialog = CreateWorkerView(self.main_controller.bitshares_instance) return_value = create_worker_dialog.exec_() # User clicked save if return_value == 1: worker_name = create_worker_dialog.worker_name - self.main_ctrl.create_worker(worker_name) + self.main_controller.create_worker(worker_name) self.config.add_worker_config(worker_name, create_worker_dialog.worker_data) self.add_worker_widget(worker_name) diff --git a/dexbot/worker.py b/dexbot/worker.py index 7f38e7bcf..987dcb664 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -21,20 +21,24 @@ class WorkerInfrastructure(threading.Thread): - def __init__( - self, - config, - bitshares_instance=None, - view=None - ): + def __init__(self, config, bitshares_instance=None, view=None): super().__init__() # BitShares instance self.bitshares = bitshares_instance or shared_bitshares_instance() - self.config = copy.deepcopy(config) + + # Global configuration file including all the workers + # Why is this deep copied? + # self.config = copy.deepcopy(config) + self.config = config + + # Main view self.view = view + self.jobs = set() self.notify = None + + # Active workers self.workers = {} self.accounts = set() From 29804656c055ffc6a361c51c4c384b999109d46d Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 7 Nov 2018 15:28:02 +0200 Subject: [PATCH 0952/1846] Revert "WIP Refactor names and Change config" This reverts commit 7e51719f8681ce18882fee7ef4e60bfe8c9b9541. --- dexbot/views/worker_item.py | 27 ++++++++--------- dexbot/views/worker_list.py | 60 +++++++++++++------------------------ dexbot/worker.py | 18 +++++------ 3 files changed, 41 insertions(+), 64 deletions(-) diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 974f56541..5415cbcb7 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -13,14 +13,14 @@ class WorkerItemWidget(QtWidgets.QWidget, Ui_widget): - def __init__(self, worker_name, config, main_controller, view): + 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.worker_config = config - self.main_controller = main_controller + self.worker_config = self.main_ctrl.config.get_worker_config(worker_name) self.view = view - self.running = False self.setupUi(self) @@ -73,9 +73,8 @@ def _toggle_worker(self, toggle_label_text, toggle_alignment): @gui_error def start_worker(self): self.set_status("Starting worker") - # thread.start() HERE self._start_worker() - self.main_controller.start_worker(self.worker_name, self.worker_config, self.view) + self.main_ctrl.start_worker(self.worker_name, self.worker_config, self.view) def _start_worker(self): self.running = True @@ -85,7 +84,7 @@ def _start_worker(self): def pause_worker(self): self.set_status("Pausing worker") self._pause_worker() - self.main_controller.pause_worker(self.worker_name) + self.main_ctrl.pause_worker(self.worker_name) def _pause_worker(self): self.running = False @@ -142,7 +141,7 @@ def remove_widget_dialog(self): self.remove_widget() def remove_widget(self): - self.main_controller.remove_worker(self.worker_name) + self.main_ctrl.remove_worker(self.worker_name) self.view.remove_worker_widget(self.worker_name) self.main_controller.config.remove_worker(self.worker_name) self.deleteLater() @@ -150,7 +149,7 @@ def remove_widget(self): def reload_widget(self, worker_name): """ Reload the data of the widget """ - self.worker_config = self.main_controller.config.get_worker_config(worker_name) + self.worker_config = self.main_ctrl.config.get_worker_config(worker_name) self.setup_ui_data(self.worker_config) self._pause_worker() @@ -160,7 +159,7 @@ def handle_open_details(self): @gui_error def handle_edit_worker(self): - edit_worker_dialog = EditWorkerView(self, self.main_controller.bitshares_instance, + edit_worker_dialog = EditWorkerView(self, self.main_ctrl.bitshares_instance, self.worker_name, self.worker_config) return_value = edit_worker_dialog.exec_() @@ -168,10 +167,10 @@ def handle_edit_worker(self): if return_value: new_worker_name = edit_worker_dialog.worker_name self.view.change_worker_widget_name(self.worker_name, new_worker_name) - self.main_controller.pause_worker(self.worker_name, config=self.worker_config) - self.main_controller.config.replace_worker_config(self.worker_name, - new_worker_name, - edit_worker_dialog.worker_data) + self.main_ctrl.pause_worker(self.worker_name, config=self.worker_config) + self.main_ctrl.config.replace_worker_config(self.worker_name, + new_worker_name, + edit_worker_dialog.worker_data) self.worker_name = new_worker_name self.reload_widget(new_worker_name) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index ec16463ef..beba741ac 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -12,56 +12,38 @@ from .errors import gui_error from .layouts.flow_layout import FlowLayout -from PyQt5 import QtGui -from PyQt5.QtCore import QThreadPool -from PyQt5.QtWidgets import QMainWindow +from PyQt5 import QtGui, QtWidgets from bitsharesapi.bitsharesnoderpc import BitSharesNodeRPC -class MainView(QMainWindow, Ui_MainWindow): +class MainView(QtWidgets.QMainWindow, Ui_MainWindow): - def __init__(self, main_controller): + def __init__(self, main_ctrl): super().__init__() self.setupUi(self) - self.main_controller = main_controller + self.main_ctrl = main_ctrl - # Global configuration - self.config = self.main_controller.config - - # View settings + self.config = main_ctrl.config self.max_workers = 10 - - # Number of active workers - self.num_of_active_workers = 0 - - # Worker item widgets on the main view + self.num_of_workers = 0 self.worker_widgets = {} - self.closing = False self.statusbar_updater = None self.statusbar_updater_first_run = True - self.main_controller.set_info_handler(self.set_worker_status) + self.main_ctrl.set_info_handler(self.set_worker_status) self.layout = FlowLayout(self.scrollAreaContent) - # View events self.add_worker_button.clicked.connect(lambda: self.handle_add_worker()) self.settings_button.clicked.connect(lambda: self.handle_open_settings()) self.help_button.clicked.connect(lambda: self.handle_open_documentation()) - # Configure threading - self.thread_pool = QThreadPool() - # Create thread pool size of the maximum number of workers. - self.thread_pool.setMaxThreadCount(self.max_workers) - print('Multi threading with max {} threads'.format(self.thread_pool.maxThreadCount())) - # Load worker widgets from config file - # Todo: THREADING HERE - # Assign thread for each worker item widget and then set it up so that start worker button starts the thread - for worker_name in self.config.workers_data: + workers = self.config.workers_data + 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 - if self.num_of_active_workers >= self.max_workers: + if self.num_of_workers >= self.max_workers: self.add_worker_button.setEnabled(False) break @@ -69,31 +51,31 @@ def __init__(self, main_controller): self.dispatcher = ThreadDispatcher(self) self.dispatcher.start() - # Statusbar updater self.status_bar.showMessage("ver {} - Node delay: - ms".format(__version__)) - self.statusbar_updater = Thread(target=self._update_statusbar_message) + self.statusbar_updater = Thread( + target=self._update_statusbar_message + ) self.statusbar_updater.start() QtGui.QFontDatabase.addApplicationFont(":/bot_widget/font/SourceSansPro-Bold.ttf") def add_worker_widget(self, worker_name): - config = self.main_controller.config.get_worker_config(worker_name) - - widget = WorkerItemWidget(worker_name, config, self.main_controller, view=self) + config = self.config.get_worker_config(worker_name) + widget = WorkerItemWidget(worker_name, config, self.main_ctrl, self) widget.setFixedSize(widget.frameSize()) self.layout.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_active_workers += 1 - if self.num_of_active_workers >= self.max_workers: + self.num_of_workers += 1 + if self.num_of_workers >= self.max_workers: self.add_worker_button.setEnabled(False) def remove_worker_widget(self, worker_name): self.worker_widgets.pop(worker_name, None) - self.num_of_active_workers -= 1 - if self.num_of_active_workers < self.max_workers: + self.num_of_workers -= 1 + if self.num_of_workers < self.max_workers: self.add_worker_button.setEnabled(True) def change_worker_widget_name(self, old_worker_name, new_worker_name): @@ -102,13 +84,13 @@ def change_worker_widget_name(self, old_worker_name, new_worker_name): @gui_error def handle_add_worker(self): - create_worker_dialog = CreateWorkerView(self.main_controller.bitshares_instance) + create_worker_dialog = CreateWorkerView(self.main_ctrl.bitshares_instance) return_value = create_worker_dialog.exec_() # User clicked save if return_value == 1: worker_name = create_worker_dialog.worker_name - self.main_controller.create_worker(worker_name) + self.main_ctrl.create_worker(worker_name) self.config.add_worker_config(worker_name, create_worker_dialog.worker_data) self.add_worker_widget(worker_name) diff --git a/dexbot/worker.py b/dexbot/worker.py index 987dcb664..7f38e7bcf 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -21,24 +21,20 @@ class WorkerInfrastructure(threading.Thread): - def __init__(self, config, bitshares_instance=None, view=None): + def __init__( + self, + config, + bitshares_instance=None, + view=None + ): super().__init__() # BitShares instance self.bitshares = bitshares_instance or shared_bitshares_instance() - - # Global configuration file including all the workers - # Why is this deep copied? - # self.config = copy.deepcopy(config) - self.config = config - - # Main view + self.config = copy.deepcopy(config) self.view = view - self.jobs = set() self.notify = None - - # Active workers self.workers = {} self.accounts = set() From 97ba41b34db4c4418804e2c5c82c5612b7d6847a Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 7 Nov 2018 15:28:57 +0200 Subject: [PATCH 0953/1846] Revert "Remove unused import" This reverts commit 83b612886a909b0ffd23b2b66fb4d96f30175601. --- dexbot/worker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dexbot/worker.py b/dexbot/worker.py index 7f38e7bcf..8e87072f2 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -8,6 +8,7 @@ import dexbot.errors as errors from dexbot.strategies.base import StrategyBase +from bitshares import BitShares from bitshares.notify import Notify from bitshares.instance import shared_bitshares_instance From 32a7c171aca1bfca0e74e8fd1df6467789f2b12f Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 7 Nov 2018 15:29:12 +0200 Subject: [PATCH 0954/1846] Revert "Remove config_lock" This reverts commit c3b1e392838aaeeddf4c091d1cb04bab107b8b8d. --- dexbot/worker.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/dexbot/worker.py b/dexbot/worker.py index 8e87072f2..f5f440378 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -36,6 +36,7 @@ def __init__( self.view = view self.jobs = set() self.notify = None + self.config_lock = threading.RLock() self.workers = {} self.accounts = set() @@ -49,6 +50,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 +82,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']: @@ -111,6 +114,7 @@ def on_block(self, data): 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 @@ -122,11 +126,13 @@ def on_block(self, data): 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): 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)) @@ -140,8 +146,10 @@ def on_market(self, data): 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): + self.config_lock.acquire() account = account_update.account for worker_name, worker in self.config["workers"].items(): if self.workers[worker_name].disabled: @@ -156,10 +164,12 @@ def on_account(self, account_update): 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): - self.config['workers'][worker_name] = config['workers'][worker_name] - self.init_workers(config) + with self.config_lock: + self.config['workers'][worker_name] = config['workers'][worker_name] + self.init_workers(config) self.update_notify() def run(self): @@ -181,9 +191,9 @@ def stop(self, worker_name=None, pause=False): # Worker was not found meaning it does not exist or it is paused already return - # with self.config_lock: - account = self.config['workers'][worker_name]['account'] - self.config['workers'].pop(worker_name) + with self.config_lock: + account = self.config['workers'][worker_name]['account'] + self.config['workers'].pop(worker_name) self.accounts.remove(account) if pause: @@ -212,25 +222,23 @@ def remove_worker(self, worker_name=None): 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) + 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, bitshares_instance): # Initialize the base strategy to get control over the data strategy = StrategyBase(worker_name, config, bitshares_instance=bitshares_instance) - # Purge all worker data and cancel orders strategy.purge() @staticmethod def remove_offline_worker_data(worker_name): - # Remove all worker data, but don't cancel orders StrategyBase.purge_all_local_worker_data(worker_name) def do_next_tick(self, job): From 03cf8f74fda5b605d3b7fe477ba5238000285673 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 7 Nov 2018 15:29:28 +0200 Subject: [PATCH 0955/1846] Revert "Change create_worker() in main_controller.py" This reverts commit 21af52ca9814722d79e2c04298239bc5a46ed25b. --- dexbot/controllers/main_controller.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 8620b5c3d..ebe422e5a 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -6,7 +6,6 @@ from dexbot.helper import initialize_orders_log, initialize_data_folders from dexbot.worker import WorkerInfrastructure from dexbot.views.errors import PyQtHandler -from dexbot.storage import Storage from appdirs import user_data_dir from bitshares.instance import set_shared_bitshares_instance @@ -17,8 +16,6 @@ class MainController: def __init__(self, bitshares_instance, config): self.bitshares_instance = bitshares_instance set_shared_bitshares_instance(bitshares_instance) - - # Global configuration which includes all the workers self.config = config self.worker_manager = None @@ -80,10 +77,5 @@ def remove_worker(self, worker_name): @staticmethod def create_worker(worker_name): - # Todo: Rename this function to something better - # In case worker is deleted only from config file, there are still information with the name in the database - # This function removes all that data and cancels orders so that new worker can take the name in it's use + # Deletes old worker's data WorkerInfrastructure.remove_offline_worker_data(worker_name) - - # Wound't this be just shortcut to achieve the same thing?? - # Storage.clear_worker_data(worker_name) From 9cbeea26ad313d24ddeeb1bf078a075e10afab7e Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 7 Nov 2018 15:30:00 +0200 Subject: [PATCH 0956/1846] Revert "Fix typo in worker_controller.py" This reverts commit ec112796ef183dcb413d2266ee1d84eedf91a7c0. --- dexbot/controllers/worker_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index 33c9256a7..a6d1fae34 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -84,8 +84,8 @@ def get_strategy_mode(worker_data): return worker_data['mode'] @staticmethod - def get_allow_instant_fill(worker_data): - return worker_data['allow_instant_fill'] + def get_allow_instant_fill(worder_data): + return worder_data['allow_instant_fill'] @staticmethod def get_assets(worker_data): From f900c46248bd7c6fcdf36aa9d6f2ab2ec0e363a4 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 7 Nov 2018 15:30:13 +0200 Subject: [PATCH 0957/1846] Revert "Refactor config.py" This reverts commit c88450fd18afe125cf80280921609e6a13855bf3. --- dexbot/cli.py | 2 +- dexbot/config.py | 37 +++++++---------------- dexbot/controllers/settings_controller.py | 2 +- dexbot/views/worker_item.py | 2 +- 4 files changed, 14 insertions(+), 29 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index c05d10a64..e778f4644 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -143,7 +143,7 @@ def configure(ctx): config = Config(path=ctx.obj['configfile']) configure_dexbot(config, ctx) - config.save() + config.save_config() click.echo("New configuration saved") if config.get('systemd_status', 'disabled') == 'enabled': diff --git a/dexbot/config.py b/dexbot/config.py index 3e6aeb874..b5ac113d2 100644 --- a/dexbot/config.py +++ b/dexbot/config.py @@ -20,8 +20,6 @@ def __init__(self, config=None, path=None): :param str path: path to the config file """ super().__init__() - - # Check if using custom path for the config if path: self.config_dir = os.path.dirname(path) self.config_file = path @@ -30,21 +28,18 @@ def __init__(self, config=None, path=None): self.config_file = DEFAULT_CONFIG_FILE if config: - self.create(config, self.config_file) + self.create_config(config, self.config_file) + self._config = self.load_config(self.config_file) else: if not os.path.isfile(self.config_file): - # Config file was not found, creating default config+ - self.create(self.default_data, self.config_file) - - # Load config to cache from the configuration file - self._config = self.load(self.config_file) + self.create_config(self.default_data, self.config_file) + self._config = self.load_config(self.config_file) # In case there is not a list of nodes in the config file, # the node will be replaced by a list of pre-defined nodes. - # This prevents deleting worker config in a case where user has deleted nodes but left worker if isinstance(self._config['node'], str): self._config['node'] = self.node_list - self.save() + self.save_config() def __setitem__(self, key, value): self._config[key] = value @@ -63,13 +58,7 @@ def get(self, key, default=None): @property def default_data(self): - """ Default data used to create an empty configuration file - :return: dict: node list and workers dict - """ - return { - 'node': self.node_list, - 'workers': {} - } + return {'node': self.node_list, 'workers': {}} @property def workers_data(self): @@ -83,7 +72,7 @@ def dict(self): return self._config @staticmethod - def create(config, path=None): + def create_config(config, path=None): if not path: config_dir = DEFAULT_CONFIG_DIR config_file = DEFAULT_CONFIG_FILE @@ -98,19 +87,19 @@ def create(config, path=None): yaml.dump(config, f, default_flow_style=False) @staticmethod - def load(path=None): + def load_config(path=None): if not path: path = DEFAULT_CONFIG_FILE with open(path, 'r') as f: return Config.ordered_load(f, loader=yaml.SafeLoader) - def save(self): + def save_config(self): with open(self.config_file, 'w') as f: yaml.dump(self._config, f, default_flow_style=False) def refresh_config(self): - self._config = self.load(self.config_file) + self._config = self.load_config(self.config_file) @staticmethod def get_worker_config_file(worker_name, path=None): @@ -134,11 +123,7 @@ def get_worker_config(self, worker_name): config['workers'] = OrderedDict({worker_name: config['workers'][worker_name]}) return config - # Return only the config items matching the worker_name - # config = self.workers_data.get(worker_name) - # return config - - def remove_worker(self, worker_name): + def remove_worker_config(self, worker_name): self._config['workers'].pop(worker_name, None) with open(self.config_file, 'w') as f: diff --git a/dexbot/controllers/settings_controller.py b/dexbot/controllers/settings_controller.py index ff6269c17..374419afd 100644 --- a/dexbot/controllers/settings_controller.py +++ b/dexbot/controllers/settings_controller.py @@ -107,7 +107,7 @@ def save_nodes_to_config(self, nodes): nodes = self.remove_empty_items(nodes) self.config['node'] = nodes - self.config.save() + self.config.save_config() # Update status self.view.notification_label.setText('Settings successfully saved!') diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 5415cbcb7..217aa0218 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -143,7 +143,7 @@ def remove_widget_dialog(self): def remove_widget(self): self.main_ctrl.remove_worker(self.worker_name) self.view.remove_worker_widget(self.worker_name) - self.main_controller.config.remove_worker(self.worker_name) + self.main_ctrl.config.remove_worker_config(self.worker_name) self.deleteLater() def reload_widget(self, worker_name): From e26abc7b7ea360094c6b6e65835bfb7c657902c2 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 7 Nov 2018 15:30:43 +0200 Subject: [PATCH 0958/1846] Revert "Refactor gui.py variables" This reverts commit b016e8418facfd63c398b73abb3870272cd5252a. --- dexbot/gui.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/dexbot/gui.py b/dexbot/gui.py index e0fe5f081..d1543f70b 100644 --- a/dexbot/gui.py +++ b/dexbot/gui.py @@ -2,17 +2,16 @@ from dexbot.config import Config from dexbot.controllers.main_controller import MainController +from dexbot.views.worker_list import MainView from dexbot.controllers.wallet_controller import WalletController -from dexbot.views.create_wallet import CreateWalletView from dexbot.views.unlock_wallet import UnlockWalletView -from dexbot.views.worker_list import MainView +from dexbot.views.create_wallet import CreateWalletView from PyQt5.Qt import QApplication from bitshares import BitShares class App(QApplication): - def __init__(self, sys_argv): super(App, self).__init__(sys_argv) @@ -20,16 +19,16 @@ def __init__(self, sys_argv): bitshares_instance = BitShares(config['node'], num_retries=-1) # Wallet unlock - wallet_controller = WalletController(bitshares_instance) - if wallet_controller.wallet_created(): - unlock_view = UnlockWalletView(wallet_controller) + unlock_ctrl = WalletController(bitshares_instance) + if unlock_ctrl.wallet_created(): + unlock_view = UnlockWalletView(unlock_ctrl) else: - unlock_view = CreateWalletView(wallet_controller) + unlock_view = CreateWalletView(unlock_ctrl) if unlock_view.exec_(): - bitshares_instance = wallet_controller.bitshares - self.main_controller = MainController(bitshares_instance, config) - self.main_view = MainView(self.main_controller) + bitshares_instance = unlock_ctrl.bitshares + self.main_ctrl = MainController(bitshares_instance, config) + self.main_view = MainView(self.main_ctrl) self.main_view.show() else: sys.exit() From e1f927dc994cec33b88c880c504ea3340f72ea0c Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 7 Nov 2018 17:48:59 +0500 Subject: [PATCH 0959/1846] Disable allow_partial for closer orders allow_partial behaving is not very good for valley mode because it may decrease liquidity closer to center. There are actually no situations when we must allow orders significantly smaller than own-side orders or opposite-site orders. --- 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 31107d093..7c4e9384c 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -484,7 +484,7 @@ def allocate_asset(self, asset, asset_balance): self.log.debug('Limiting {} order by opposite order: {} {}'.format( order_type, opposite_asset_limit, symbol)) self.place_closer_order(asset, closest_own_order, own_asset_limit=own_asset_limit, - opposite_asset_limit=opposite_asset_limit, allow_partial=True) + opposite_asset_limit=opposite_asset_limit, allow_partial=False) elif not opposite_orders: # Do not try to do anything than placing higher buy whether there is no sell orders return From ed7f3e3a18d87c8a7cf56b0ab8552e5dcf96e4ff Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 8 Nov 2018 13:39:36 +0500 Subject: [PATCH 0960/1846] Fix comment --- 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 7c4e9384c..f51c2fc41 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -647,7 +647,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): """ new_order_amount = closer_bound / (1 + self.increment * 0.2) - # Limit sell order to available balance + # Limit order to available balance if asset_balance < new_order_amount - order_amount: new_order_amount = order_amount + asset_balance['amount'] self.log.info('Limiting new {} order to avail asset balance: {:.8f} {}' From d7a2054b476b6d7b191f43d34471ad6419ee46a7 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 8 Nov 2018 13:41:21 +0500 Subject: [PATCH 0961/1846] More code consistency for orders increase in different modes --- dexbot/strategies/staggered_orders.py | 30 ++++++++++++++------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index f51c2fc41..32338c21b 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -722,29 +722,30 @@ def increase_order_sizes(self, asset, asset_balance, orders): if (order_amount * (1 + self.increment / 10) < closer_order_bound and closer_order_bound - order_amount >= order_amount * self.increment / 2): - amount_base = closer_order_bound + new_order_amount = closer_order_bound # Limit order to available balance - if asset_balance < amount_base - order_amount: - amount_base = order_amount + asset_balance['amount'] + if asset_balance < new_order_amount - order_amount: + new_order_amount = order_amount + asset_balance['amount'] self.log.info('Limiting new order to avail asset balance: {:.8f} {}' - .format(amount_base, asset_balance['symbol'])) + .format(new_order_amount, asset_balance['symbol'])) price = 0 if asset == 'quote': price = (order['price'] ** -1) + quote_amount = new_order_amount elif asset == 'base': price = order['price'] + quote_amount = new_order_amount / price self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}, amount: {}, price: {:.8f}' .format(order_type, self.mode, order_amount, price)) self.cancel(order) if asset == 'quote': - self.market_sell(amount_base, price) + self.market_sell(quote_amount, price) elif asset == 'base': - amount_quote = amount_base / price - self.market_buy(amount_quote, price) + self.market_buy(quote_amount, price) # One increase at a time. This prevents running more than one increment round simultaneously. return @@ -790,30 +791,31 @@ def increase_order_sizes(self, asset, asset_balance, orders): if (order_amount * (1 + self.increment / 10) < closer_order_bound and closer_order_bound - order_amount >= order_amount * (math.sqrt(1 + self.increment) - 1) / 2): - amount_base = closer_order_bound + new_order_amount = closer_order_bound # Limit order to available balance - if asset_balance < amount_base - order_amount: - amount_base = order_amount + asset_balance['amount'] + if asset_balance < new_order_amount - order_amount: + new_order_amount = order_amount + asset_balance['amount'] self.log.info('Limiting new order to avail asset balance: {:.8f} {}' - .format(amount_base, asset_balance['symbol'])) + .format(new_order_amount, asset_balance['symbol'])) price = 0 if asset == 'quote': price = (order['price'] ** -1) + quote_amount = new_order_amount elif asset == 'base': price = order['price'] + quote_amount = new_order_amount / price self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}' ', amount: {:.8f}, price: {:.8f}' .format(order_type, self.mode, order_amount, price)) self.cancel(order) if asset == 'quote': - self.market_sell(amount_base, price) + self.market_sell(quote_amount, price) elif asset == 'base': - amount_quote = amount_base / price - self.market_buy(amount_quote, price) + self.market_buy(quote_amount, price) # One increase at a time. This prevents running more than one increment round simultaneously. return From a579a180c05622ca144a4502accbba1ae162dec7 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 8 Nov 2018 13:42:56 +0500 Subject: [PATCH 0962/1846] Update logging of orders increase --- dexbot/strategies/staggered_orders.py | 54 +++++++++++++-------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 32338c21b..bfbbe68c2 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -563,6 +563,8 @@ def increase_order_sizes(self, asset, asset_balance, orders): """ total_balance = 0 order_type = '' + symbol = '' + precision = 0 # First of all, make sure all orders are not partially filled for order in orders: @@ -570,6 +572,17 @@ def increase_order_sizes(self, asset, asset_balance, orders): self.replace_partially_filled_order(order) return + if asset == 'quote': + total_balance = self.quote_total_balance + order_type = 'sell' + symbol = self.market['quote']['symbol'] + precision = self.market['quote']['precision'] + elif asset == 'base': + total_balance = self.base_total_balance + order_type = 'buy' + symbol = self.market['base']['symbol'] + precision = self.market['base']['precision'] + # Mountain mode: if (self.mode == 'mountain' or (self.mode == 'buy_slope' and asset == 'quote') or @@ -589,13 +602,6 @@ def increase_order_sizes(self, asset, asset_balance, orders): Also when making an order it's size always will be limited by available free balance """ - if asset == 'quote': - total_balance = self.quote_total_balance - order_type = 'sell' - elif asset == 'base': - total_balance = self.base_total_balance - order_type = 'buy' - # Get orders and amounts to be compared. Note: orders are sorted from low price to high for order in orders: order_index = orders.index(order) @@ -650,8 +656,8 @@ def increase_order_sizes(self, asset, asset_balance, orders): # Limit order to available balance if asset_balance < new_order_amount - order_amount: new_order_amount = order_amount + asset_balance['amount'] - self.log.info('Limiting new {} order to avail asset balance: {:.8f} {}' - .format(order_type, new_order_amount, asset_balance['symbol'])) + self.log.debug('Limiting new {} order to avail balance: {:.8f} {}' + .format(order_type, new_order_amount, symbol)) quote_amount = 0 price = 0 @@ -662,6 +668,8 @@ def increase_order_sizes(self, asset, asset_balance, orders): price = order['price'] quote_amount = new_order_amount / price + self.log.info('Increasing {} order at price {:.8f} from {:.{prec}f} to {:.{prec}f} {}' + .format(order_type, price, order_amount, new_order_amount, symbol, prec=precision)) self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}, amount: {}, price: {:.8f}' .format(order_type, self.mode, order_amount, price)) self.cancel(order) @@ -685,13 +693,6 @@ def increase_order_sizes(self, asset, asset_balance, orders): Maximum size is (example for buy orders): 1. As many "base" as the order below (closer_order_bound) """ - if asset == 'quote': - total_balance = self.quote_total_balance - order_type = 'sell' - elif asset == 'base': - total_balance = self.base_total_balance - order_type = 'buy' - orders_count = len(orders) orders = list(reversed(orders)) @@ -727,8 +728,8 @@ def increase_order_sizes(self, asset, asset_balance, orders): # Limit order to available balance if asset_balance < new_order_amount - order_amount: new_order_amount = order_amount + asset_balance['amount'] - self.log.info('Limiting new order to avail asset balance: {:.8f} {}' - .format(new_order_amount, asset_balance['symbol'])) + self.log.debug('Limiting new order to avail asset balance: {:.8f} {}' + .format(new_order_amount, symbol)) price = 0 @@ -738,10 +739,11 @@ def increase_order_sizes(self, asset, asset_balance, orders): elif asset == 'base': price = order['price'] quote_amount = new_order_amount / price + self.log.info('Increasing {} order at price {:.8f} from {:.{prec}f} to {:.{prec}f} {}' + .format(order_type, price, order_amount, new_order_amount, symbol, prec=precision)) self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}, amount: {}, price: {:.8f}' .format(order_type, self.mode, order_amount, price)) self.cancel(order) - if asset == 'quote': self.market_sell(quote_amount, price) elif asset == 'base': @@ -750,13 +752,6 @@ def increase_order_sizes(self, asset, asset_balance, orders): return elif self.mode == 'neutral': - if asset == 'quote': - total_balance = self.quote_total_balance - order_type = 'sell' - elif asset == 'base': - total_balance = self.base_total_balance - order_type = 'buy' - orders_count = len(orders) orders = list(reversed(orders)) @@ -796,8 +791,8 @@ def increase_order_sizes(self, asset, asset_balance, orders): # Limit order to available balance if asset_balance < new_order_amount - order_amount: new_order_amount = order_amount + asset_balance['amount'] - self.log.info('Limiting new order to avail asset balance: {:.8f} {}' - .format(new_order_amount, asset_balance['symbol'])) + self.log.debug('Limiting new order to avail asset balance: {:.8f} {}' + .format(new_order_amount, symbol)) price = 0 @@ -807,11 +802,12 @@ def increase_order_sizes(self, asset, asset_balance, orders): elif asset == 'base': price = order['price'] quote_amount = new_order_amount / price + self.log.info('Increasing {} order at price {:.8f} from {:.{prec}f} to {:.{prec}f} {}' + .format(order_type, price, order_amount, new_order_amount, symbol, prec=precision)) self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}' ', amount: {:.8f}, price: {:.8f}' .format(order_type, self.mode, order_amount, price)) self.cancel(order) - if asset == 'quote': self.market_sell(quote_amount, price) elif asset == 'base': From 4eceb8152cf8e98c26bfd00e4bfb3c6031bd2746 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 9 Nov 2018 20:25:12 +0500 Subject: [PATCH 0963/1846] Fix actual spread calculation when starting one-sided --- dexbot/strategies/staggered_orders.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index bfbbe68c2..613540f60 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -432,9 +432,11 @@ def allocate_asset(self, asset, asset_balance): if opposite_orders: closest_opposite_order = opposite_orders[0] closest_opposite_price = closest_opposite_order['price'] ** -1 - else: + elif asset == 'base': # For one-sided start, calculate closest_opposite_price empirically closest_opposite_price = self.market_center_price * (1 + self.target_spread / 2) + elif asset == 'quote': + closest_opposite_price = (self.market_center_price / (1 + self.target_spread / 2)) ** -1 closest_own_price = closest_own_order['price'] self.actual_spread = (closest_opposite_price / closest_own_price) - 1 From 7ca4b68e7bbe61c7a3678ca63094172b149e6aef Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 9 Nov 2018 20:38:15 +0500 Subject: [PATCH 0964/1846] Implement invert option to filter_sell_orders() --- dexbot/strategies/base.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 89eb282e4..c5267726a 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -855,12 +855,13 @@ def filter_buy_orders(self, orders, sort=None): return buy_orders - def filter_sell_orders(self, orders, sort=None): + def filter_sell_orders(self, orders, sort=None, invert=True): """ Return sell orders from list of orders. Can be used to pick sell orders from a list that is not up to date with the blockchain data. :param list | orders: List of orders :param string | sort: DESC or ASC will sort the orders accordingly, default None + :param bool | invert: return inverted orders or not :return list | sell_orders: List of sell orders only """ sell_orders = [] @@ -870,7 +871,9 @@ def filter_sell_orders(self, orders, sort=None): # Check if the order is buy order, by comparing asset symbol of the order and the market if order['base']['symbol'] != self.market['base']['symbol']: # Invert order before appending to the list, this gives easier comparison in strategy logic - sell_orders.append(order.invert()) + if invert: + order = order.invert() + sell_orders.append(order) if sort: sell_orders = self.sort_orders_by_price(sell_orders, sort) From 517fdebb36425926f7139eafdcd0f6e9ad6be0d3 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 9 Nov 2018 20:58:10 +0500 Subject: [PATCH 0965/1846] Remove unused variable self.initial_market_center_price --- dexbot/strategies/staggered_orders.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 613540f60..e755878a6 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -114,7 +114,6 @@ def __init__(self, *args, **kwargs): # Assume we are in bootstrap mode by default. This prevents weird things when bootstrap was interrupted self.bootstrapping = True self.market_center_price = None - self.initial_market_center_price = None self.buy_orders = [] self.sell_orders = [] self.actual_spread = self.target_spread + 1 @@ -171,10 +170,6 @@ def maintain_strategy(self, *args, **kwargs): # On empty market we have to pass the user specified center price self.market_center_price = self.calculate_center_price(center_price=self.center_price, suppress_errors=True) - if self.market_center_price and not self.initial_market_center_price: - # Save initial market center price - self.initial_market_center_price = self.market_center_price - # Still not have market_center_price? Empty market, don't continue if not self.market_center_price: self.log.warning('Cannot calculate center price on empty market, please set is manually') From e005c858a76671c9bb7f80cf4056d79b31932857 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 9 Nov 2018 20:32:18 +0500 Subject: [PATCH 0966/1846] Migrate staggered orders to base.py Closes: #385 --- dexbot/strategies/staggered_orders.py | 59 ++++++++++++--------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index e755878a6..e100d0633 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -2,12 +2,11 @@ from datetime import datetime, timedelta from bitshares.dex import Dex -from dexbot.basestrategy import BaseStrategy, ConfigElement from dexbot.strategies.base import StrategyBase, DetailElement from dexbot.qt_queue.idle_queue import idle_add -class Strategy(BaseStrategy): +class Strategy(StrategyBase): """ Staggered Orders strategy """ @classmethod @@ -40,7 +39,7 @@ def configure(cls, return_base_config=True): ('sell_slope', 'Sell Slope') ] - return BaseStrategy.configure(return_base_config) + [ + return StrategyBase.configure(return_base_config) + [ ConfigElement( 'mode', 'choice', 'mountain', 'Strategy mode', 'How to allocate funds and profits. Doesn\'t effect existing orders, only future ones', modes), @@ -121,7 +120,6 @@ def __init__(self, *args, **kwargs): self.base_total_balance = 0 self.quote_balance = None self.base_balance = None - self.ticker = None self.quote_asset_threshold = 0 self.base_asset_threshold = 0 # Initial balance history elements should not be equal to avoid immediate bootstrap turn off @@ -164,11 +162,11 @@ def maintain_strategy(self, *args, **kwargs): self.refresh_orders() # Check if market center price is calculated - if not self.bootstrapping: - self.market_center_price = self.calculate_center_price(suppress_errors=True) - elif not self.market_center_price: - # On empty market we have to pass the user specified center price - self.market_center_price = self.calculate_center_price(center_price=self.center_price, suppress_errors=True) + self.market_center_price = self.get_market_center_price(suppress_errors=True) + + # Set center price to manual value if needed + if self.center_price: + self.market_center_price = self.center_price # Still not have market_center_price? Empty market, don't continue if not self.market_center_price: @@ -195,9 +193,6 @@ def maintain_strategy(self, *args, **kwargs): self.log_maintenance_time() return - # Get ticker data - self.ticker = self.market.ticker() - # Prepare to bundle operations into single transaction self.bitshares.bundle = True @@ -307,7 +302,7 @@ def refresh_balances(self, total_balances=True, use_cached_orders=False): :param bool | use_cached_orders: when calculating orders balance, use cached orders from self.cached_orders """ # Get current account balances - account_balances = self.total_balance(order_ids=[], return_asset=True) + account_balances = self.count_asset(order_ids=[], return_asset=True) self.base_balance = account_balances['base'] self.quote_balance = account_balances['quote'] @@ -329,9 +324,9 @@ def refresh_balances(self, total_balances=True, use_cached_orders=False): if use_cached_orders and self.cached_orders: orders = self.cached_orders else: - orders = self.orders + orders = self.get_own_orders order_ids = [order['id'] for order in orders] - orders_balance = self.orders_balance(order_ids) + orders_balance = self.get_allocated_assets(order_ids) # Total balance per asset (orders balance and available balance) self.quote_total_balance = orders_balance['quote'] + self.quote_balance['amount'] @@ -340,12 +335,12 @@ def refresh_balances(self, total_balances=True, use_cached_orders=False): def refresh_orders(self): """ Updates buy and sell orders """ - orders = self.orders + orders = self.get_own_orders self.cached_orders = orders # Sort orders so that order with index 0 is closest to the center price and -1 is furthers - self.buy_orders = self.get_buy_orders('DESC', orders) - self.sell_orders = self.get_sell_orders('DESC', orders) + self.buy_orders = self.filter_buy_orders(orders, sort='DESC') + self.sell_orders = self.filter_sell_orders(orders, sort='DESC', invert=False) def remove_outside_orders(self, sell_orders, buy_orders): """ Remove orders that exceed boundaries @@ -671,9 +666,9 @@ def increase_order_sizes(self, asset, asset_balance, orders): .format(order_type, self.mode, order_amount, price)) self.cancel(order) if asset == 'quote': - self.market_sell(quote_amount, price) + self.place_market_sell_order(quote_amount, price) elif asset == 'base': - self.market_buy(quote_amount, price) + self.place_market_buy_order(quote_amount, price) # Only one increase at a time. This prevents running more than one increment round simultaneously return elif (self.mode == 'valley' or @@ -742,9 +737,9 @@ def increase_order_sizes(self, asset, asset_balance, orders): .format(order_type, self.mode, order_amount, price)) self.cancel(order) if asset == 'quote': - self.market_sell(quote_amount, price) + self.place_market_sell_order(quote_amount, price) elif asset == 'base': - self.market_buy(quote_amount, price) + self.place_market_buy_order(quote_amount, price) # One increase at a time. This prevents running more than one increment round simultaneously. return @@ -806,9 +801,9 @@ def increase_order_sizes(self, asset, asset_balance, orders): .format(order_type, self.mode, order_amount, price)) self.cancel(order) if asset == 'quote': - self.market_sell(quote_amount, price) + self.place_market_sell_order(quote_amount, price) elif asset == 'base': - self.market_buy(quote_amount, price) + self.place_market_buy_order(quote_amount, price) # One increase at a time. This prevents running more than one increment round simultaneously. return @@ -853,10 +848,10 @@ def replace_partially_filled_order(self, order): self.log.info('Replacing partially filled {} order'.format(order_type)) self.cancel(order) if order_type == 'buy': - self.market_buy(order['quote']['amount'], order['price']) + self.place_market_buy_order(order['quote']['amount'], order['price']) elif order_type == 'sell': price = order['price'] ** -1 - self.market_sell(order['base']['amount'], price) + self.place_market_sell_order(order['base']['amount'], price) if self.returnOrderId: self.refresh_balances(total_balances=False) else: @@ -962,9 +957,9 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False quote_amount = balance if place_order and asset == 'base': - self.market_buy(quote_amount, price) + self.place_market_buy_order(quote_amount, price) elif place_order and asset == 'quote': - self.market_sell(quote_amount, price) + self.place_market_sell_order(quote_amount, price) return {"amount": quote_amount, "price": price} @@ -1037,9 +1032,9 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals quote_amount = balance if place_order and asset == 'base': - self.market_buy(quote_amount, price) + self.place_market_buy_order(quote_amount, price) elif place_order and asset == 'quote': - self.market_sell(quote_amount, price) + self.place_market_sell_order(quote_amount, price) return {"amount": quote_amount, "price": price} @@ -1118,7 +1113,7 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) if place_order: - self.market_sell(amount_quote, price) + self.place_market_sell_order(amount_quote, price) else: return {"amount": amount_quote, "price": price} @@ -1230,7 +1225,7 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) if place_order: - self.market_buy(amount_quote, price) + self.place_market_buy_order(amount_quote, price) else: return {"amount": amount_quote, "price": price} From dd12ef1a4a77b2030e29808d74b97e9c2b201b56 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 11 Nov 2018 13:49:21 +0500 Subject: [PATCH 0967/1846] Refactor opposite-sided partially filled orders handling Do not try to allocate funds if partially filled order on opposite side. This avoids unneded allocation rounds in valley mode. --- dexbot/strategies/staggered_orders.py | 46 +++++++++++---------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index e100d0633..c61a17cda 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -480,34 +480,26 @@ def allocate_asset(self, asset, asset_balance): elif not opposite_orders: # Do not try to do anything than placing higher buy whether there is no sell orders return + elif not self.check_partial_fill(closest_opposite_order, fill_threshold=0): + """ Partially filled order on the opposite side, wait until closest order will be fully + fillled. Previously we did reservation of funds for next order and allocated remainder, but + this approach is not well-suited for valley mode, causing liquidity decrease around center. + """ + self.log.debug('Partially filled order on opposite side, reserving funds until fully filled') + return + elif ((asset == 'base' and furthest_own_order_price / + (1 + self.increment) < self.lower_bound) or + (asset == 'quote' and furthest_own_order_price * + (1 + self.increment) > self.upper_bound)): + # Lower/upper bound has been reached and now will start allocating rest of the balance. + self.bootstrapping = False + self.log.debug('Increasing sizes of {} orders'.format(order_type)) + self.increase_order_sizes(asset, asset_balance, own_orders) else: - if not self.check_partial_fill(closest_opposite_order): - """ Detect partially filled order on the opposite side and - reserve appropriate amount to place closer order - """ - funds_to_reserve = 0 - closer_own_order = self.place_closer_order(asset, closest_own_order, place_order=False) - if asset == 'base': - funds_to_reserve = closer_own_order['amount'] * closer_own_order['price'] - elif asset == 'quote': - funds_to_reserve = closer_own_order['amount'] - self.log.debug('Partially filled order on opposite side, reserving funds for next {} order: ' - '{:.8f} {}'.format(order_type, funds_to_reserve, symbol)) - asset_balance -= funds_to_reserve - if asset_balance > own_threshold: - if ((asset == 'base' and furthest_own_order_price / - (1 + self.increment) < self.lower_bound) or - (asset == 'quote' and furthest_own_order_price * - (1 + self.increment) > self.upper_bound)): - # Lower/upper bound has been reached and now will start allocating rest of the balance. - self.bootstrapping = False - self.log.debug('Increasing sizes of {} orders'.format(order_type)) - self.increase_order_sizes(asset, asset_balance, own_orders) - else: - # Range bound is not reached, we need to add additional orders at the extremes - self.bootstrapping = False - self.log.debug('Placing further order than current furthest {} order'.format(order_type)) - self.place_further_order(asset, furthest_own_order, allow_partial=True) + # Range bound is not reached, we need to add additional orders at the extremes + self.bootstrapping = False + self.log.debug('Placing further order than current furthest {} order'.format(order_type)) + self.place_further_order(asset, furthest_own_order, allow_partial=True) else: self.replace_partially_filled_order(closest_own_order) else: From 2f0201f573fdc5041353f4227da021fe5a342299 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 11 Nov 2018 14:06:49 +0500 Subject: [PATCH 0968/1846] Refactor own-side partially filled orders handling If target spread is not reached and we need to place closer order, we need to make sure current closest order is 100% unfilled. When target spread is reached, we are replacing order only if it was filled no less than `self.fill_threshold`. This helps to avoid too often replacements. --- dexbot/strategies/staggered_orders.py | 175 ++++++++++++++------------ 1 file changed, 93 insertions(+), 82 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c61a17cda..046bc594c 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -96,7 +96,7 @@ def __init__(self, *args, **kwargs): self.upper_bound = self.worker['upper_bound'] self.lower_bound = self.worker['lower_bound'] # This fill threshold prevents too often orders replacements draining fee_asset - self.partial_fill_threshold = self.increment / 10 + self.partial_fill_threshold = 0.15 self.is_instant_fill_enabled = self.worker.get('instant_fill', True) self.is_center_price_dynamic = self.worker['center_price_dynamic'] @@ -416,92 +416,103 @@ def allocate_asset(self, asset, asset_balance): if asset == 'quote': furthest_own_order_price = furthest_own_order_price ** -1 - # Check if the order was partially filled - if self.check_partial_fill(closest_own_order): - # Calculate actual spread - if opposite_orders: - closest_opposite_order = opposite_orders[0] - closest_opposite_price = closest_opposite_order['price'] ** -1 - elif asset == 'base': - # For one-sided start, calculate closest_opposite_price empirically - closest_opposite_price = self.market_center_price * (1 + self.target_spread / 2) - elif asset == 'quote': - closest_opposite_price = (self.market_center_price / (1 + self.target_spread / 2)) ** -1 - - closest_own_price = closest_own_order['price'] - self.actual_spread = (closest_opposite_price / closest_own_price) - 1 - - if self.actual_spread >= self.target_spread + self.increment: - """ Note: because we're using operations batching, there is possible a situation when we will have - both free balances and `self.actual_spread >= self.target_spread + self.increment`. In such case - there will be TWO orders placed, one buy and one sell despite only one would be enough to reach - target spread. Sure, we can add a workaround for that by overriding `closest_opposite_price` for - second call of allocate_asset(). We are not doing this because we're not doing assumption on - which side order (buy or sell) should be placed first. So, when placing two closer orders from - both sides, spread will be no less than `target_spread - increment`, thus not making any loss. - """ - if (self.bootstrapping and - self.base_balance_history[2] == self.base_balance_history[0] and - self.quote_balance_history[2] == self.quote_balance_history[0]): - # Turn off bootstrap mode whether we're didn't allocated assets during previous 3 maintenance - self.log.debug('Turning bootstrapping off: actual_spread > target_spread, we have free ' - 'balances and cannot allocate them normally 3 times in a row') - self.bootstrapping = False - - # Place order closer to the center price - self.log.debug('Placing closer {} order; actual spread: {:.4%}, target + increment: {:.4%}' - .format(order_type, self.actual_spread, self.target_spread + self.increment)) - if self.bootstrapping: - self.place_closer_order(asset, closest_own_order) - else: - # Place order limited by size of the opposite-side order - if (self.mode == 'mountain' or - (self.mode == 'buy_slope' and asset == 'base') or - (self.mode == 'sell_slope' and asset == 'quote')): - opposite_asset_limit = None - own_asset_limit = closest_opposite_order['quote']['amount'] - self.log.debug('Limiting {} order by opposite order: {} {}' - .format(order_type, own_asset_limit, symbol)) - elif self.mode == 'neutral': - opposite_asset_limit = closest_opposite_order['base']['amount'] * \ - math.sqrt(1 + self.increment) - own_asset_limit = None - self.log.debug('Limiting {} order by opposite order: {} {}'.format( - order_type, opposite_asset_limit, symbol)) - elif (self.mode == 'valley' or - (self.mode == 'buy_slope' and asset == 'quote') or - (self.mode == 'sell_slope' and asset == 'base')): - opposite_asset_limit = closest_opposite_order['base']['amount'] - own_asset_limit = None - self.log.debug('Limiting {} order by opposite order: {} {}'.format( - order_type, opposite_asset_limit, symbol)) - self.place_closer_order(asset, closest_own_order, own_asset_limit=own_asset_limit, - opposite_asset_limit=opposite_asset_limit, allow_partial=False) - elif not opposite_orders: - # Do not try to do anything than placing higher buy whether there is no sell orders - return - elif not self.check_partial_fill(closest_opposite_order, fill_threshold=0): - """ Partially filled order on the opposite side, wait until closest order will be fully - fillled. Previously we did reservation of funds for next order and allocated remainder, but - this approach is not well-suited for valley mode, causing liquidity decrease around center. + # Calculate actual spread + if opposite_orders: + closest_opposite_order = opposite_orders[0] + closest_opposite_price = closest_opposite_order['price'] ** -1 + elif asset == 'base': + # For one-sided start, calculate closest_opposite_price empirically + closest_opposite_price = self.market_center_price * (1 + self.target_spread / 2) + elif asset == 'quote': + closest_opposite_price = (self.market_center_price / (1 + self.target_spread / 2)) ** -1 + + closest_own_price = closest_own_order['price'] + self.actual_spread = (closest_opposite_price / closest_own_price) - 1 + + if self.actual_spread >= self.target_spread + self.increment: + if not self.check_partial_fill(closest_own_order, fill_threshold=0): + # Replace closest order if it was partially filled for any % + """ Note on partial filled orders handling: if target spread is not reached and we need to place + closer order, we need to make sure current closest order is 100% unfilled. When target spread is + reached, we are replacing order only if it was filled no less than `self.fill_threshold`. This + helps to avoid too often replacements. """ - self.log.debug('Partially filled order on opposite side, reserving funds until fully filled') + self.replace_partially_filled_order(closest_own_order) return - elif ((asset == 'base' and furthest_own_order_price / - (1 + self.increment) < self.lower_bound) or - (asset == 'quote' and furthest_own_order_price * - (1 + self.increment) > self.upper_bound)): - # Lower/upper bound has been reached and now will start allocating rest of the balance. + + if (self.bootstrapping and + self.base_balance_history[2] == self.base_balance_history[0] and + self.quote_balance_history[2] == self.quote_balance_history[0]): + # Turn off bootstrap mode whether we're didn't allocated assets during previous 3 maintenance + self.log.debug('Turning bootstrapping off: actual_spread > target_spread, we have free ' + 'balances and cannot allocate them normally 3 times in a row') self.bootstrapping = False - self.log.debug('Increasing sizes of {} orders'.format(order_type)) - self.increase_order_sizes(asset, asset_balance, own_orders) + + """ Note: because we're using operations batching, there is possible a situation when we will have + both free balances and `self.actual_spread >= self.target_spread + self.increment`. In such case + there will be TWO orders placed, one buy and one sell despite only one would be enough to reach + target spread. Sure, we can add a workaround for that by overriding `closest_opposite_price` for + second call of allocate_asset(). We are not doing this because we're not doing assumption on + which side order (buy or sell) should be placed first. So, when placing two closer orders from + both sides, spread will be no less than `target_spread - increment`, thus not making any loss. + """ + + # Place order closer to the center price + self.log.debug('Placing closer {} order; actual spread: {:.4%}, target + increment: {:.4%}' + .format(order_type, self.actual_spread, self.target_spread + self.increment)) + if self.bootstrapping: + self.place_closer_order(asset, closest_own_order) else: - # Range bound is not reached, we need to add additional orders at the extremes - self.bootstrapping = False - self.log.debug('Placing further order than current furthest {} order'.format(order_type)) - self.place_further_order(asset, furthest_own_order, allow_partial=True) - else: + # Place order limited by size of the opposite-side order + if (self.mode == 'mountain' or + (self.mode == 'buy_slope' and asset == 'base') or + (self.mode == 'sell_slope' and asset == 'quote')): + opposite_asset_limit = None + own_asset_limit = closest_opposite_order['quote']['amount'] + self.log.debug('Limiting {} order by opposite order: {} {}' + .format(order_type, own_asset_limit, symbol)) + elif self.mode == 'neutral': + opposite_asset_limit = closest_opposite_order['base']['amount'] * \ + math.sqrt(1 + self.increment) + own_asset_limit = None + self.log.debug('Limiting {} order by opposite order: {} {}'.format( + order_type, opposite_asset_limit, symbol)) + elif (self.mode == 'valley' or + (self.mode == 'buy_slope' and asset == 'quote') or + (self.mode == 'sell_slope' and asset == 'base')): + opposite_asset_limit = closest_opposite_order['base']['amount'] + own_asset_limit = None + self.log.debug('Limiting {} order by opposite order: {} {}'.format( + order_type, opposite_asset_limit, symbol)) + self.place_closer_order(asset, closest_own_order, own_asset_limit=own_asset_limit, + opposite_asset_limit=opposite_asset_limit, allow_partial=False) + elif not self.check_partial_fill(closest_own_order): + # Replace closest own order if `fill % > default threshold` self.replace_partially_filled_order(closest_own_order) + return + elif not opposite_orders: + # Do not try to do anything than placing higher buy whether there is no sell orders + return + elif not self.check_partial_fill(closest_opposite_order, fill_threshold=0): + """ Partially filled order on the opposite side, wait until closest order will be fully + fillled. Previously we did reservation of funds for next order and allocated remainder, but + this approach is not well-suited for valley mode, causing liquidity decrease around center. + """ + self.log.debug('Partially filled order on opposite side, reserving funds until fully filled') + return + elif ((asset == 'base' and furthest_own_order_price / + (1 + self.increment) < self.lower_bound) or + (asset == 'quote' and furthest_own_order_price * + (1 + self.increment) > self.upper_bound)): + # Lower/upper bound has been reached and now will start allocating rest of the balance. + self.bootstrapping = False + self.log.debug('Increasing sizes of {} orders'.format(order_type)) + self.increase_order_sizes(asset, asset_balance, own_orders) + else: + # Range bound is not reached, we need to add additional orders at the extremes + self.bootstrapping = False + self.log.debug('Placing further order than current furthest {} order'.format(order_type)) + self.place_further_order(asset, furthest_own_order, allow_partial=True) else: # Place first buy order as close to the lower bound as possible self.bootstrapping = True From d55f1911d0ed99fcac10fcd0d9f643c6cd6d9aaa Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 11 Nov 2018 14:56:12 +0500 Subject: [PATCH 0969/1846] Fix symbol in log messages --- dexbot/strategies/staggered_orders.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 046bc594c..188f336c3 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -393,17 +393,20 @@ def allocate_asset(self, asset, asset_balance): own_asset_limit = None own_orders = [] own_threshold = 0 - symbol = '' + own_symbol = '' + opposite_symbol = '' if asset == 'base': order_type = 'buy' - symbol = self.base_balance['symbol'] + own_symbol = self.base_balance['symbol'] + opposite_symbol = self.quote_balance['symbol'] own_orders = self.buy_orders opposite_orders = self.sell_orders own_threshold = self.base_asset_threshold elif asset == 'quote': order_type = 'sell' - symbol = self.quote_balance['symbol'] + own_symbol = self.quote_balance['symbol'] + opposite_symbol = self.base_balance['symbol'] own_orders = self.sell_orders opposite_orders = self.buy_orders own_threshold = self.quote_asset_threshold @@ -470,20 +473,20 @@ def allocate_asset(self, asset, asset_balance): opposite_asset_limit = None own_asset_limit = closest_opposite_order['quote']['amount'] self.log.debug('Limiting {} order by opposite order: {} {}' - .format(order_type, own_asset_limit, symbol)) + .format(order_type, own_asset_limit, own_symbol)) elif self.mode == 'neutral': opposite_asset_limit = closest_opposite_order['base']['amount'] * \ math.sqrt(1 + self.increment) own_asset_limit = None self.log.debug('Limiting {} order by opposite order: {} {}'.format( - order_type, opposite_asset_limit, symbol)) + order_type, opposite_asset_limit, opposite_symbol)) elif (self.mode == 'valley' or (self.mode == 'buy_slope' and asset == 'quote') or (self.mode == 'sell_slope' and asset == 'base')): opposite_asset_limit = closest_opposite_order['base']['amount'] own_asset_limit = None self.log.debug('Limiting {} order by opposite order: {} {}'.format( - order_type, opposite_asset_limit, symbol)) + order_type, opposite_asset_limit, opposite_symbol)) self.place_closer_order(asset, closest_own_order, own_asset_limit=own_asset_limit, opposite_asset_limit=opposite_asset_limit, allow_partial=False) elif not self.check_partial_fill(closest_own_order): From 3ea08c66026e4bedca0388c396c95917d3b84fba Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 12 Nov 2018 09:10:52 +0200 Subject: [PATCH 0970/1846] Change dexbot version number to 0.7.20 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 874b56260..c15ab60bb 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.19' +VERSION = '0.7.20' AUTHOR = 'Codaone Oy' __version__ = VERSION From 025bae7c00fc0e38967f3bcb91221beb91e06bca Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 12 Nov 2018 09:15:25 +0200 Subject: [PATCH 0971/1846] Change dexbot version number to 0.7.21 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index c15ab60bb..2b412b17f 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.20' +VERSION = '0.7.21' AUTHOR = 'Codaone Oy' __version__ = VERSION From 1afd90182e0b0c07501ec499b67dc068c2b0d4ff Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 12 Nov 2018 09:23:37 +0200 Subject: [PATCH 0972/1846] Change dexbot version number to 0.7.22 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 2b412b17f..750ebb3ce 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.21' +VERSION = '0.7.22' AUTHOR = 'Codaone Oy' __version__ = VERSION From 5ec1bd38582d247063188945db28f078f5e7ca46 Mon Sep 17 00:00:00 2001 From: joelvai Date: Mon, 12 Nov 2018 19:26:05 +0200 Subject: [PATCH 0973/1846] Fix missing ConfigElement import --- 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 188f336c3..4b17e5c6f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta from bitshares.dex import Dex -from dexbot.strategies.base import StrategyBase, DetailElement +from dexbot.strategies.base import StrategyBase, ConfigElement, DetailElement from dexbot.qt_queue.idle_queue import idle_add From 4bd91dac8d9e54373d350390ee8bcabb549d5468 Mon Sep 17 00:00:00 2001 From: joelvai Date: Mon, 12 Nov 2018 19:28:02 +0200 Subject: [PATCH 0974/1846] Fix onTick() error in place_closer_order() --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 4b17e5c6f..870dcf6e3 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -899,12 +899,12 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False # Check for instant fill if asset == 'base': price = order['price'] * (1 + self.increment) - if not self.is_instant_fill_enabled and price > float(self.ticker['lowestAsk']): + if not self.is_instant_fill_enabled and price > float(self.ticker().get('lowestAsk')): self.log.info('Refusing to place an order which crosses lowest ask') return None elif asset == 'quote': price = (order['price'] ** -1) / (1 + self.increment) - if not self.is_instant_fill_enabled and price < float(self.ticker['highestBid']): + if not self.is_instant_fill_enabled and price < float(self.ticker().get('highestBid')): self.log.info('Refusing to place an order which crosses highest bid') return None From 12f509c702e101df1b32b2b116e5c8b7e84f4b9f Mon Sep 17 00:00:00 2001 From: joelvai Date: Mon, 12 Nov 2018 19:30:20 +0200 Subject: [PATCH 0975/1846] Fix minor PEP8 code warnings --- dexbot/strategies/staggered_orders.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 870dcf6e3..0bc387cd8 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -504,7 +504,7 @@ def allocate_asset(self, asset, asset_balance): self.log.debug('Partially filled order on opposite side, reserving funds until fully filled') return elif ((asset == 'base' and furthest_own_order_price / - (1 + self.increment) < self.lower_bound) or + (1 + self.increment) < self.lower_bound) or (asset == 'quote' and furthest_own_order_price * (1 + self.increment) > self.upper_bound)): # Lower/upper bound has been reached and now will start allocating rest of the balance. @@ -655,7 +655,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): if asset_balance < new_order_amount - order_amount: new_order_amount = order_amount + asset_balance['amount'] self.log.debug('Limiting new {} order to avail balance: {:.8f} {}' - .format(order_type, new_order_amount, symbol)) + .format(order_type, new_order_amount, symbol)) quote_amount = 0 price = 0 @@ -727,9 +727,10 @@ def increase_order_sizes(self, asset, asset_balance, orders): if asset_balance < new_order_amount - order_amount: new_order_amount = order_amount + asset_balance['amount'] self.log.debug('Limiting new order to avail asset balance: {:.8f} {}' - .format(new_order_amount, symbol)) + .format(new_order_amount, symbol)) price = 0 + quote_amount = 0 if asset == 'quote': price = (order['price'] ** -1) @@ -790,7 +791,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): if asset_balance < new_order_amount - order_amount: new_order_amount = order_amount + asset_balance['amount'] self.log.debug('Limiting new order to avail asset balance: {:.8f} {}' - .format(new_order_amount, symbol)) + .format(new_order_amount, symbol)) price = 0 From 073b0c30a6e7722a1d975bd057fc09a1257957a8 Mon Sep 17 00:00:00 2001 From: joelvai Date: Mon, 12 Nov 2018 19:31:54 +0200 Subject: [PATCH 0976/1846] Change dexbot version number to 0.7.23 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 750ebb3ce..0250373d0 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.22' +VERSION = '0.7.23' AUTHOR = 'Codaone Oy' __version__ = VERSION From 4cfe28a959d6b69c9356fa9d136de320b214bc98 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Mon, 12 Nov 2018 13:54:43 -0800 Subject: [PATCH 0977/1846] external orders, only one entry for config --- dexbot/strategies/base.py | 90 ++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 44 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 5a12a6da7..446b35f5d 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -138,11 +138,13 @@ def configure(cls, return_base_config=True): # External exchanges used to calculate center price exchanges = [ - ('gecko', 'coingecko'), - ('ccxt-kraken', 'kraken'), - ('ccxt-bitfinex', 'bitfinex'), - ('ccxt-gdax', 'gdax'), - ('ccxt-binance', 'binance') + ('none', 'None - Use Manual or Bitshares DEX Price (default)'), + ('gecko', 'Coingecko'), + ('waves', 'Waves DEX'), + ('kraken', 'Kraken'), + ('bitfinex', 'Bitfinex'), + ('gdax', 'Gdax'), + ('binance', 'Binance') ] # Common configs @@ -153,10 +155,7 @@ def configure(cls, return_base_config=True): ConfigElement('market', 'string', 'USD:BTS', 'Market', 'BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"', r'[A-Z\.]+[:\/][A-Z\.]+'), - ConfigElement('external_center_price', 'bool', True, - 'Use External center price (if not available, defaults to manual center price)', - 'External center price expressed in base asset: BASE/QUOTE', None), - ConfigElement('external_center_price_source', 'choice', 'gecko', 'External Source', + ConfigElement('external_center_price_source', 'choice', exchanges[0], 'External Source', 'External Price Source, select one', exchanges), ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset', 'Asset to be used to pay transaction fees', @@ -251,9 +250,8 @@ def __init__(self, self.fetch_depth = 8 # Set external price source - if self.worker.get('external_center_price', False): - self.external_price_source = self.worker.get('external_center_price_source') - + self.external_price_source = self.worker.get('external_center_price_source') + # Set fee asset fee_asset_symbol = self.worker.get('fee_asset') @@ -611,10 +609,14 @@ def get_lowest_own_sell_order(self, orders=None): except IndexError: return None - def get_external_market_center_price(self): + def get_external_market_center_price(self, external_source): # Todo: Work in progress - ticker_price = 6363.00 # Dummy price - return ticker_price + print("inside get_emcp, exchange: ", external_source, sep=':') # debug + market = self.market.get_string('/') + print("market:", market, sep=' ') # debug + # center_price = process_pair(exchange, market) #todo: use process pair object to get center price + center_price = 0.08888 # Dummy price for now + return center_price def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors=False): """ Returns the center price of market including own orders. @@ -624,26 +626,31 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors :param bool | suppress_errors: :return: Market center price as float """ - - buy_price = self.get_market_buy_price(quote_amount=quote_amount, - base_amount=base_amount, exclude_own_orders=False) - sell_price = self.get_market_sell_price(quote_amount=quote_amount, - base_amount=base_amount, exclude_own_orders=False) - - if buy_price is None or buy_price == 0.0: - if not suppress_errors: - self.log.critical("Cannot estimate center price, there is no highest bid.") - self.disabled = True - return None - - if sell_price is None or sell_price == 0.0: - if not suppress_errors: - self.log.critical("Cannot estimate center price, there is no lowest ask.") - self.disabled = True - return None - - # Calculate and return market center price - return buy_price * math.sqrt(sell_price / buy_price) + center_price = None + external_source = self.external_price_source + + if external_source != 'none': + center_price = self.get_external_market_center_price(external_source) + else: + buy_price = self.get_market_buy_price(quote_amount=quote_amount, + base_amount=base_amount, exclude_own_orders=False) + sell_price = self.get_market_sell_price(quote_amount=quote_amount, + base_amount=base_amount, exclude_own_orders=False) + if buy_price is None or buy_price == 0.0: + if not suppress_errors: + self.log.critical("Cannot estimate center price, there is no highest bid.") + self.disabled = True + return None + + if sell_price is None or sell_price == 0.0: + if not suppress_errors: + self.log.critical("Cannot estimate center price, there is no lowest ask.") + self.disabled = True + return None + # Calculate and return market center price + center_price = buy_price * math.sqrt(sell_price / buy_price) + print("center_price : " , center_price, sep=' ') # debug + return center_price def get_market_buy_price(self, quote_amount=0, base_amount=0, exclude_own_orders=True): """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with @@ -880,13 +887,12 @@ def filter_buy_orders(self, orders, sort=None): return buy_orders - def filter_sell_orders(self, orders, sort=None, invert=True): + def filter_sell_orders(self, orders, sort=None): """ Return sell orders from list of orders. Can be used to pick sell orders from a list that is not up to date with the blockchain data. :param list | orders: List of orders :param string | sort: DESC or ASC will sort the orders accordingly, default None - :param bool | invert: return inverted orders or not :return list | sell_orders: List of sell orders only """ sell_orders = [] @@ -896,9 +902,7 @@ def filter_sell_orders(self, orders, sort=None, invert=True): # Check if the order is buy order, by comparing asset symbol of the order and the market if order['base']['symbol'] != self.market['base']['symbol']: # Invert order before appending to the list, this gives easier comparison in strategy logic - if invert: - order = order.invert() - sell_orders.append(order) + sell_orders.append(order.invert()) if sort: sell_orders = self.sort_orders_by_price(sell_orders, sort) @@ -1060,8 +1064,7 @@ def place_market_buy_order(self, amount, price, return_none=False, *args, **kwar self.disabled = True return None - self.log.info('Placing a buy order for {:.{prec}f} {} @ {:.8f}' - .format(base_amount, symbol, price, prec=precision)) + self.log.info('Placing a buy order for {} {} @ {:.8f}'.format(base_amount, symbol, price)) # Place the order buy_transaction = self.retry_action( @@ -1114,8 +1117,7 @@ def place_market_sell_order(self, amount, price, return_none=False, *args, **kwa self.disabled = True return None - self.log.info('Placing a sell order for {:.{prec}f} {} @ {:.8f}' - .format(quote_amount, symbol, price, prec=precision)) + self.log.info('Placing a sell order for {} {} @ {:.8f}'.format(quote_amount, symbol, price)) # Place the order sell_transaction = self.retry_action( From b7346351ea08b249116fe62d8a7b667b9fcdf496 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Mon, 12 Nov 2018 13:57:15 -0800 Subject: [PATCH 0978/1846] text edit --- dexbot/strategies/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 446b35f5d..4dc0afbd8 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -138,7 +138,7 @@ def configure(cls, return_base_config=True): # External exchanges used to calculate center price exchanges = [ - ('none', 'None - Use Manual or Bitshares DEX Price (default)'), + ('none', 'None. Use Manual or Bitshares DEX Price (default)'), ('gecko', 'Coingecko'), ('waves', 'Waves DEX'), ('kraken', 'Kraken'), From 574c4f407e373053c492acf4f8e1313e7b37e3c6 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Mon, 12 Nov 2018 14:55:36 -0800 Subject: [PATCH 0979/1846] merge in update from codaone master in base.py --- dexbot/strategies/base.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 4dc0afbd8..47459edb3 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -887,12 +887,13 @@ def filter_buy_orders(self, orders, sort=None): return buy_orders - def filter_sell_orders(self, orders, sort=None): + def filter_sell_orders(self, orders, sort=None, invert=True): """ Return sell orders from list of orders. Can be used to pick sell orders from a list that is not up to date with the blockchain data. :param list | orders: List of orders :param string | sort: DESC or ASC will sort the orders accordingly, default None + :param bool | invert: return inverted orders or not :return list | sell_orders: List of sell orders only """ sell_orders = [] @@ -902,6 +903,8 @@ def filter_sell_orders(self, orders, sort=None): # Check if the order is buy order, by comparing asset symbol of the order and the market if order['base']['symbol'] != self.market['base']['symbol']: # Invert order before appending to the list, this gives easier comparison in strategy logic + if invert: + order = order.invert() sell_orders.append(order.invert()) if sort: @@ -1064,7 +1067,8 @@ def place_market_buy_order(self, amount, price, return_none=False, *args, **kwar self.disabled = True return None - self.log.info('Placing a buy order for {} {} @ {:.8f}'.format(base_amount, symbol, price)) + self.log.info('Placing a buy order for {:.{prec}f} {} @ {:.8f}' + .format(base_amount, symbol, price, prec=precision)) # Place the order buy_transaction = self.retry_action( From 60d0660040c1c5d8acfeb2e973993f14273305b4 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Mon, 12 Nov 2018 14:58:55 -0800 Subject: [PATCH 0980/1846] update base.py with codaone master --- dexbot/strategies/base.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 47459edb3..2da23f3c6 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -887,7 +887,7 @@ def filter_buy_orders(self, orders, sort=None): return buy_orders - def filter_sell_orders(self, orders, sort=None, invert=True): + def filter_sell_orders(self, orders, sort=None, invert=True): """ Return sell orders from list of orders. Can be used to pick sell orders from a list that is not up to date with the blockchain data. @@ -905,7 +905,7 @@ def filter_sell_orders(self, orders, sort=None, invert=True): # Invert order before appending to the list, this gives easier comparison in strategy logic if invert: order = order.invert() - sell_orders.append(order.invert()) + sell_orders.append(order) if sort: sell_orders = self.sort_orders_by_price(sell_orders, sort) @@ -1121,7 +1121,8 @@ def place_market_sell_order(self, amount, price, return_none=False, *args, **kwa self.disabled = True return None - self.log.info('Placing a sell order for {} {} @ {:.8f}'.format(quote_amount, symbol, price)) + self.log.info('Placing a sell order for {:.{prec}f} {} @ {:.8f}' + .format(quote_amount, symbol, price, prec=precision)) # Place the order sell_transaction = self.retry_action( From 2652b28cb5b32f8bbd3fbb1ff7aec1dced3dcdd4 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Mon, 12 Nov 2018 18:03:50 -0800 Subject: [PATCH 0981/1846] make external price override manual center_price in relative orders only --- dexbot/strategies/base.py | 43 ++++++++++++---------------- dexbot/strategies/relative_orders.py | 10 +++++-- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 2da23f3c6..701f51577 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -609,13 +609,13 @@ def get_lowest_own_sell_order(self, orders=None): except IndexError: return None - def get_external_market_center_price(self, external_source): + def get_external_market_center_price(self): # Todo: Work in progress - print("inside get_emcp, exchange: ", external_source, sep=':') # debug + print("inside get_emcp, exchange: ", self.external_price_source, "fetch depth", self.fetch_depth, sep=':') # debug market = self.market.get_string('/') print("market:", market, sep=' ') # debug # center_price = process_pair(exchange, market) #todo: use process pair object to get center price - center_price = 0.08888 # Dummy price for now + center_price = 0.0778888 # Dummy price for now return center_price def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors=False): @@ -627,28 +627,23 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors :return: Market center price as float """ center_price = None - external_source = self.external_price_source - - if external_source != 'none': - center_price = self.get_external_market_center_price(external_source) - else: - buy_price = self.get_market_buy_price(quote_amount=quote_amount, - base_amount=base_amount, exclude_own_orders=False) - sell_price = self.get_market_sell_price(quote_amount=quote_amount, - base_amount=base_amount, exclude_own_orders=False) - if buy_price is None or buy_price == 0.0: - if not suppress_errors: - self.log.critical("Cannot estimate center price, there is no highest bid.") - self.disabled = True - return None - - if sell_price is None or sell_price == 0.0: - if not suppress_errors: - self.log.critical("Cannot estimate center price, there is no lowest ask.") - self.disabled = True - return None + buy_price = self.get_market_buy_price(quote_amount=quote_amount, + base_amount=base_amount, exclude_own_orders=False) + sell_price = self.get_market_sell_price(quote_amount=quote_amount, + base_amount=base_amount, exclude_own_orders=False) + if buy_price is None or buy_price == 0.0: + if not suppress_errors: + self.log.critical("Cannot estimate center price, there is no highest bid.") + self.disabled = True + return None + + if sell_price is None or sell_price == 0.0: + if not suppress_errors: + self.log.critical("Cannot estimate center price, there is no lowest ask.") + self.disabled = True + return None # Calculate and return market center price - center_price = buy_price * math.sqrt(sell_price / buy_price) + center_price = buy_price * math.sqrt(sell_price / buy_price) print("center_price : " , center_price, sep=' ') # debug return center_price diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 3463a5815..6aac1a392 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -80,15 +80,19 @@ def __init__(self, *args, **kwargs): if not self.market_center_price: self.empty_market = True - + # Worker parameters self.is_center_price_dynamic = self.worker['center_price_dynamic'] if self.is_center_price_dynamic: self.center_price = None self.center_price_depth = self.worker.get('center_price_depth', 0) else: - self.center_price = self.worker["center_price"] - + external_source = self.external_price_source + if external_source != 'none': + self.center_price = self.get_external_market_center_price() + else: + self.center_price = self.worker["center_price"] + self.is_relative_order_size = self.worker.get('relative_order_size', False) self.is_asset_offset = self.worker.get('center_price_offset', False) self.manual_offset = self.worker.get('manual_offset', 0) / 100 From 0aa62733194752cfdff9f08adb6d7df9cc0a7b30 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Mon, 12 Nov 2018 18:33:20 -0800 Subject: [PATCH 0982/1846] fix logic in relative orders for external price --- dexbot/strategies/base.py | 13 +++++++------ dexbot/strategies/relative_orders.py | 6 +++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 701f51577..7d646d1d8 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -140,7 +140,7 @@ def configure(cls, return_base_config=True): exchanges = [ ('none', 'None. Use Manual or Bitshares DEX Price (default)'), ('gecko', 'Coingecko'), - ('waves', 'Waves DEX'), + ('waves', 'Waves DEX (todo)'), ('kraken', 'Kraken'), ('bitfinex', 'Bitfinex'), ('gdax', 'Gdax'), @@ -610,12 +610,13 @@ def get_lowest_own_sell_order(self, orders=None): return None def get_external_market_center_price(self): - # Todo: Work in progress - print("inside get_emcp, exchange: ", self.external_price_source, "fetch depth", self.fetch_depth, sep=':') # debug + print("inside get_emcp, exchange: ", self.external_price_source, + "fetch depth", self.fetch_depth, sep=':') # debug market = self.market.get_string('/') print("market:", market, sep=' ') # debug - # center_price = process_pair(exchange, market) #todo: use process pair object to get center price - center_price = 0.0778888 # Dummy price for now + # center_price = price_feed(exchange, market) + center_price = 0.10778888 # Dummy price for now + print("external dummy price", center_price, sep=' ') return center_price def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors=False): @@ -644,7 +645,7 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors return None # Calculate and return market center price center_price = buy_price * math.sqrt(sell_price / buy_price) - print("center_price : " , center_price, sep=' ') # debug + print("inside get_market_center_price : " , center_price, sep=' ') # debug return center_price def get_market_buy_price(self, quote_amount=0, base_amount=0, exclude_own_orders=True): diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 6aac1a392..5fd80170b 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -75,7 +75,7 @@ def __init__(self, *args, **kwargs): self.error_onAccount = self.error # Market status - self.market_center_price = self.get_market_center_price(suppress_errors=True) + self.market_center_price = self.get_market_center_price(suppress_errors=True) self.empty_market = False if not self.market_center_price: @@ -90,8 +90,12 @@ def __init__(self, *args, **kwargs): external_source = self.external_price_source if external_source != 'none': self.center_price = self.get_external_market_center_price() + if self.center_price is None: + self.center_price = self.worker["center_price"] # set as manual + print("inside relative orders, get external center price", self.center_price, sep=' ') # debug else: self.center_price = self.worker["center_price"] + print("inside relative orders, no external center price", self.center_price, sep=' ') # debug self.is_relative_order_size = self.worker.get('relative_order_size', False) self.is_asset_offset = self.worker.get('center_price_offset', False) From 9c09c2e89d82638021d4ad75bd680e942af3ed39 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 13 Nov 2018 00:40:54 -0800 Subject: [PATCH 0983/1846] integrate test for ccxt feed to base.py --- dexbot/strategies/base.py | 7 +- dexbot/strategies/external_feeds/__init__.py | 0 dexbot/strategies/external_feeds/ccxt_feed.py | 133 ++++-------------- .../strategies/external_feeds/price_feed.py | 34 +++++ 4 files changed, 65 insertions(+), 109 deletions(-) create mode 100644 dexbot/strategies/external_feeds/__init__.py create mode 100644 dexbot/strategies/external_feeds/price_feed.py diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 7d646d1d8..afa9892d7 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -10,6 +10,7 @@ from dexbot.storage import Storage from dexbot.statemachine import StateMachine from dexbot.helper import truncate +from dexbot.strategies.external_feeds.price_feed import PriceFeed from events import Events import bitshares.exceptions @@ -614,7 +615,11 @@ def get_external_market_center_price(self): "fetch depth", self.fetch_depth, sep=':') # debug market = self.market.get_string('/') print("market:", market, sep=' ') # debug - # center_price = price_feed(exchange, market) + print("exchange:", self.external_price_source, sep=' ') + pf = PriceFeed(self.external_price_source, market) + center_price = pf.get_center_price() + print(" PriceFeed ", center_price, sep=':') + center_price = 0.10778888 # Dummy price for now print("external dummy price", center_price, sep=' ') return center_price diff --git a/dexbot/strategies/external_feeds/__init__.py b/dexbot/strategies/external_feeds/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dexbot/strategies/external_feeds/ccxt_feed.py b/dexbot/strategies/external_feeds/ccxt_feed.py index cd8b657a0..56c4b24d8 100644 --- a/dexbot/strategies/external_feeds/ccxt_feed.py +++ b/dexbot/strategies/external_feeds/ccxt_feed.py @@ -2,131 +2,48 @@ import asyncio import functools import ccxt.async_support as accxt -import ccxt # noqa: E402 from pprint import pprint -from dexbot.strategies.external_feeds.styles import red, green, yellow, bold, underline -from dexbot.strategies.external_feeds.process_pair import print_args - - def get_exchanges(): return ccxt.exchanges -def get_ticker(exchange, symbol): +async def print_ticker(symbol, id): + # Verbose mode will show the order of execution to verify concurrency + exchange = getattr(accxt, id)({'verbose': True}) + await exchange.fetch_ticker(symbol) + await exchange.close() + + +async def fetch_ticker(exchange, symbol): + ticker = None try: - ticker = exchange.fetch_ticker(symbol.upper()) - except ccxt.DDoSProtection as e: - print(type(e).__name__, e.args, 'DDoS Protection (ignoring)') + ticker = await exchange.fetch_ticker(symbol.upper()) + except Exception as e: + print(type(e).__name__, e.args, 'Exchange Error (ignoring)') except ccxt.RequestTimeout as e: print(type(e).__name__, e.args, 'Request Timeout (ignoring)') except ccxt.ExchangeNotAvailable as e: print(type(e).__name__, e.args, 'Exchange Not Available due to downtime or maintenance (ignoring)') - except ccxt.AuthenticationError as e: - print(type(e).__name__, e.args, 'Authentication Error (missing API keys, ignoring)') - return ticker - - -async def fetch_ticker(exchange, symbol): - ticker = await exchange.fetchTicker(symbol) await exchange.close() return ticker -async def print_ticker(symbol, id): - # Verbose mode will show the order of execution to verify concurrency - exchange = getattr(accxt, id)({'verbose': True}) - print(await exchange.fetch_ticker(symbol)) - await exchange.close() - - -# Unit tests -# Todo: Move tests to own files -@click.group() -def main(): - pass - - -@main.command() -def test_async2(): +def get_ccxt_price(symbol, exchange_name): """ Get all tickers from multiple exchanges using async """ - symbol = 'ETH/BTC' - print_ethbtc_ticker = functools.partial(print_ticker, symbol) - [asyncio.ensure_future(print_ethbtc_ticker(id)) for id in [ - 'bitfinex', - 'binance', - 'kraken', - 'gdax', - 'bittrex', - ]] - pending = asyncio.Task.all_tasks() - loop = asyncio.get_event_loop() - loop.run_until_complete(asyncio.gather(*pending)) - - -@main.command() -def test_async(): - """ Get ticker for bitfinex using async """ - bitfinex = accxt.bitfinex({'enableRateLimit': True, }) + center_price = None + exchange = getattr(accxt, exchange_name)({'verbose':False}) ticker = asyncio.get_event_loop().run_until_complete( - fetch_ticker(bitfinex, 'BTC/USDT')) - print(ticker) - - -@main.command() -@click.argument('exchange') -@click.argument('symbol') -def test_feed(exchange, symbol): - """ Usage: exchange [symbol] - Symbol is required, for example: - python ccxt_feed.py test_feed gdax BTC/USD - """ - usage = "Usage: python ccxt_feed.py id [symbol]" - try: - id = exchange # Get exchange id from command line arguments - exchange_found = id in ccxt.exchanges - # Check if the exchange is supported by ccxt - if exchange_found: - print_args('Instantiating', green(id)) - # instantiate the exchange by id - exchange = getattr(ccxt, id)() - # load all markets from the exchange - markets = exchange.load_markets() - sym = symbol - if sym: - ticker = get_ticker(exchange, sym) - print_args( - green(exchange.id), - yellow(sym), - 'ticker', - ticker['datetime'], - 'high: ' + str(ticker['high']), - 'low: ' + str(ticker['low']), - 'bid: ' + str(ticker['bid']), - 'ask: ' + str(ticker['ask']), - 'volume: ' + str(ticker['quoteVolume'])) - else: - print_args('Symbol not found') - print_exchange_symbols(exchange) - print(usage) - else: - print_args('Exchange ' + red(id) + ' not found') - print(usage) - except Exception as e: - print(type(e).__name__, e.args, str(e)) - print(usage) - - -@main.command() -def test_exch_list(): - """ - gets a list of supported exchanges - """ - supported_exchanges = get_exchanges() - exchange_list = ', '.join(str(name) for name in supported_exchanges) - print(bold(underline('Supported exchanges: '))) - pprint(exchange_list, width=80) + fetch_ticker(exchange, symbol)) + if ticker: + center_price = (ticker['bid'] + ticker['ask'])/2 + return center_price if __name__ == '__main__': - main() + + exchanges =['bitfinex', 'kraken', 'binance', 'gdax'] + symbol = 'BTC/USDT' + center_price = [get_ccxt_price(symbol, e) for e in exchanges] + print(' exchange: ', exchanges, ' symbol: ', symbol, sep=':') + print(' center_price: ', center_price) diff --git a/dexbot/strategies/external_feeds/price_feed.py b/dexbot/strategies/external_feeds/price_feed.py new file mode 100644 index 000000000..9f10b10ea --- /dev/null +++ b/dexbot/strategies/external_feeds/price_feed.py @@ -0,0 +1,34 @@ +from dexbot.strategies.external_feeds.ccxt_feed import get_ccxt_price +#from ccxt_feed import get_ccxt_price + +#from gecko_feed import get_gecko_price +#from waves_feed import get_waves_price + +class PriceFeed: + + def __init__(self, exchange, symbol): + self.exchange = exchange + self.symbol = symbol + self.alt_exchanges = ['gecko', 'waves'] # assume all other exchanges are ccxt + + + def get_center_price(self): + price = None + if self.exchange not in self.alt_exchanges: + print("use ccxt exchange ", self.exchange, ' symbol ', self.symbol, sep=":") + price = get_ccxt_price(self.symbol, self.exchange) + elif self.exchange == 'gecko': + print("gecko - WIP todo") + elif self.exchange == 'waves': + print("waves - WIP todo") + return price + + +if __name__ == '__main__': + exchanges = ['bitfinex', 'kraken', 'gecko', 'waves'] + symbol = 'BTC/USD' + + for exchange in exchanges: + pf = PriceFeed(exchange, symbol) + center_price = pf.get_center_price() + print("center price: ", center_price) From fbc6fb13e450ef45d0a181429cbd462a1bc57726 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 13 Nov 2018 00:48:56 -0800 Subject: [PATCH 0984/1846] initial waves feed --- dexbot/strategies/external_feeds/waves_feed.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 dexbot/strategies/external_feeds/waves_feed.py diff --git a/dexbot/strategies/external_feeds/waves_feed.py b/dexbot/strategies/external_feeds/waves_feed.py new file mode 100644 index 000000000..02b428997 --- /dev/null +++ b/dexbot/strategies/external_feeds/waves_feed.py @@ -0,0 +1,9 @@ +import requests + +# waves feed - todo + +# GET /ticker/{AMOUNT_ASSET}/{PRICE_ASSET} +ticker_url = https://marketdata.wavesplatform.com/api/ticker/BTC/USD + +#Get Symbols Example: +symbols_url = http://marketdata.wavesplatform.com/api/symbols From a65d33367b75d17b332ce6b15e6ce860a457794e Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 14 Nov 2018 00:00:13 +0500 Subject: [PATCH 0985/1846] Improve fallback logic When target spread is not reached, first try to compare free balances. If some side is bigger than another, cancel order on that side. If there is no free balance on any side, just measure which side is more far from market center price and cancel order on it. --- dexbot/strategies/staggered_orders.py | 57 +++++++++++++++++++-------- 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 0bc387cd8..71450f453 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -199,12 +199,18 @@ def maintain_strategy(self, *args, **kwargs): # BASE asset check if self.base_balance > self.base_asset_threshold: # Allocate available BASE funds + base_allocated = False self.allocate_asset('base', self.base_balance) + else: + base_allocated = True # QUOTE asset check if self.quote_balance > self.quote_asset_threshold: # Allocate available QUOTE funds + quote_allocated = False self.allocate_asset('quote', self.quote_balance) + else: + quote_allocated = True # Send pending operations if not self.bitshares.txbuffer.is_empty(): @@ -268,24 +274,41 @@ def maintain_strategy(self, *args, **kwargs): self.log_maintenance_time() return - # Measure which price is closer to the center - buy_distance = self.market_center_price - highest_buy_price - sell_distance = lowest_sell_price - self.market_center_price - - if buy_distance > sell_distance: - if self.market_center_price > highest_buy_price * (1 + self.target_spread): - # Cancel lowest buy order because center price moved up. - # On the next run there will be placed next buy order closer to the new center - self.log.info('Free balances are not changing and we are not in bootstrap mode and target spread is ' - 'not reached. Cancelling lowest buy order as a fallback.') - self.cancel(self.buy_orders[-1]) + # What amount of quote may be obtained if buy using avail base balance + can_obtain_quote = self.base_balance['amount'] * self.market_center_price + side_to_cancel = None + + """ The logic is following: compare on which side we have bigger free balance, then cancel furthest order on + that side to be able to place closer order. This is for situations when amount obtained from previous trade + is not enough to place closer order. + """ + if can_obtain_quote > self.quote_balance['amount'] and not base_allocated: + side_to_cancel = 'buy' + elif self.quote_balance['amount'] > can_obtain_quote and not quote_allocated: + side_to_cancel = 'sell' + + if not side_to_cancel: + """ Balance-based cancel logic didn't give a result, so use logic based on distance to market center + """ + # Measure which price is closer to the center + buy_distance = self.market_center_price - highest_buy_price + sell_distance = lowest_sell_price - self.market_center_price + + if buy_distance > sell_distance: + side_to_cancel = 'buy' + else: + side_to_cancel = 'sell' + + if side_to_cancel == 'buy': + self.log.info('Free balances are not changing, bootstrap is off and target spread is not reached. ' + 'Cancelling lowest buy order as a fallback') + self.cancel(self.buy_orders[-1]) + elif side_to_cancel == 'sell': + self.log.info('Free balances are not changing, bootstrap is off and target spread is not reached. ' + 'Cancelling highest sell order as a fallback') + self.cancel(self.sell_orders[-1]) else: - if self.market_center_price < lowest_sell_price * (1 - self.target_spread): - # Cancel highest sell order because center price moved down. - # On the next run there will be placed next sell closer to the new center - self.log.info('Free balances are not changing and we are not in bootstrap mode and target spread is ' - 'not reached. Cancelling highest sell order as a fallback.') - self.cancel(self.sell_orders[-1]) + self.log.info('Target spread is not reached but cannot determine what furthest order to cancel') self.last_check = datetime.now() self.log_maintenance_time() From 3e25d2c44cbc853d57201d7cddfe1430b183d9d6 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 13 Nov 2018 14:56:55 -0800 Subject: [PATCH 0986/1846] add tests for waves --- .../external_feeds/tests/waves_test.py | 25 +++++++++++++++++ .../strategies/external_feeds/waves_feed.py | 28 +++++++++++++++---- 2 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 dexbot/strategies/external_feeds/tests/waves_test.py diff --git a/dexbot/strategies/external_feeds/tests/waves_test.py b/dexbot/strategies/external_feeds/tests/waves_test.py new file mode 100644 index 000000000..bffe25cb7 --- /dev/null +++ b/dexbot/strategies/external_feeds/tests/waves_test.py @@ -0,0 +1,25 @@ +import pywaves as pw + +# GET /ticker/{AMOUNT_ASSET}/{PRICE_ASSET} +#ticker_url = https://marketdata.wavesplatform.com/api/ticker/BTC/USD + +# set the asset pair +WAVES_BTC = pw.AssetPair(pw.WAVES, pw.BTC) + +# get last price and volume +print("%s %s" % (WAVES_BTC.last(), WAVES_BTC.volume())) + +# get ticker +ticker = WAVES_BTC.ticker() +print(ticker['24h_open']) +print(ticker['24h_vwap']) + +# get last 10 trades +trades = WAVES_BTC.trades(10) +for t in trades: + print("%s %s %s %s" % (t['buyer'], t['seller'], t['price'], t['amount'])) + +# get last 10 daily OHLCV candles +ohlcv = WAVES_BTC.candles(1440, 10) +for t in ohlcv: + print("%s %s %s %s %s" % (t['open'], t['high'], t['low'], t['close'], t['volume'])) diff --git a/dexbot/strategies/external_feeds/waves_feed.py b/dexbot/strategies/external_feeds/waves_feed.py index 02b428997..bffe25cb7 100644 --- a/dexbot/strategies/external_feeds/waves_feed.py +++ b/dexbot/strategies/external_feeds/waves_feed.py @@ -1,9 +1,25 @@ -import requests - -# waves feed - todo +import pywaves as pw # GET /ticker/{AMOUNT_ASSET}/{PRICE_ASSET} -ticker_url = https://marketdata.wavesplatform.com/api/ticker/BTC/USD +#ticker_url = https://marketdata.wavesplatform.com/api/ticker/BTC/USD + +# set the asset pair +WAVES_BTC = pw.AssetPair(pw.WAVES, pw.BTC) + +# get last price and volume +print("%s %s" % (WAVES_BTC.last(), WAVES_BTC.volume())) + +# get ticker +ticker = WAVES_BTC.ticker() +print(ticker['24h_open']) +print(ticker['24h_vwap']) -#Get Symbols Example: -symbols_url = http://marketdata.wavesplatform.com/api/symbols +# get last 10 trades +trades = WAVES_BTC.trades(10) +for t in trades: + print("%s %s %s %s" % (t['buyer'], t['seller'], t['price'], t['amount'])) + +# get last 10 daily OHLCV candles +ohlcv = WAVES_BTC.candles(1440, 10) +for t in ohlcv: + print("%s %s %s %s %s" % (t['open'], t['high'], t['low'], t['close'], t['volume'])) From 8ac787c7c357ba2e5b9969b761d604fac5bb6d98 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 13 Nov 2018 18:16:03 -0800 Subject: [PATCH 0987/1846] integrate waves, gecko into price_feed --- .../strategies/external_feeds/gecko_feed.py | 6 +- .../strategies/external_feeds/price_feed.py | 15 ++-- .../strategies/external_feeds/process_pair.py | 1 - .../external_feeds/tests/waves_test.py | 8 +++ .../strategies/external_feeds/waves_feed.py | 71 ++++++++++++------- 5 files changed, 67 insertions(+), 34 deletions(-) diff --git a/dexbot/strategies/external_feeds/gecko_feed.py b/dexbot/strategies/external_feeds/gecko_feed.py index 4d38fb02e..3ed1a0ae8 100644 --- a/dexbot/strategies/external_feeds/gecko_feed.py +++ b/dexbot/strategies/external_feeds/gecko_feed.py @@ -23,7 +23,7 @@ def print_usage(): "Symbol is required, for example:", yellow('BTC/USD'), sep='') -def get_gecko_json(url): +def get_json(url): r = requests.get(url) json_obj = r.json() return json_obj @@ -39,12 +39,12 @@ def check_gecko_symbol_exists(coin_list, symbol): def get_gecko_market_price(base, quote): try: - coin_list = get_gecko_json(GECKO_COINS_URL+'list') + coin_list = get_json(GECKO_COINS_URL+'list') quote_name = check_gecko_symbol_exists(coin_list, quote.lower()) lookup_pair = "?vs_currency="+base.lower()+"&ids="+quote_name market_url = GECKO_COINS_URL+'markets'+lookup_pair debug(market_url) - ticker = get_gecko_json(market_url) + ticker = get_json(market_url) current_price = None for entry in ticker: current_price = entry['current_price'] diff --git a/dexbot/strategies/external_feeds/price_feed.py b/dexbot/strategies/external_feeds/price_feed.py index 9f10b10ea..eb94b8fe2 100644 --- a/dexbot/strategies/external_feeds/price_feed.py +++ b/dexbot/strategies/external_feeds/price_feed.py @@ -1,8 +1,8 @@ from dexbot.strategies.external_feeds.ccxt_feed import get_ccxt_price -#from ccxt_feed import get_ccxt_price - +from dexbot.strategies.external_feeds.waves_feed import get_waves_price +from dexbot.strategies.external_feeds.process_pair import split_pair #from gecko_feed import get_gecko_price -#from waves_feed import get_waves_price + class PriceFeed: @@ -13,6 +13,7 @@ def __init__(self, exchange, symbol): def get_center_price(self): + pair = split_pair(symbol) price = None if self.exchange not in self.alt_exchanges: print("use ccxt exchange ", self.exchange, ' symbol ', self.symbol, sep=":") @@ -20,15 +21,19 @@ def get_center_price(self): elif self.exchange == 'gecko': print("gecko - WIP todo") elif self.exchange == 'waves': - print("waves - WIP todo") + print("use waves -", self.exchange, ' symbol ', self.symbol, sep=":") + price = get_waves_price(pair[1], pair[0]) + return price + if __name__ == '__main__': exchanges = ['bitfinex', 'kraken', 'gecko', 'waves'] - symbol = 'BTC/USD' + symbol = 'BTC/USD' # quote/base for external exchanges for exchange in exchanges: pf = PriceFeed(exchange, symbol) center_price = pf.get_center_price() print("center price: ", center_price) + diff --git a/dexbot/strategies/external_feeds/process_pair.py b/dexbot/strategies/external_feeds/process_pair.py index ba6a4739a..acdc11824 100644 --- a/dexbot/strategies/external_feeds/process_pair.py +++ b/dexbot/strategies/external_feeds/process_pair.py @@ -1,6 +1,5 @@ import re - def print_args(*args): print(' '.join([str(arg) for arg in args])) diff --git a/dexbot/strategies/external_feeds/tests/waves_test.py b/dexbot/strategies/external_feeds/tests/waves_test.py index bffe25cb7..76c44dfbe 100644 --- a/dexbot/strategies/external_feeds/tests/waves_test.py +++ b/dexbot/strategies/external_feeds/tests/waves_test.py @@ -3,6 +3,14 @@ # GET /ticker/{AMOUNT_ASSET}/{PRICE_ASSET} #ticker_url = https://marketdata.wavesplatform.com/api/ticker/BTC/USD +def get_asset(symbol, coin_list): + asset_id = None + try: + asset_id = [obj for obj in coin_list if obj['symbol'] == symbol][0]['assetID'] + except IndexError as e: + print(e) + return pw.Asset(asset_id) + # set the asset pair WAVES_BTC = pw.AssetPair(pw.WAVES, pw.BTC) diff --git a/dexbot/strategies/external_feeds/waves_feed.py b/dexbot/strategies/external_feeds/waves_feed.py index bffe25cb7..13a844e66 100644 --- a/dexbot/strategies/external_feeds/waves_feed.py +++ b/dexbot/strategies/external_feeds/waves_feed.py @@ -1,25 +1,46 @@ -import pywaves as pw - -# GET /ticker/{AMOUNT_ASSET}/{PRICE_ASSET} -#ticker_url = https://marketdata.wavesplatform.com/api/ticker/BTC/USD - -# set the asset pair -WAVES_BTC = pw.AssetPair(pw.WAVES, pw.BTC) - -# get last price and volume -print("%s %s" % (WAVES_BTC.last(), WAVES_BTC.volume())) - -# get ticker -ticker = WAVES_BTC.ticker() -print(ticker['24h_open']) -print(ticker['24h_vwap']) - -# get last 10 trades -trades = WAVES_BTC.trades(10) -for t in trades: - print("%s %s %s %s" % (t['buyer'], t['seller'], t['price'], t['amount'])) - -# get last 10 daily OHLCV candles -ohlcv = WAVES_BTC.candles(1440, 10) -for t in ohlcv: - print("%s %s %s %s %s" % (t['open'], t['high'], t['low'], t['close'], t['volume'])) +import requests + +WAVES_URL = 'https://marketdata.wavesplatform.com/api/' +SYMBOLS_URL = "/symbols" +MARKET_URL = "/ticker/" + + +def get_json(url): + r = requests.get(url) + json_obj = r.json() + return json_obj + +def get_waves_symbols(): + symbol_list = get_json(WAVES_URL + SYMBOLS_URL) + return symbol_list + + +def get_last_price(base, quote): + current_price = None + try: + market_bq = MARKET_URL + quote +'/'+ base # external exchange format + ticker = get_json(WAVES_URL + market_bq) + current_price = ticker['24h_close'] + except Exception as e: + pass # No pair found on waves dex for external price. + return current_price + + +def get_waves_price(base, quote): + current_price = get_last_price(base, quote) + + if current_price is None: # try inversion + price = get_last_price(quote, base) + current_price = 1/float(price) + return current_price + + + +if __name__ == '__main__': + + symbol = 'BTC/USD' # quote/base for external exchanges + print(symbol, "=") + pair = split_pair(symbol) + current_price = get_waves_price(pair[1], pair[0]) + print(current_price) + From 510d7c9b28b495d718c0c98d23f1806e270262e6 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 13 Nov 2018 18:16:47 -0800 Subject: [PATCH 0988/1846] test integration --- .../strategies/external_feeds/gecko_feed.py | 51 ++++++++++--------- .../strategies/external_feeds/price_feed.py | 17 +++++-- .../strategies/external_feeds/process_pair.py | 7 +++ .../strategies/external_feeds/waves_feed.py | 8 +-- 4 files changed, 52 insertions(+), 31 deletions(-) diff --git a/dexbot/strategies/external_feeds/gecko_feed.py b/dexbot/strategies/external_feeds/gecko_feed.py index 3ed1a0ae8..77ed7c601 100644 --- a/dexbot/strategies/external_feeds/gecko_feed.py +++ b/dexbot/strategies/external_feeds/gecko_feed.py @@ -2,7 +2,8 @@ import requests from dexbot.strategies.external_feeds.styles import yellow -from dexbot.strategies.external_feeds.process_pair import split_pair, filter_prefix_symbol, filter_bit_symbol +from dexbot.strategies.external_feeds.process_pair import split_pair, filter_prefix_symbol, filter_bit_symbol, debug + """ To use Gecko API, note that gecko does not provide pairs by default. For base/quote one must be listed as ticker and the other as fullname, @@ -10,12 +11,6 @@ https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin """ GECKO_COINS_URL = 'https://api.coingecko.com/api/v3/coins/' -isDebug = False - - -def debug(*args): - if isDebug: - print(' '.join([str(arg) for arg in args])) def print_usage(): @@ -37,7 +32,7 @@ def check_gecko_symbol_exists(coin_list, symbol): return None -def get_gecko_market_price(base, quote): +def get_market_price(base, quote): try: coin_list = get_json(GECKO_COINS_URL+'list') quote_name = check_gecko_symbol_exists(coin_list, quote.lower()) @@ -56,30 +51,20 @@ def get_gecko_market_price(base, quote): return None -# Unit tests -# Todo: Move tests to own files -@click.group() -def main(): - pass - - -@main.command() -@click.argument('symbol') -def test_feed(symbol): - """ - [symbol] Symbol example: btc/usd or btc:usd - """ +def get_gecko_price(symbol): + current_price = None try: pair = split_pair(symbol) # pair=[quote, base] filtered_pair = [filter_bit_symbol(j) for j in [filter_prefix_symbol(i) for i in pair]] debug(filtered_pair) new_quote = filtered_pair[0] new_base = filtered_pair[1] - current_price = get_gecko_market_price(new_base, new_quote) - + current_price = get_market_price(new_base, new_quote) + if current_price is None: # Try inverted version debug(" Trying pair inversion...") - current_price = get_gecko_market_price(new_quote, new_base) + current_price = get_market_price(new_quote, new_base) + print(new_base + '/' + new_quote, str(current_price), sep=':') if current_price is not None: # Re-invert price actual_price = 1/current_price @@ -89,6 +74,24 @@ def test_feed(symbol): except Exception as e: print(type(e).__name__, e.args, str(e)) print_usage() + return current_price + + +# Unit tests +# Todo: Move tests to own files +@click.group() +def main(): + pass + + +@main.command() +@click.argument('symbol') +def test_feed(symbol): + """ + [symbol] Symbol example: btc/usd or btc:usd + """ + price = get_gecko_price(symbol) + print(price) if __name__ == '__main__': diff --git a/dexbot/strategies/external_feeds/price_feed.py b/dexbot/strategies/external_feeds/price_feed.py index eb94b8fe2..18e47e34b 100644 --- a/dexbot/strategies/external_feeds/price_feed.py +++ b/dexbot/strategies/external_feeds/price_feed.py @@ -1,6 +1,8 @@ from dexbot.strategies.external_feeds.ccxt_feed import get_ccxt_price from dexbot.strategies.external_feeds.waves_feed import get_waves_price -from dexbot.strategies.external_feeds.process_pair import split_pair +from dexbot.strategies.external_feeds.gecko_feed import get_gecko_price + +from dexbot.strategies.external_feeds.process_pair import split_pair, filter_prefix_symbol, filter_bit_symbol, debug #from gecko_feed import get_gecko_price @@ -10,19 +12,25 @@ def __init__(self, exchange, symbol): self.exchange = exchange self.symbol = symbol self.alt_exchanges = ['gecko', 'waves'] # assume all other exchanges are ccxt + self.pair = split_pair(self.symbol) + def prefilter(self): + raw_pair = self.pair + self.pair = [filter_bit_symbol(j) for j in [filter_prefix_symbol(i) for i in raw_pair]] + + def get_center_price(self): - pair = split_pair(symbol) price = None if self.exchange not in self.alt_exchanges: print("use ccxt exchange ", self.exchange, ' symbol ', self.symbol, sep=":") price = get_ccxt_price(self.symbol, self.exchange) elif self.exchange == 'gecko': - print("gecko - WIP todo") + print("gecko exchange - ", self.exchange, ' symbol ', self.symbol, sep=":") + price = get_gecko_price(self.symbol) elif self.exchange == 'waves': print("use waves -", self.exchange, ' symbol ', self.symbol, sep=":") - price = get_waves_price(pair[1], pair[0]) + price = get_waves_price(self.pair[1], self.pair[0]) return price @@ -34,6 +42,7 @@ def get_center_price(self): for exchange in exchanges: pf = PriceFeed(exchange, symbol) + pf.prefilter() center_price = pf.get_center_price() print("center price: ", center_price) diff --git a/dexbot/strategies/external_feeds/process_pair.py b/dexbot/strategies/external_feeds/process_pair.py index acdc11824..574ce6314 100644 --- a/dexbot/strategies/external_feeds/process_pair.py +++ b/dexbot/strategies/external_feeds/process_pair.py @@ -1,5 +1,12 @@ import re +isDebug = False + +def debug(*args): + if isDebug: + print(' '.join([str(arg) for arg in args])) + + def print_args(*args): print(' '.join([str(arg) for arg in args])) diff --git a/dexbot/strategies/external_feeds/waves_feed.py b/dexbot/strategies/external_feeds/waves_feed.py index 13a844e66..686c27790 100644 --- a/dexbot/strategies/external_feeds/waves_feed.py +++ b/dexbot/strategies/external_feeds/waves_feed.py @@ -1,4 +1,5 @@ import requests +from dexbot.strategies.external_feeds.process_pair import split_pair, filter_prefix_symbol, filter_bit_symbol, debug WAVES_URL = 'https://marketdata.wavesplatform.com/api/' SYMBOLS_URL = "/symbols" @@ -27,8 +28,7 @@ def get_last_price(base, quote): def get_waves_price(base, quote): - current_price = get_last_price(base, quote) - + current_price = get_last_price(base, quote) if current_price is None: # try inversion price = get_last_price(quote, base) current_price = 1/float(price) @@ -40,7 +40,9 @@ def get_waves_price(base, quote): symbol = 'BTC/USD' # quote/base for external exchanges print(symbol, "=") - pair = split_pair(symbol) + raw_pair = split_pair(symbol) + pair = [filter_bit_symbol(j) for j in [filter_prefix_symbol(i) for i in raw_pair]] + current_price = get_waves_price(pair[1], pair[0]) print(current_price) From 0f005629debc8cf8f093dedf0f71a52f2f11c215 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 13 Nov 2018 22:38:58 -0800 Subject: [PATCH 0989/1846] refactor params for waves --- .../strategies/external_feeds/gecko_feed.py | 6 ++-- .../strategies/external_feeds/price_feed.py | 8 ++--- .../strategies/external_feeds/waves_feed.py | 30 ++++++++++++++----- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/dexbot/strategies/external_feeds/gecko_feed.py b/dexbot/strategies/external_feeds/gecko_feed.py index 77ed7c601..c6048771b 100644 --- a/dexbot/strategies/external_feeds/gecko_feed.py +++ b/dexbot/strategies/external_feeds/gecko_feed.py @@ -65,12 +65,12 @@ def get_gecko_price(symbol): debug(" Trying pair inversion...") current_price = get_market_price(new_quote, new_base) - print(new_base + '/' + new_quote, str(current_price), sep=':') + debug(new_base + '/' + new_quote, str(current_price)) if current_price is not None: # Re-invert price actual_price = 1/current_price - print(new_quote + '/' + new_base, str(actual_price), sep=':') + debug(new_quote + '/' + new_base, str(actual_price)) else: - print(symbol, current_price, sep=':') + debug(symbol, current_price) except Exception as e: print(type(e).__name__, e.args, str(e)) print_usage() diff --git a/dexbot/strategies/external_feeds/price_feed.py b/dexbot/strategies/external_feeds/price_feed.py index 18e47e34b..4f35dbe41 100644 --- a/dexbot/strategies/external_feeds/price_feed.py +++ b/dexbot/strategies/external_feeds/price_feed.py @@ -3,7 +3,6 @@ from dexbot.strategies.external_feeds.gecko_feed import get_gecko_price from dexbot.strategies.external_feeds.process_pair import split_pair, filter_prefix_symbol, filter_bit_symbol, debug -#from gecko_feed import get_gecko_price class PriceFeed: @@ -30,19 +29,18 @@ def get_center_price(self): price = get_gecko_price(self.symbol) elif self.exchange == 'waves': print("use waves -", self.exchange, ' symbol ', self.symbol, sep=":") - price = get_waves_price(self.pair[1], self.pair[0]) - + price = get_waves_price(symbol_=self.symbol) return price - if __name__ == '__main__': - exchanges = ['bitfinex', 'kraken', 'gecko', 'waves'] + exchanges = ['gecko', 'bitfinex', 'kraken', 'waves'] symbol = 'BTC/USD' # quote/base for external exchanges for exchange in exchanges: pf = PriceFeed(exchange, symbol) pf.prefilter() + print(pf.pair) center_price = pf.get_center_price() print("center price: ", center_price) diff --git a/dexbot/strategies/external_feeds/waves_feed.py b/dexbot/strategies/external_feeds/waves_feed.py index 686c27790..9c1daa2c7 100644 --- a/dexbot/strategies/external_feeds/waves_feed.py +++ b/dexbot/strategies/external_feeds/waves_feed.py @@ -11,6 +11,7 @@ def get_json(url): json_obj = r.json() return json_obj + def get_waves_symbols(): symbol_list = get_json(WAVES_URL + SYMBOLS_URL) return symbol_list @@ -27,22 +28,37 @@ def get_last_price(base, quote): return current_price -def get_waves_price(base, quote): - current_price = get_last_price(base, quote) +def get_waves_by_pair(pair): + current_price = get_last_price(pair[1], pair[0]) # base, quote if current_price is None: # try inversion - price = get_last_price(quote, base) + price = get_last_price(pair[0], pair[1]) current_price = 1/float(price) return current_price +def get_waves_price(**kwargs): + price = None + for key, value in list(kwargs.items()): + print("The value of {} is {}".format(key, value)) + if key == "pair_": + price = get_waves_by_pair(value) + elif key == "symbol_": + pair = split_pair(value) + price = get_waves_by_pair(pair) + return price + if __name__ == '__main__': symbol = 'BTC/USD' # quote/base for external exchanges print(symbol, "=") - raw_pair = split_pair(symbol) - pair = [filter_bit_symbol(j) for j in [filter_prefix_symbol(i) for i in raw_pair]] + raw_pair = split_pair(symbol) + pair = [filter_bit_symbol(j) for j in [filter_prefix_symbol(i) for i in raw_pair]] - current_price = get_waves_price(pair[1], pair[0]) - print(current_price) + pair_price = get_waves_price(pair_=pair) + print("pair price", pair_price) + +# current_price = get_waves_price(pair[1], pair[0]) + current_price = get_waves_price(symbol_=symbol) + print("symbol price", current_price) From 2658469b065105024c4c8e59c59ef4aa765a82ef Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Wed, 14 Nov 2018 12:56:13 -0800 Subject: [PATCH 0990/1846] add pywaves to requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 22fb689dd..0b29432c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ aiohttp requests>=2.20.0 yarl==1.1.0 ccxt==1.17.434 +pywaves From 5d4713538fced44ceec5441010da9968e1c87e5c Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Wed, 14 Nov 2018 18:37:36 -0800 Subject: [PATCH 0991/1846] mult args for get price --- .../strategies/external_feeds/gecko_feed.py | 39 ++++++++++++------- .../strategies/external_feeds/price_feed.py | 33 +++++++++++++--- .../strategies/external_feeds/process_pair.py | 2 +- 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/dexbot/strategies/external_feeds/gecko_feed.py b/dexbot/strategies/external_feeds/gecko_feed.py index c6048771b..f147b61f0 100644 --- a/dexbot/strategies/external_feeds/gecko_feed.py +++ b/dexbot/strategies/external_feeds/gecko_feed.py @@ -51,31 +51,38 @@ def get_market_price(base, quote): return None -def get_gecko_price(symbol): +def get_gecko_price_by_pair(pair): current_price = None try: - pair = split_pair(symbol) # pair=[quote, base] - filtered_pair = [filter_bit_symbol(j) for j in [filter_prefix_symbol(i) for i in pair]] - debug(filtered_pair) - new_quote = filtered_pair[0] - new_base = filtered_pair[1] - current_price = get_market_price(new_base, new_quote) - + quote = pair[0] + base = pair[1] + current_price = get_market_price(base, quote) if current_price is None: # Try inverted version debug(" Trying pair inversion...") - current_price = get_market_price(new_quote, new_base) - - debug(new_base + '/' + new_quote, str(current_price)) + current_price = get_market_price(quote, base) + debug(base + '/' + quote, str(current_price)) if current_price is not None: # Re-invert price actual_price = 1/current_price - debug(new_quote + '/' + new_base, str(actual_price)) + debug(quote + '/' + base, str(actual_price)) else: - debug(symbol, current_price) + debug(pair, current_price) except Exception as e: print(type(e).__name__, e.args, str(e)) - print_usage() return current_price + +def get_gecko_price(**kwargs): + price = None + for key, value in list(kwargs.items()): + debug("The value of {} is {}".format(key, value)) # debug + if key == "pair_": + price = get_gecko_price_by_pair(value) + elif key == "symbol_": + pair = split_pair(value) # pair=[quote, base] + price = get_gecko_price_by_pair(pair) + return price + + # Unit tests # Todo: Move tests to own files @@ -90,9 +97,11 @@ def test_feed(symbol): """ [symbol] Symbol example: btc/usd or btc:usd """ - price = get_gecko_price(symbol) + price = get_gecko_price(symbol_=symbol) print(price) + pair = split_pair(symbol) + price = get_gecko_price(pair_=pair) if __name__ == '__main__': main() diff --git a/dexbot/strategies/external_feeds/price_feed.py b/dexbot/strategies/external_feeds/price_feed.py index 4f35dbe41..373f51558 100644 --- a/dexbot/strategies/external_feeds/price_feed.py +++ b/dexbot/strategies/external_feeds/price_feed.py @@ -4,6 +4,21 @@ from dexbot.strategies.external_feeds.process_pair import split_pair, filter_prefix_symbol, filter_bit_symbol, debug +""" +Note from Marko Paasila: + +We have been calling the unit-of-measure BASE and the asset-of-interest QUOTE. +Since there seem to be confusing definitions around, we just had to settle one way, and be consistent. +We chose the way @xeroc had in python-bitshares, where the market is BTSUSD or BTS:USD or BTS/USD, +and price is USD/BTS. This is opposite to how bitshares-ui shows it (or I'm not sure of that, +but at least unit-of-measure is QUOTE and not BASE there). + +So in DEXBot: + +unit of measure = BASE +asset of interest = QUOTE + +""" class PriceFeed: @@ -14,11 +29,15 @@ def __init__(self, exchange, symbol): self.pair = split_pair(self.symbol) - def prefilter(self): + def filter_symbols(self): raw_pair = self.pair self.pair = [filter_bit_symbol(j) for j in [filter_prefix_symbol(i) for i in raw_pair]] - - + debug(self.pair) + +# def get_usd_alternative(self): +# def get_consolidated_alternative(self): + + def get_center_price(self): price = None if self.exchange not in self.alt_exchanges: @@ -26,20 +45,22 @@ def get_center_price(self): price = get_ccxt_price(self.symbol, self.exchange) elif self.exchange == 'gecko': print("gecko exchange - ", self.exchange, ' symbol ', self.symbol, sep=":") - price = get_gecko_price(self.symbol) + price = get_gecko_price(symbol_=self.symbol) elif self.exchange == 'waves': print("use waves -", self.exchange, ' symbol ', self.symbol, sep=":") price = get_waves_price(symbol_=self.symbol) + return price if __name__ == '__main__': - exchanges = ['gecko', 'bitfinex', 'kraken', 'waves'] + + exchanges = ['gecko', 'bitfinex', 'kraken', 'waves', 'gdax', 'binance'] symbol = 'BTC/USD' # quote/base for external exchanges for exchange in exchanges: pf = PriceFeed(exchange, symbol) - pf.prefilter() + pf.filter_symbols() print(pf.pair) center_price = pf.get_center_price() print("center price: ", center_price) diff --git a/dexbot/strategies/external_feeds/process_pair.py b/dexbot/strategies/external_feeds/process_pair.py index 574ce6314..23d5bb1c6 100644 --- a/dexbot/strategies/external_feeds/process_pair.py +++ b/dexbot/strategies/external_feeds/process_pair.py @@ -1,6 +1,6 @@ import re -isDebug = False +isDebug = True def debug(*args): if isDebug: From 3c5d0d3af469018d5c83bcdf40fcc011c9e49b35 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 15 Nov 2018 17:41:36 -0800 Subject: [PATCH 0992/1846] usdt parsing and debugging --- dexbot/strategies/external_feeds/ccxt_feed.py | 31 +++- .../strategies/external_feeds/price_feed.py | 134 +++++++++++++----- .../strategies/external_feeds/process_pair.py | 7 +- 3 files changed, 132 insertions(+), 40 deletions(-) mode change 100644 => 100755 dexbot/strategies/external_feeds/price_feed.py diff --git a/dexbot/strategies/external_feeds/ccxt_feed.py b/dexbot/strategies/external_feeds/ccxt_feed.py index 56c4b24d8..b37e8f2d7 100644 --- a/dexbot/strategies/external_feeds/ccxt_feed.py +++ b/dexbot/strategies/external_feeds/ccxt_feed.py @@ -1,12 +1,10 @@ import click +import json import asyncio import functools import ccxt.async_support as accxt from pprint import pprint -def get_exchanges(): - return ccxt.exchanges - async def print_ticker(symbol, id): # Verbose mode will show the order of execution to verify concurrency @@ -15,6 +13,13 @@ async def print_ticker(symbol, id): await exchange.close() +async def get_ccxt_load_markets(exchange_id): + exchange = getattr(accxt, exchange_id)({'verbose': False}) + symbols = await exchange.load_markets() + await exchange.close() + return symbols + + async def fetch_ticker(exchange, symbol): ticker = None try: @@ -33,17 +38,29 @@ def get_ccxt_price(symbol, exchange_name): """ Get all tickers from multiple exchanges using async """ center_price = None exchange = getattr(accxt, exchange_name)({'verbose':False}) - ticker = asyncio.get_event_loop().run_until_complete( - fetch_ticker(exchange, symbol)) + ticker = asyncio.get_event_loop().run_until_complete(fetch_ticker(exchange, symbol)) if ticker: center_price = (ticker['bid'] + ticker['ask'])/2 return center_price if __name__ == '__main__': - + # result = asyncio.get_event_loop().run_until_complete(print_ticker(symbol, 'bitfinex')) exchanges =['bitfinex', 'kraken', 'binance', 'gdax'] - symbol = 'BTC/USDT' + symbol = 'BTC/USDT' + + # testing get all pairs. + for exchange_id in exchanges: + print("\n\n\n") + print(exchange_id) + result = asyncio.get_event_loop().run_until_complete(get_ccxt_load_markets(exchange_id)) + all_symbols= [obj for obj in result] + print(all_symbols) + + # testing get center price + # center_price = [asyncio.get_event_loop().run_until_complete(get_ccxt_price(symbol, e)) for e in exchanges] + center_price = [get_ccxt_price(symbol, e) for e in exchanges] print(' exchange: ', exchanges, ' symbol: ', symbol, sep=':') print(' center_price: ', center_price) + diff --git a/dexbot/strategies/external_feeds/price_feed.py b/dexbot/strategies/external_feeds/price_feed.py old mode 100644 new mode 100755 index 373f51558..921a1a8dc --- a/dexbot/strategies/external_feeds/price_feed.py +++ b/dexbot/strategies/external_feeds/price_feed.py @@ -1,67 +1,137 @@ from dexbot.strategies.external_feeds.ccxt_feed import get_ccxt_price from dexbot.strategies.external_feeds.waves_feed import get_waves_price from dexbot.strategies.external_feeds.gecko_feed import get_gecko_price - -from dexbot.strategies.external_feeds.process_pair import split_pair, filter_prefix_symbol, filter_bit_symbol, debug +from dexbot.strategies.external_feeds.process_pair import split_pair, join_pair, filter_prefix_symbol, filter_bit_symbol, debug +import re """ Note from Marko Paasila: - We have been calling the unit-of-measure BASE and the asset-of-interest QUOTE. -Since there seem to be confusing definitions around, we just had to settle one way, and be consistent. We chose the way @xeroc had in python-bitshares, where the market is BTSUSD or BTS:USD or BTS/USD, and price is USD/BTS. This is opposite to how bitshares-ui shows it (or I'm not sure of that, but at least unit-of-measure is QUOTE and not BASE there). So in DEXBot: - unit of measure = BASE asset of interest = QUOTE - """ class PriceFeed: def __init__(self, exchange, symbol): - self.exchange = exchange - self.symbol = symbol - self.alt_exchanges = ['gecko', 'waves'] # assume all other exchanges are ccxt - self.pair = split_pair(self.symbol) + self._alt_exchanges = ['gecko', 'waves'] # assume all other exchanges are ccxt + self._exchange= exchange + self._symbol=symbol + self._pair= split_pair(symbol) + + + @property + def symbol(self): + return self._symbol + + + @symbol.setter + def symbol(self, symbol): + self._symbol = symbol + self._pair = split_pair(self._symbol) + @property + def pair(self): + return self._pair + + + @pair.setter + def pair(self, pair): + self._pair = pair + self._symbol = join_pair(pair) + + + @property + def exchange(self): + return self._exchange + + + @exchange.setter + def exchange(self, exchange): + self._exchange = exchange + + def filter_symbols(self): - raw_pair = self.pair - self.pair = [filter_bit_symbol(j) for j in [filter_prefix_symbol(i) for i in raw_pair]] - debug(self.pair) + raw_pair = self._pair + self._pair = [filter_bit_symbol(j) for j in [filter_prefix_symbol(i) for i in raw_pair]] + debug(self._pair) + + + def set_alt_usd_pair(self): + """ + get center price by search and replace for USD with USDT only + extend this method in the future for other usdt like options, e.g. USDC, TUSD,etc + """ + alt_usd_pair = self._pair + i = 0 + while i < 2: + if re.match(r'^USD$', self._pair[i], re.I): + alt_usd_pair[i] = re.sub(r'USD','USDT', self._pair[i]) + i = i+1 + self._pair = alt_usd_pair + self._symbol = join_pair(self._pair) + -# def get_usd_alternative(self): -# def get_consolidated_alternative(self): + def get_center_price(self, type): + if type == "USDT": + self.set_alt_usd_pair() + print(symbol) + return self._get_center_price() - def get_center_price(self): + + + def _get_center_price(self): + symbol = self._symbol price = None - if self.exchange not in self.alt_exchanges: - print("use ccxt exchange ", self.exchange, ' symbol ', self.symbol, sep=":") - price = get_ccxt_price(self.symbol, self.exchange) - elif self.exchange == 'gecko': - print("gecko exchange - ", self.exchange, ' symbol ', self.symbol, sep=":") - price = get_gecko_price(symbol_=self.symbol) - elif self.exchange == 'waves': - print("use waves -", self.exchange, ' symbol ', self.symbol, sep=":") - price = get_waves_price(symbol_=self.symbol) - + if self._exchange not in self._alt_exchanges: + print("use ccxt exchange ", self._exchange, ' symbol ', symbol, sep=":") + price = get_ccxt_price(symbol, self._exchange) + elif self._exchange == 'gecko': + print("gecko exchange - ", self._exchange, ' symbol ', symbol, sep=":") + price = get_gecko_price(symbol_=symbol) + elif self._exchange == 'waves': + print("use waves -", self._exchange, ' symbol ', symbol, sep=":") + price = get_waves_price(symbol_=symbol) return price -if __name__ == '__main__': +if __name__ == '__main__': + center_price = None exchanges = ['gecko', 'bitfinex', 'kraken', 'waves', 'gdax', 'binance'] - symbol = 'BTC/USD' # quote/base for external exchanges + + # exchanges = ['bitfinex'] + symbol = 'BTC/USDT' + + """ + pf = PriceFeed('bitfinex', symbol) + pf.filter_symbols() + + center_price = get_ccxt_price(symbol, 'bitfinex') + print(center_price) + + center_price = pf.get_center_price(None) + print("center price: ", center_price) + + center_price = pf.get_center_price("USDT") + print("usdt center price: ", center_price) + """ + for exchange in exchanges: + symbol = 'BTC/USD' pf = PriceFeed(exchange, symbol) pf.filter_symbols() - print(pf.pair) - center_price = pf.get_center_price() - print("center price: ", center_price) - + center_price = pf.get_center_price(None) + print("center price: ", center_price) + if center_price is None: # try USDT + center_price = pf.get_center_price("USDT") + print("s/usd/usdt, center price: ", center_price) + diff --git a/dexbot/strategies/external_feeds/process_pair.py b/dexbot/strategies/external_feeds/process_pair.py index 23d5bb1c6..e2adaf6ea 100644 --- a/dexbot/strategies/external_feeds/process_pair.py +++ b/dexbot/strategies/external_feeds/process_pair.py @@ -10,7 +10,7 @@ def debug(*args): def print_args(*args): print(' '.join([str(arg) for arg in args])) - + def filter_prefix_symbol(symbol): # Example open.USD or bridge.USD, remove leading bit up to . base = '' @@ -36,6 +36,11 @@ def split_pair(symbol): return pair +def join_pair(pair): + symbol = pair[0]+'/'+pair[1] + return symbol + + def get_consolidated_pair(base, quote): # Split into two USD pairs, STEEM/BTS=(BTS/USD * USD/STEEM) pair1 = [base, 'USD'] # BTS/USD pair=[quote, base] From 626a13fda66d982f91c87a7b9e73f297567981e6 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 15 Nov 2018 17:43:06 -0800 Subject: [PATCH 0993/1846] remove dummy price --- dexbot/strategies/base.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index afa9892d7..082bec237 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -617,11 +617,14 @@ def get_external_market_center_price(self): print("market:", market, sep=' ') # debug print("exchange:", self.external_price_source, sep=' ') pf = PriceFeed(self.external_price_source, market) - center_price = pf.get_center_price() + center_price = pf.get_center_price(None) print(" PriceFeed ", center_price, sep=':') - - center_price = 0.10778888 # Dummy price for now - print("external dummy price", center_price, sep=' ') + if center_price is None: # try USDT + center_price = pf.get_center_price("USDT") + print("s/USD/USDT, center price: ", center_price) + +# center_price = 0.10778888 # Dummy price for now +# print("external dummy price", center_price, sep=' ') return center_price def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors=False): From 658f4e32aed539d9502d64a9a1ca51bcc220f698 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 15 Nov 2018 18:03:07 -0800 Subject: [PATCH 0994/1846] remove dummy, fix div by 0 in market_center_price --- dexbot/strategies/base.py | 19 +++++++++++------- .../strategies/external_feeds/price_feed.py | 20 ------------------- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 082bec237..9a7100bf8 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -611,18 +611,22 @@ def get_lowest_own_sell_order(self, orders=None): return None def get_external_market_center_price(self): - print("inside get_emcp, exchange: ", self.external_price_source, - "fetch depth", self.fetch_depth, sep=':') # debug + center_price = None + print("inside get_emcp, exchange: ", self.external_price_source, sep=':') # debug market = self.market.get_string('/') print("market:", market, sep=' ') # debug - print("exchange:", self.external_price_source, sep=' ') + +# market = 'BTC/USD' # override for testing debut +# print("dummy market:", market, sep=' ') # debug + + print("exchange:", self.external_price_source, sep=' ') pf = PriceFeed(self.external_price_source, market) + pf.filter_symbols() center_price = pf.get_center_price(None) print(" PriceFeed ", center_price, sep=':') if center_price is None: # try USDT center_price = pf.get_center_price("USDT") print("s/USD/USDT, center price: ", center_price) - # center_price = 0.10778888 # Dummy price for now # print("external dummy price", center_price, sep=' ') return center_price @@ -651,9 +655,10 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors self.log.critical("Cannot estimate center price, there is no lowest ask.") self.disabled = True return None - # Calculate and return market center price - center_price = buy_price * math.sqrt(sell_price / buy_price) - print("inside get_market_center_price : " , center_price, sep=' ') # debug + # Calculate and return market center price. make sure buy_price has value + if buy_price is not None: + center_price = buy_price * math.sqrt(sell_price / buy_price) + print("inside get_market_center_price : " , center_price, sep=' ') # debug return center_price def get_market_buy_price(self, quote_amount=0, base_amount=0, exclude_own_orders=True): diff --git a/dexbot/strategies/external_feeds/price_feed.py b/dexbot/strategies/external_feeds/price_feed.py index 921a1a8dc..0c38ea90d 100755 --- a/dexbot/strategies/external_feeds/price_feed.py +++ b/dexbot/strategies/external_feeds/price_feed.py @@ -78,14 +78,11 @@ def set_alt_usd_pair(self): self._symbol = join_pair(self._pair) - def get_center_price(self, type): if type == "USDT": self.set_alt_usd_pair() - print(symbol) return self._get_center_price() - def _get_center_price(self): symbol = self._symbol @@ -106,24 +103,7 @@ def _get_center_price(self): if __name__ == '__main__': center_price = None exchanges = ['gecko', 'bitfinex', 'kraken', 'waves', 'gdax', 'binance'] - - # exchanges = ['bitfinex'] symbol = 'BTC/USDT' - - """ - pf = PriceFeed('bitfinex', symbol) - pf.filter_symbols() - - center_price = get_ccxt_price(symbol, 'bitfinex') - print(center_price) - - center_price = pf.get_center_price(None) - print("center price: ", center_price) - - center_price = pf.get_center_price("USDT") - print("usdt center price: ", center_price) - """ - for exchange in exchanges: symbol = 'BTC/USD' From a03c90ab0e08aeeeadffac402c4717e2adb561ec Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 15 Nov 2018 18:42:40 +0500 Subject: [PATCH 0995/1846] Remove excess debug messages for neutral mode --- dexbot/strategies/staggered_orders.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 71450f453..8c1e3fae9 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -799,14 +799,12 @@ def increase_order_sizes(self, asset, asset_balance, orders): # Maximize order up to max possible amount if we can closer_order_bound = new_amount - self.log.debug('order amount: {:.8f}, closer_order_bound: {:.8f}' - .format(order_amount, closer_order_bound)) - self.log.debug('diff: {:.8f}, half of increase: {:.8f}' - .format(closer_order_bound - order_amount, - order_amount * (math.sqrt(1 + self.increment) - 1) / 2)) - - if (order_amount * (1 + self.increment / 10) < closer_order_bound and - closer_order_bound - order_amount >= order_amount * (math.sqrt(1 + self.increment) - 1) / 2): + order_amount_normalized = order_amount * (1 + self.increment / 10) + if ((order_amount_normalized < further_order_bound and + further_order_bound - order_amount >= order_amount * (math.sqrt(1 + self.increment) - 1) / 2) \ + or + (order_amount_normalized < closer_order_bound and + closer_order_bound - order_amount >= order_amount * (math.sqrt(1 + self.increment) - 1) / 2)): new_order_amount = closer_order_bound From 357d73f967e71125ebceb00db4592a0e642b7fa9 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 15 Nov 2018 18:40:27 +0500 Subject: [PATCH 0996/1846] Improve increase logic when sides are imbalanced Imagine we have the following buy orders: [100 100 100 10 10 10] Current increase logic: [100 100 100 10 10 30] [100 100 100 10 30 30] [100 100 100 30 30 30] New increase logic: [100 100 100 30 10 10] [100 100 100 30 30 10] [100 100 100 30 30 30] The concept for valley and neutral modes is "always increase as far as possible". --- dexbot/strategies/staggered_orders.py | 129 ++++++++++++++++++++++---- 1 file changed, 112 insertions(+), 17 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 8c1e3fae9..27a7bc321 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -703,7 +703,6 @@ def increase_order_sizes(self, asset, asset_balance, orders): elif (self.mode == 'valley' or (self.mode == 'buy_slope' and asset == 'base') or (self.mode == 'sell_slope' and asset == 'quote')): - """ Starting from the furthest order, for each order, see if it is approximately maximum size. If it is, move on to next. @@ -712,40 +711,86 @@ def increase_order_sizes(self, asset, asset_balance, orders): If furthest is reached, increase it to maximum size. Maximum size is (example for buy orders): - 1. As many "base" as the order below (closer_order_bound) + 1. As many "base" as the further order (further_order_bound) + 2. As many "base" as the order closer to center (closer_order_bound) """ orders_count = len(orders) orders = list(reversed(orders)) + closest_order = orders[-1] + closest_order_bound = closest_order['base']['amount'] * (1 + self.increment) + for order in orders: order_index = orders.index(order) order_amount = order['base']['amount'] + if order_index == 0: + # This is a furthest order + further_order_bound = order['base']['amount'] + furthest_order_bound = order['base']['amount'] + else: + # Not a furthest order + further_order = orders[order_index - 1] + further_order_bound = further_order['base']['amount'] + if order_index + 1 < orders_count: # Closer order is an order which one-step closer to the center closer_order = orders[order_index + 1] closer_order_bound = closer_order['base']['amount'] + is_closest_order = False else: """ Special processing for the closest order. Calculate new order amount based on orders count, but do not allow to perform too small increase rounds. New lowest buy / highest sell should be higher by at least one increment. """ - closer_order_bound = order_amount * (1 + self.increment) + is_closest_order = True + closer_order_bound = closest_order_bound new_amount = (total_balance / orders_count) / (1 + self.increment / 100) - if new_amount > closer_order_bound: + if furthest_order_bound < new_amount > closer_order_bound: # Maximize order up to max possible amount if we can closer_order_bound = new_amount - """ Check whether order amount is less than closer order and the diff is more than 50% of one increment - Note: we can use only 50% or less diffs. Bigger will not work. For example, with diff 80% an order - may have an actual difference like 30% from closer and 70% from further. - """ - if (order_amount * (1 + self.increment / 10) < closer_order_bound and - closer_order_bound - order_amount >= order_amount * self.increment / 2): + order_amount_normalized = order_amount * (1 + self.increment / 10) + need_increase = False + + if (order_amount_normalized < further_order_bound and + further_order_bound - order_amount >= order_amount * self.increment / 2 and + order_amount_normalized < closest_order_bound): + """ Check whether order amount is less than further order and also less than `closest order + + increment`. We need this check to be able to increase closer orders more smoothly. Here is the + example: + + [100 100 100 10 10 10] -- starting point, buy orders, result of imbalanced sides + [100 100 100 12 10 10] + [100 100 100 12 12 10] + [100 100 100 12 12 12] + + Note: This check is taking precedence because we need to begin new increase round only after all + orders will be max-sized. + """ + need_increase = True + + if is_closest_order: + new_order_amount = closer_order_bound + else: + # Do not allow to increase more than further order amount + new_order_amount = min(closer_order_bound * (1 + self.increment), further_order_bound) + + if new_order_amount < order_amount_normalized: + # Skip order if new amount is less than current for any reason + need_increase = False + elif (order_amount_normalized < closer_order_bound and + closer_order_bound - order_amount >= order_amount * self.increment / 2): + """ Check whether order amount is less than closer or order and the diff is more than 50% of one + increment. Note: we can use only 50% or less diffs. Bigger will not work. For example, with diff 80% + an order may have an actual difference like 30% from closer and 70% from further. + """ new_order_amount = closer_order_bound + need_increase = True + if need_increase: # Limit order to available balance if asset_balance < new_order_amount - order_amount: new_order_amount = order_amount + asset_balance['amount'] @@ -774,40 +819,90 @@ def increase_order_sizes(self, asset, asset_balance, orders): return elif self.mode == 'neutral': + """ Starting from the furthest order, for each order, see if it is approximately + maximum size. + If it is, move on to next. + If not, cancel it and replace with maximum size order. Maximum order size will be a + size of closer-to-center order. Then return. + If furthest is reached, increase it to maximum size. + + Maximum size is (example for buy orders): + 1. As many "base * sqrt(1 + increment)" as the further order (further_order_bound) + 2. As many "base / sqrt(1 + increment)" as the order closer to center (closer_order_bound) + """ + orders_count = len(orders) orders = list(reversed(orders)) + closest_order = orders[-1] + closest_order_bound = closest_order['base']['amount'] * math.sqrt(1 + self.increment) + for order in orders: order_index = orders.index(order) order_amount = order['base']['amount'] + if order_index == 0: + # This is a furthest order + further_order_bound = order['base']['amount'] + furthest_order_bound = order['base']['amount'] + else: + # Not a furthest order + further_order = orders[order_index - 1] + further_order_bound = further_order['base']['amount'] * math.sqrt(1 + self.increment) + if order_index + 1 < orders_count: # Closer order is an order which one-step closer to the center closer_order = orders[order_index + 1] closer_order_bound = closer_order['base']['amount'] / math.sqrt(1 + self.increment) + is_closest_order = False else: - closer_order_bound = order_amount * math.sqrt(1 + self.increment) + is_closest_order = True + closer_order_bound = closest_order_bound new_orders_sum = 0 amount = order_amount for o in orders: new_orders_sum += amount amount = amount / math.sqrt(1 + self.increment) + virtual_furthest_order_bound = amount * (total_balance / new_orders_sum) new_amount = order_amount * (total_balance / new_orders_sum) - if new_amount > closer_order_bound: + if new_amount > closer_order_bound and virtual_furthest_order_bound > furthest_order_bound: # Maximize order up to max possible amount if we can closer_order_bound = new_amount + need_increase = False order_amount_normalized = order_amount * (1 + self.increment / 10) - if ((order_amount_normalized < further_order_bound and - further_order_bound - order_amount >= order_amount * (math.sqrt(1 + self.increment) - 1) / 2) \ - or - (order_amount_normalized < closer_order_bound and - closer_order_bound - order_amount >= order_amount * (math.sqrt(1 + self.increment) - 1) / 2)): + + if (order_amount_normalized < further_order_bound and + further_order_bound - order_amount >= order_amount * (math.sqrt(1 + self.increment) - 1) / 2): + # Order is less than further order and diff is more than `increment / 2` + + if is_closest_order: + new_order_amount = closer_order_bound + need_increase = True + else: + price = closest_order['price'] + amount = closest_order['base']['amount'] + while price > order['price'] * (1 + self.increment / 10): + # Calculate closer order amount based on current closest order + previous_amount = amount + price = price / (1 + self.increment) + amount = amount / math.sqrt(1 + self.increment) + if order_amount_normalized < previous_amount: + # Current order is less than virtually calculated next order + # Do not allow to increase more than further order amount + new_order_amount = min(closer_order['base']['amount'], further_order_bound) + need_increase = True + + elif (order_amount_normalized < closer_order_bound and + closer_order_bound - order_amount >= order_amount * (math.sqrt(1 + self.increment) - 1) / 2): + # Order is less than closer order and diff is more than `increment / 2` new_order_amount = closer_order_bound + need_increase = True + if need_increase: # Limit order to available balance if asset_balance < new_order_amount - order_amount: new_order_amount = order_amount + asset_balance['amount'] From 202ae4ce3f0934f30e0f0a228859e28443c6d815 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 16 Nov 2018 17:19:54 +0500 Subject: [PATCH 0997/1846] Enhance size of order increment in neutral mode To avoid excessive orders replacements increase orders by `1 + increment` steps as in other modes. --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 27a7bc321..6592fe209 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -857,7 +857,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): is_closest_order = False else: is_closest_order = True - closer_order_bound = closest_order_bound + closer_order_bound = order['base']['amount'] * (1 + self.increment) new_orders_sum = 0 amount = order_amount @@ -892,7 +892,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): if order_amount_normalized < previous_amount: # Current order is less than virtually calculated next order # Do not allow to increase more than further order amount - new_order_amount = min(closer_order['base']['amount'], further_order_bound) + new_order_amount = min(order['base']['amount'] * (1 + self.increment), further_order_bound) need_increase = True elif (order_amount_normalized < closer_order_bound and From 499cb41ebd1e6586c59e90223dd033be005668cd Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 20 Nov 2018 13:11:11 -0800 Subject: [PATCH 0998/1846] remove todo - in cli --- dexbot/strategies/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 9a7100bf8..ee5bd7026 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -141,7 +141,7 @@ def configure(cls, return_base_config=True): exchanges = [ ('none', 'None. Use Manual or Bitshares DEX Price (default)'), ('gecko', 'Coingecko'), - ('waves', 'Waves DEX (todo)'), + ('waves', 'Waves DEX'), ('kraken', 'Kraken'), ('bitfinex', 'Bitfinex'), ('gdax', 'Gdax'), From 0397e90de0e87657225eb00c40a7e982a3ac0799 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 19 Nov 2018 01:02:17 +0500 Subject: [PATCH 0999/1846] Fix orders limiting in mountain mode Previous limiting was wrong, turning mountain into buy slope when sides are imbalanced. --- dexbot/strategies/staggered_orders.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 6592fe209..042e839fe 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -490,8 +490,12 @@ def allocate_asset(self, asset, asset_balance): self.place_closer_order(asset, closest_own_order) else: # Place order limited by size of the opposite-side order - if (self.mode == 'mountain' or - (self.mode == 'buy_slope' and asset == 'base') or + if self.mode == 'mountain': + opposite_asset_limit = closest_opposite_order['base']['amount'] * (1 + self.increment) + own_asset_limit = None + self.log.debug('Limiting {} order by opposite order: {} {}'.format( + order_type, opposite_asset_limit, opposite_symbol)) + elif ((self.mode == 'buy_slope' and asset == 'base') or (self.mode == 'sell_slope' and asset == 'quote')): opposite_asset_limit = None own_asset_limit = closest_opposite_order['quote']['amount'] From 7fcf04924953ef0ecce157bc9fa6c8781734c1c5 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 19 Nov 2018 15:37:34 +0500 Subject: [PATCH 1000/1846] Restore reservation code when part-filled on opposite side In dd12ef1a4a77b2030e29808d74b97e9c2b201b56 we completely turned off orders increases for situation when partially filled order detected on opposite side. This is not very-well suited for ocassionally-running workers. This is also helps to prevent excess spread tradings on huge price drifts. In new version additional_reserve factor is added to reserve some more funds than currently needed, this would be useful for valley-like modes. --- dexbot/strategies/staggered_orders.py | 49 ++++++++++++++++----------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 042e839fe..736b19254 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -523,26 +523,37 @@ def allocate_asset(self, asset, asset_balance): elif not opposite_orders: # Do not try to do anything than placing higher buy whether there is no sell orders return - elif not self.check_partial_fill(closest_opposite_order, fill_threshold=0): - """ Partially filled order on the opposite side, wait until closest order will be fully - fillled. Previously we did reservation of funds for next order and allocated remainder, but - this approach is not well-suited for valley mode, causing liquidity decrease around center. - """ - self.log.debug('Partially filled order on opposite side, reserving funds until fully filled') - return - elif ((asset == 'base' and furthest_own_order_price / - (1 + self.increment) < self.lower_bound) or - (asset == 'quote' and furthest_own_order_price * - (1 + self.increment) > self.upper_bound)): - # Lower/upper bound has been reached and now will start allocating rest of the balance. - self.bootstrapping = False - self.log.debug('Increasing sizes of {} orders'.format(order_type)) - self.increase_order_sizes(asset, asset_balance, own_orders) else: - # Range bound is not reached, we need to add additional orders at the extremes - self.bootstrapping = False - self.log.debug('Placing further order than current furthest {} order'.format(order_type)) - self.place_further_order(asset, furthest_own_order, allow_partial=True) + if not self.check_partial_fill(closest_opposite_order, fill_threshold=0): + """ Detect partially filled order on the opposite side and reserve appropriate amount to place + closer order. We adding some additional reserve to be able to place next order whether + new allocation round will be started, this is mostly for valley-like modes. + """ + funds_to_reserve = 0 + additional_reserve = 1 + self.increment * 1.1 + closer_own_order = self.place_closer_order(asset, closest_own_order, place_order=False) + + if asset == 'base': + funds_to_reserve = closer_own_order['amount'] * closer_own_order['price'] * additional_reserve + elif asset == 'quote': + funds_to_reserve = closer_own_order['amount'] * additional_reserve + self.log.debug('Partially filled order on opposite side, reserving funds for next {} order: ' + '{:.8f} {}'.format(order_type, funds_to_reserve, own_symbol)) + asset_balance -= funds_to_reserve + if asset_balance > own_threshold: + if ((asset == 'base' and furthest_own_order_price / + (1 + self.increment) < self.lower_bound) or + (asset == 'quote' and furthest_own_order_price * + (1 + self.increment) > self.upper_bound)): + # Lower/upper bound has been reached and now will start allocating rest of the balance. + self.bootstrapping = False + self.log.debug('Increasing sizes of {} orders'.format(order_type)) + self.increase_order_sizes(asset, asset_balance, own_orders) + else: + # Range bound is not reached, we need to add additional orders at the extremes + self.bootstrapping = False + self.log.debug('Placing further order than current furthest {} order'.format(order_type)) + self.place_further_order(asset, furthest_own_order, allow_partial=True) else: # Place first buy order as close to the lower bound as possible self.bootstrapping = True From 488425cbf4624d8c68aa2db7fbbd2d4058c1e692 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 19 Nov 2018 16:29:57 +0500 Subject: [PATCH 1001/1846] Handle exception when broadcasting a trx There is a race condition possible when partially filled order was further filled before new order actually replaced them. We can just ignore such case and continue to work. --- dexbot/strategies/staggered_orders.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 736b19254..3d77f90f8 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1,4 +1,7 @@ +import sys import math +import traceback +import bitsharesapi.exceptions from datetime import datetime, timedelta from bitshares.dex import Dex @@ -214,7 +217,16 @@ def maintain_strategy(self, *args, **kwargs): # Send pending operations if not self.bitshares.txbuffer.is_empty(): - self.execute() + try: + self.execute() + except bitsharesapi.exceptions.RPCError: + """ Handle exception without stopping the worker. The goal is to handle race condition when partially + filled order was further filled before we actually replaced them. + """ + self.log.warning('Got exception during broadcasting trx:') + traceback.print_exc(file=sys.stdout) + self.log.warning('Ignoring that exception and continue') + return self.bitshares.bundle = False # Maintain the history of free balances after maintenance runs. From 56413c9a2df71ec1b78a7651176a109df4f4da41 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 19 Nov 2018 16:46:34 +0500 Subject: [PATCH 1002/1846] Add info messages when placing close/further orders --- dexbot/strategies/staggered_orders.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 3d77f90f8..61f4a56bf 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1107,8 +1107,10 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False quote_amount = balance if place_order and asset == 'base': + self.log.info('Placing closer buy order') self.place_market_buy_order(quote_amount, price) elif place_order and asset == 'quote': + self.log.info('Placing closer sell order') self.place_market_sell_order(quote_amount, price) return {"amount": quote_amount, "price": price} @@ -1182,8 +1184,10 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals quote_amount = balance if place_order and asset == 'base': + self.log.info('Placing further buy order') self.place_market_buy_order(quote_amount, price) elif place_order and asset == 'quote': + self.log.info('Placing further sell order') self.place_market_sell_order(quote_amount, price) return {"amount": quote_amount, "price": price} From 75e8874c53868fa5fc5d9f55316a9f858abc5275 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 19 Nov 2018 17:21:32 +0500 Subject: [PATCH 1003/1846] Fix typo --- 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 61f4a56bf..c55d4f0de 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -784,7 +784,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): if (order_amount_normalized < further_order_bound and further_order_bound - order_amount >= order_amount * self.increment / 2 and order_amount_normalized < closest_order_bound): - """ Check whether order amount is less than further order and also less than `closest order + + """ Check whether order amount is less than further order and also less than `closer order + increment`. We need this check to be able to increase closer orders more smoothly. Here is the example: From 3ec74641501fa80fa6750aae4f150725f656e79a Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 19 Nov 2018 17:21:53 +0500 Subject: [PATCH 1004/1846] Speed up increases in valley mode When increasing orders with imbalanced sides, use at least 15% increment steps. --- dexbot/strategies/staggered_orders.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c55d4f0de..acefc6d6d 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -125,6 +125,7 @@ def __init__(self, *args, **kwargs): self.base_balance = None self.quote_asset_threshold = 0 self.base_asset_threshold = 0 + self.min_increase_factor = 1.15 # Initial balance history elements should not be equal to avoid immediate bootstrap turn off self.quote_balance_history = [1, 2, 3] self.base_balance_history = [1, 2, 3] @@ -542,7 +543,7 @@ def allocate_asset(self, asset, asset_balance): new allocation round will be started, this is mostly for valley-like modes. """ funds_to_reserve = 0 - additional_reserve = 1 + self.increment * 1.1 + additional_reserve = max(1 + self.increment, self.min_increase_factor) * 1.05 closer_own_order = self.place_closer_order(asset, closest_own_order, place_order=False) if asset == 'base': @@ -798,11 +799,10 @@ def increase_order_sizes(self, asset, asset_balance, orders): """ need_increase = True - if is_closest_order: - new_order_amount = closer_order_bound - else: - # Do not allow to increase more than further order amount - new_order_amount = min(closer_order_bound * (1 + self.increment), further_order_bound) + # To speed up the process, use at least N% increases + increase_factor = max(1 + self.increment, self.min_increase_factor) + # Do not allow to increase more than further order amount + new_order_amount = min(closer_order_bound * increase_factor, further_order_bound) if new_order_amount < order_amount_normalized: # Skip order if new amount is less than current for any reason From 46d5b6089890a252ed7d0ff89d25c067d46fe3fb Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 20 Nov 2018 23:26:38 +0500 Subject: [PATCH 1005/1846] Update comment --- 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 acefc6d6d..17b1600d5 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -534,7 +534,7 @@ def allocate_asset(self, asset, asset_balance): self.replace_partially_filled_order(closest_own_order) return elif not opposite_orders: - # Do not try to do anything than placing higher buy whether there is no sell orders + # Do not try to do anything than placing closer order whether there is no opposite orders return else: if not self.check_partial_fill(closest_opposite_order, fill_threshold=0): From dec67c2325c604dff95eb3780422552cae7733b5 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 21 Nov 2018 00:21:19 +0500 Subject: [PATCH 1006/1846] Refactor partially filled orders replacements Instead of dumb-replace closest partially filled order, check whether opposite-side order is also partially filled. This condition is clearly indicates a spread trade. If that condition is not met, just allocate funds to increase orders sizes as usual. When increasing orders sizes, do not try to dumb-replace partially filled orders too. Instead, check available funds and currently remaining order amount. Put increased order only if we have enough balance. Otherwise, just keep it. --- dexbot/strategies/staggered_orders.py | 52 ++++++++++++++------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 17b1600d5..73ff41b9a 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -529,8 +529,13 @@ def allocate_asset(self, asset, asset_balance): order_type, opposite_asset_limit, opposite_symbol)) self.place_closer_order(asset, closest_own_order, own_asset_limit=own_asset_limit, opposite_asset_limit=opposite_asset_limit, allow_partial=False) - elif not self.check_partial_fill(closest_own_order): - # Replace closest own order if `fill % > default threshold` + elif not self.check_partial_fill(closest_own_order) and not self.check_partial_fill(closest_opposite_order): + """ Replace closest own order if `fill % > default threshold` and only when opposite order is also + partially filled. This would prevent an abuse case when we are operationg on inactive market. An + attacker can massively dump the price and then he can buy back the asset cheaper. Only applicable + when sides are massively imbalanced. Though in the normal market a similar condition may happen + naturally on significant price drops. This check helps to redistribute funds more smoothly. + """ self.replace_partially_filled_order(closest_own_order) return elif not opposite_orders: @@ -615,12 +620,6 @@ def increase_order_sizes(self, asset, asset_balance, orders): symbol = '' precision = 0 - # First of all, make sure all orders are not partially filled - for order in orders: - if not self.check_partial_fill(order, fill_threshold=0): - self.replace_partially_filled_order(order) - return - if asset == 'quote': total_balance = self.quote_total_balance order_type = 'sell' @@ -702,11 +701,6 @@ def increase_order_sizes(self, asset, asset_balance, orders): """ new_order_amount = closer_bound / (1 + self.increment * 0.2) - # Limit order to available balance - if asset_balance < new_order_amount - order_amount: - new_order_amount = order_amount + asset_balance['amount'] - self.log.debug('Limiting new {} order to avail balance: {:.8f} {}' - .format(order_type, new_order_amount, symbol)) quote_amount = 0 price = 0 @@ -717,6 +711,12 @@ def increase_order_sizes(self, asset, asset_balance, orders): price = order['price'] quote_amount = new_order_amount / price + if asset_balance < new_order_amount - order['for_sale']['amount']: + # Balance should be enough to replace partially filled order + self.log.debug('Not enough balance to increase {} order at price {:.8f}' + .format(order_type, price)) + return + self.log.info('Increasing {} order at price {:.8f} from {:.{prec}f} to {:.{prec}f} {}' .format(order_type, price, order_amount, new_order_amount, symbol, prec=precision)) self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}, amount: {}, price: {:.8f}' @@ -818,12 +818,6 @@ def increase_order_sizes(self, asset, asset_balance, orders): need_increase = True if need_increase: - # Limit order to available balance - if asset_balance < new_order_amount - order_amount: - new_order_amount = order_amount + asset_balance['amount'] - self.log.debug('Limiting new order to avail asset balance: {:.8f} {}' - .format(new_order_amount, symbol)) - price = 0 quote_amount = 0 @@ -833,6 +827,13 @@ def increase_order_sizes(self, asset, asset_balance, orders): elif asset == 'base': price = order['price'] quote_amount = new_order_amount / price + + if asset_balance < new_order_amount - order['for_sale']['amount']: + # Balance should be enough to replace partially filled order + self.log.debug('Not enough balance to increase {} order at price {:.8f}' + .format(order_type, price)) + return + self.log.info('Increasing {} order at price {:.8f} from {:.{prec}f} to {:.{prec}f} {}' .format(order_type, price, order_amount, new_order_amount, symbol, prec=precision)) self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}, amount: {}, price: {:.8f}' @@ -930,12 +931,6 @@ def increase_order_sizes(self, asset, asset_balance, orders): need_increase = True if need_increase: - # Limit order to available balance - if asset_balance < new_order_amount - order_amount: - new_order_amount = order_amount + asset_balance['amount'] - self.log.debug('Limiting new order to avail asset balance: {:.8f} {}' - .format(new_order_amount, symbol)) - price = 0 if asset == 'quote': @@ -944,6 +939,13 @@ def increase_order_sizes(self, asset, asset_balance, orders): elif asset == 'base': price = order['price'] quote_amount = new_order_amount / price + + if asset_balance < new_order_amount - order['for_sale']['amount']: + # Balance should be enough to replace partially filled order + self.log.debug('Not enough balance to increase {} order at price {:.8f}' + .format(order_type, price)) + return + self.log.info('Increasing {} order at price {:.8f} from {:.{prec}f} to {:.{prec}f} {}' .format(order_type, price, order_amount, new_order_amount, symbol, prec=precision)) self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}' From a2105b2320bb0027f758e8a277a155f8e486caf5 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 22 Nov 2018 08:39:20 +0200 Subject: [PATCH 1007/1846] Fix minor PEP8 warnings --- dexbot/strategies/staggered_orders.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 73ff41b9a..3bec46f82 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -560,7 +560,7 @@ def allocate_asset(self, asset, asset_balance): asset_balance -= funds_to_reserve if asset_balance > own_threshold: if ((asset == 'base' and furthest_own_order_price / - (1 + self.increment) < self.lower_bound) or + (1 + self.increment) < self.lower_bound) or (asset == 'quote' and furthest_own_order_price * (1 + self.increment) > self.upper_bound)): # Lower/upper bound has been reached and now will start allocating rest of the balance. @@ -811,8 +811,8 @@ def increase_order_sizes(self, asset, asset_balance, orders): elif (order_amount_normalized < closer_order_bound and closer_order_bound - order_amount >= order_amount * self.increment / 2): """ Check whether order amount is less than closer or order and the diff is more than 50% of one - increment. Note: we can use only 50% or less diffs. Bigger will not work. For example, with diff 80% - an order may have an actual difference like 30% from closer and 70% from further. + increment. Note: we can use only 50% or less diffs. Bigger will not work. For example, with + diff 80% an order may have an actual difference like 30% from closer and 70% from further. """ new_order_amount = closer_order_bound need_increase = True From 7b921ba70d975eac1db3a28dfeb0073da40f54e7 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 22 Nov 2018 10:23:31 +0200 Subject: [PATCH 1008/1846] Change dexbot version number to 0.7.24 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 0250373d0..e86f81695 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.23' +VERSION = '0.7.24' AUTHOR = 'Codaone Oy' __version__ = VERSION From 208ac5a44177583da40cc67274318fe19713c2fe Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 22 Nov 2018 14:50:15 -0800 Subject: [PATCH 1009/1846] add comment for external feeds to cli --- 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 5fd80170b..cced0e1b2 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -44,7 +44,7 @@ def configure(cls, return_base_config=True): ConfigElement('partial_fill_threshold', 'float', 30, 'Fill threshold', 'Order fill threshold to reset orders', (0, 100, 2, '%')), ConfigElement('reset_on_price_change', 'bool', False, 'Reset orders on center price change', - 'Reset orders when center price is changed more than threshold', None), + 'Reset orders when center price is changed more than threshold (set False for external feeds)', None), ConfigElement('price_change_threshold', 'float', 2, 'Price change threshold', 'Define center price threshold to react on', (0, 100, 2, '%')), ConfigElement('custom_expiration', 'bool', False, 'Custom expiration', From 51dc2db78e463690a0ca2dd90f628edf60d15e4c Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 22 Nov 2018 17:08:12 -0800 Subject: [PATCH 1010/1846] move gecko tests --- .../strategies/external_feeds/gecko_feed.py | 33 +--------------- .../strategies/external_feeds/price_feed.py | 12 ++---- .../external_feeds/tests/__init__.py | 0 .../external_feeds/tests/gecko_test.py | 38 +++++++++++++++++++ .../external_feeds/{ => tests}/styles.py | 0 5 files changed, 43 insertions(+), 40 deletions(-) create mode 100644 dexbot/strategies/external_feeds/tests/__init__.py create mode 100644 dexbot/strategies/external_feeds/tests/gecko_test.py rename dexbot/strategies/external_feeds/{ => tests}/styles.py (100%) diff --git a/dexbot/strategies/external_feeds/gecko_feed.py b/dexbot/strategies/external_feeds/gecko_feed.py index f147b61f0..57c9aab72 100644 --- a/dexbot/strategies/external_feeds/gecko_feed.py +++ b/dexbot/strategies/external_feeds/gecko_feed.py @@ -1,8 +1,5 @@ -import click import requests - -from dexbot.strategies.external_feeds.styles import yellow -from dexbot.strategies.external_feeds.process_pair import split_pair, filter_prefix_symbol, filter_bit_symbol, debug +from dexbot.strategies.external_feeds.process_pair import split_pair, debug """ To use Gecko API, note that gecko does not provide pairs by default. @@ -13,11 +10,6 @@ GECKO_COINS_URL = 'https://api.coingecko.com/api/v3/coins/' -def print_usage(): - print("Usage: python3 gecko_feed.py", yellow('[symbol]'), - "Symbol is required, for example:", yellow('BTC/USD'), sep='') - - def get_json(url): r = requests.get(url) json_obj = r.json() @@ -82,26 +74,3 @@ def get_gecko_price(**kwargs): price = get_gecko_price_by_pair(pair) return price - - -# Unit tests -# Todo: Move tests to own files -@click.group() -def main(): - pass - - -@main.command() -@click.argument('symbol') -def test_feed(symbol): - """ - [symbol] Symbol example: btc/usd or btc:usd - """ - price = get_gecko_price(symbol_=symbol) - print(price) - - pair = split_pair(symbol) - price = get_gecko_price(pair_=pair) - -if __name__ == '__main__': - main() diff --git a/dexbot/strategies/external_feeds/price_feed.py b/dexbot/strategies/external_feeds/price_feed.py index 0c38ea90d..b281b425a 100755 --- a/dexbot/strategies/external_feeds/price_feed.py +++ b/dexbot/strategies/external_feeds/price_feed.py @@ -5,19 +5,15 @@ import re """ -Note from Marko Paasila: -We have been calling the unit-of-measure BASE and the asset-of-interest QUOTE. -We chose the way @xeroc had in python-bitshares, where the market is BTSUSD or BTS:USD or BTS/USD, -and price is USD/BTS. This is opposite to how bitshares-ui shows it (or I'm not sure of that, -but at least unit-of-measure is QUOTE and not BASE there). - -So in DEXBot: +Note from Marko Paasila, In DEXBot: unit of measure = BASE asset of interest = QUOTE """ class PriceFeed: - + """ + price feed class to handle price feed + """ def __init__(self, exchange, symbol): self._alt_exchanges = ['gecko', 'waves'] # assume all other exchanges are ccxt self._exchange= exchange diff --git a/dexbot/strategies/external_feeds/tests/__init__.py b/dexbot/strategies/external_feeds/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dexbot/strategies/external_feeds/tests/gecko_test.py b/dexbot/strategies/external_feeds/tests/gecko_test.py new file mode 100644 index 000000000..4a8c4376a --- /dev/null +++ b/dexbot/strategies/external_feeds/tests/gecko_test.py @@ -0,0 +1,38 @@ +import click +from dexbot.strategies.external_feeds.tests.styles import yellow +from dexbot.strategies.external_feeds.gecko_feed import get_gecko_price +from dexbot.strategies.external_feeds.process_pair import split_pair + + +def print_usage(): + print("Usage: python3 gecko_feed.py", yellow('[symbol]'), + "Symbol is required, for example:", yellow('BTC/USD'), sep='') + + +# Unit tests +@click.group() +def main(): + pass + + +@main.command() +@click.argument('symbol') +def test_feed(symbol): + """ + [symbol] Symbol example: btc/usd or btc:usd + """ + try: + price = get_gecko_price(symbol_=symbol) + print(price) + + pair = split_pair(symbol) + price = get_gecko_price(pair_=pair) + + except Exception as e: + print_usage() + print(type(e).__name__, e.args, str(e)) + + +if __name__ == '__main__': + main() + diff --git a/dexbot/strategies/external_feeds/styles.py b/dexbot/strategies/external_feeds/tests/styles.py similarity index 100% rename from dexbot/strategies/external_feeds/styles.py rename to dexbot/strategies/external_feeds/tests/styles.py From 8d31936d5332dae76448f9898e14fb40075f4ebc Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 22 Nov 2018 21:56:16 -0800 Subject: [PATCH 1011/1846] moved test methods to test dir --- dexbot/strategies/external_feeds/ccxt_feed.py | 22 --------- .../strategies/external_feeds/price_feed.py | 2 +- .../external_feeds/tests/ccxt_test.py | 26 +++++++++++ .../external_feeds/tests/waves_test.py | 45 +++++++------------ .../strategies/external_feeds/waves_feed.py | 15 ------- 5 files changed, 42 insertions(+), 68 deletions(-) create mode 100644 dexbot/strategies/external_feeds/tests/ccxt_test.py diff --git a/dexbot/strategies/external_feeds/ccxt_feed.py b/dexbot/strategies/external_feeds/ccxt_feed.py index b37e8f2d7..52884becf 100644 --- a/dexbot/strategies/external_feeds/ccxt_feed.py +++ b/dexbot/strategies/external_feeds/ccxt_feed.py @@ -1,7 +1,5 @@ -import click import json import asyncio -import functools import ccxt.async_support as accxt from pprint import pprint @@ -44,23 +42,3 @@ def get_ccxt_price(symbol, exchange_name): return center_price -if __name__ == '__main__': - # result = asyncio.get_event_loop().run_until_complete(print_ticker(symbol, 'bitfinex')) - exchanges =['bitfinex', 'kraken', 'binance', 'gdax'] - symbol = 'BTC/USDT' - - # testing get all pairs. - for exchange_id in exchanges: - print("\n\n\n") - print(exchange_id) - result = asyncio.get_event_loop().run_until_complete(get_ccxt_load_markets(exchange_id)) - all_symbols= [obj for obj in result] - print(all_symbols) - - # testing get center price - # center_price = [asyncio.get_event_loop().run_until_complete(get_ccxt_price(symbol, e)) for e in exchanges] - - center_price = [get_ccxt_price(symbol, e) for e in exchanges] - print(' exchange: ', exchanges, ' symbol: ', symbol, sep=':') - print(' center_price: ', center_price) - diff --git a/dexbot/strategies/external_feeds/price_feed.py b/dexbot/strategies/external_feeds/price_feed.py index b281b425a..1f2f31a1b 100755 --- a/dexbot/strategies/external_feeds/price_feed.py +++ b/dexbot/strategies/external_feeds/price_feed.py @@ -98,7 +98,7 @@ def _get_center_price(self): if __name__ == '__main__': center_price = None - exchanges = ['gecko', 'bitfinex', 'kraken', 'waves', 'gdax', 'binance'] + exchanges = ['gecko', 'bitfinex', 'kraken', 'gdax', 'binance', 'waves'] symbol = 'BTC/USDT' for exchange in exchanges: diff --git a/dexbot/strategies/external_feeds/tests/ccxt_test.py b/dexbot/strategies/external_feeds/tests/ccxt_test.py new file mode 100644 index 000000000..4c4e10281 --- /dev/null +++ b/dexbot/strategies/external_feeds/tests/ccxt_test.py @@ -0,0 +1,26 @@ +import click +import asyncio +from dexbot.strategies.external_feeds.ccxt_feed import get_ccxt_load_markets, get_ccxt_price + + +if __name__ == '__main__': + # result = asyncio.get_event_loop().run_until_complete(print_ticker(symbol, 'bitfinex')) + exchanges =['bitfinex', 'kraken', 'binance', 'gdax'] + symbol = 'BTC/USDT' + + # testing get all pairs. + for exchange_id in exchanges: + print("\n\n\n") + print(exchange_id) + result = asyncio.get_event_loop().run_until_complete(get_ccxt_load_markets(exchange_id)) + all_symbols= [obj for obj in result] + print(all_symbols) + + # testing get center price + # center_price = [asyncio.get_event_loop().run_until_complete(get_ccxt_price(symbol, e)) for e in exchanges] + + center_price = [get_ccxt_price(symbol, e) for e in exchanges] + print(' exchange: ', exchanges, ' symbol: ', symbol, sep=':') + print(' center_price: ', center_price) + + diff --git a/dexbot/strategies/external_feeds/tests/waves_test.py b/dexbot/strategies/external_feeds/tests/waves_test.py index 76c44dfbe..af47dcaa3 100644 --- a/dexbot/strategies/external_feeds/tests/waves_test.py +++ b/dexbot/strategies/external_feeds/tests/waves_test.py @@ -1,33 +1,18 @@ -import pywaves as pw +from dexbot.strategies.external_feeds.process_pair import split_pair, filter_prefix_symbol, filter_bit_symbol +from dexbot.strategies.external_feeds.waves_feed import get_waves_price -# GET /ticker/{AMOUNT_ASSET}/{PRICE_ASSET} -#ticker_url = https://marketdata.wavesplatform.com/api/ticker/BTC/USD +if __name__ == '__main__': -def get_asset(symbol, coin_list): - asset_id = None - try: - asset_id = [obj for obj in coin_list if obj['symbol'] == symbol][0]['assetID'] - except IndexError as e: - print(e) - return pw.Asset(asset_id) + symbol = 'BTC/USD' # quote/base for external exchanges + print(symbol, "=") + raw_pair = split_pair(symbol) + pair = [filter_bit_symbol(j) for j in [filter_prefix_symbol(i) for i in raw_pair]] + + pair_price = get_waves_price(pair_=pair) + if pair_price is not None: + print("pair price", pair_price, sep=":") -# set the asset pair -WAVES_BTC = pw.AssetPair(pw.WAVES, pw.BTC) - -# get last price and volume -print("%s %s" % (WAVES_BTC.last(), WAVES_BTC.volume())) - -# get ticker -ticker = WAVES_BTC.ticker() -print(ticker['24h_open']) -print(ticker['24h_vwap']) - -# get last 10 trades -trades = WAVES_BTC.trades(10) -for t in trades: - print("%s %s %s %s" % (t['buyer'], t['seller'], t['price'], t['amount'])) - -# get last 10 daily OHLCV candles -ohlcv = WAVES_BTC.candles(1440, 10) -for t in ohlcv: - print("%s %s %s %s %s" % (t['open'], t['high'], t['low'], t['close'], t['volume'])) + current_price = get_waves_price(symbol_=symbol) + if current_price is not None: + print("symbol price", current_price, sep=":") + diff --git a/dexbot/strategies/external_feeds/waves_feed.py b/dexbot/strategies/external_feeds/waves_feed.py index 9c1daa2c7..10ce9399b 100644 --- a/dexbot/strategies/external_feeds/waves_feed.py +++ b/dexbot/strategies/external_feeds/waves_feed.py @@ -1,5 +1,4 @@ import requests -from dexbot.strategies.external_feeds.process_pair import split_pair, filter_prefix_symbol, filter_bit_symbol, debug WAVES_URL = 'https://marketdata.wavesplatform.com/api/' SYMBOLS_URL = "/symbols" @@ -48,17 +47,3 @@ def get_waves_price(**kwargs): return price -if __name__ == '__main__': - - symbol = 'BTC/USD' # quote/base for external exchanges - print(symbol, "=") - raw_pair = split_pair(symbol) - pair = [filter_bit_symbol(j) for j in [filter_prefix_symbol(i) for i in raw_pair]] - - pair_price = get_waves_price(pair_=pair) - print("pair price", pair_price) - -# current_price = get_waves_price(pair[1], pair[0]) - current_price = get_waves_price(symbol_=symbol) - print("symbol price", current_price) - From 9f1e1dca854a68106b7ed58a8593c7194e6f1f75 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 22 Nov 2018 22:03:41 -0800 Subject: [PATCH 1012/1846] add pywaves test --- .../external_feeds/tests/pywaves_test.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 dexbot/strategies/external_feeds/tests/pywaves_test.py diff --git a/dexbot/strategies/external_feeds/tests/pywaves_test.py b/dexbot/strategies/external_feeds/tests/pywaves_test.py new file mode 100644 index 000000000..8b76d7a6b --- /dev/null +++ b/dexbot/strategies/external_feeds/tests/pywaves_test.py @@ -0,0 +1,30 @@ +import pywaves as pw + + + +if __name__ == '__main__': + + try: + # set the asset pair + WAVES_BTC = pw.AssetPair(pw.WAVES, pw.BTC) + + # get last price and volume + print(WAVES_BTC.last(), WAVES_BTC.volume(), sep=' ') + + # get ticker + ticker = WAVES_BTC.ticker() + print(ticker['24h_open']) + print(ticker['24h_vwap']) + + # get last 10 trades + trades = WAVES_BTC.trades(10) + for t in trades: + print(t['buyer'], t['seller'], t['price'], t['amount'], sep=' ') + + # get last 10 daily OHLCV candles + ohlcv = WAVES_BTC.candles(1440, 10) + for t in ohlcv: + print(t['open'], t['high'], t['low'], t['close'], t['volume'], sep=' ') + + except Exception as e: + print(type(e).__name__, e.args, 'Exchange Error (ignoring)') From 02acbd535f464cb486bdbb66b720503657139e5a Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 22 Nov 2018 22:04:29 -0800 Subject: [PATCH 1013/1846] update gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 34e70e6ea..7c4f705e7 100644 --- a/.gitignore +++ b/.gitignore @@ -77,3 +77,5 @@ venv/ .idea/ dexbot/views/ui/**/*_ui.py dexbot/resources/*_rc.py +archive +*~ \ No newline at end of file From 8c878b3b981b7ba2cecbb4526645121787a571e1 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 22 Nov 2018 22:12:24 -0800 Subject: [PATCH 1014/1846] move test methods --- .../strategies/external_feeds/price_feed.py | 21 +---------- .../strategies/external_feeds/process_pair.py | 36 ------------------ .../external_feeds/tests/price_feed_test.py | 24 ++++++++++++ .../external_feeds/tests/process_pair_test.py | 37 +++++++++++++++++++ 4 files changed, 62 insertions(+), 56 deletions(-) create mode 100644 dexbot/strategies/external_feeds/tests/price_feed_test.py create mode 100644 dexbot/strategies/external_feeds/tests/process_pair_test.py diff --git a/dexbot/strategies/external_feeds/price_feed.py b/dexbot/strategies/external_feeds/price_feed.py index 1f2f31a1b..5f4d62b7b 100755 --- a/dexbot/strategies/external_feeds/price_feed.py +++ b/dexbot/strategies/external_feeds/price_feed.py @@ -4,11 +4,7 @@ from dexbot.strategies.external_feeds.process_pair import split_pair, join_pair, filter_prefix_symbol, filter_bit_symbol, debug import re -""" -Note from Marko Paasila, In DEXBot: -unit of measure = BASE -asset of interest = QUOTE -""" + class PriceFeed: """ @@ -96,18 +92,3 @@ def _get_center_price(self): -if __name__ == '__main__': - center_price = None - exchanges = ['gecko', 'bitfinex', 'kraken', 'gdax', 'binance', 'waves'] - symbol = 'BTC/USDT' - - for exchange in exchanges: - symbol = 'BTC/USD' - pf = PriceFeed(exchange, symbol) - pf.filter_symbols() - center_price = pf.get_center_price(None) - print("center price: ", center_price) - if center_price is None: # try USDT - center_price = pf.get_center_price("USDT") - print("s/usd/usdt, center price: ", center_price) - diff --git a/dexbot/strategies/external_feeds/process_pair.py b/dexbot/strategies/external_feeds/process_pair.py index e2adaf6ea..da5dd2359 100644 --- a/dexbot/strategies/external_feeds/process_pair.py +++ b/dexbot/strategies/external_feeds/process_pair.py @@ -48,39 +48,3 @@ def get_consolidated_pair(base, quote): return pair1, pair2 -# Unit Tests -# Todo: Move tests to own files -def test_consolidated_pair(): - symbol = 'STEEM:BTS' # pair = 'STEEM:BTS' or STEEM/BTS' - pair = split_pair(symbol) - pair1, pair2 = get_consolidated_pair(pair[1], pair[0]) - print(symbol, '=', pair1, pair2, sep=' ') - - -def test_split_symbol(): - try: - group = ['BTC:USD', 'STEEM/USD'] - pair = [split_pair(symbol) for symbol in group] - print('original:', group, 'result:', pair, sep=' ') - except Exception as e: - pass - - -def test_filters(): - test_symbols = ['USDT', 'bridge.USD', 'Rudex.USD', 'open.USD', - 'GDEX.USD', 'Spark.USD', 'bridge.BTC', 'BTC', 'LTC', - 'bitUSD', 'bitEUR', 'bitHKD'] - print("Test Symbols", test_symbols, sep=":") - r = [filter_prefix_symbol(i) for i in test_symbols] - print("Filter prefix symbol", r, sep=":") - r2 = [filter_bit_symbol(i) for i in r] - print("Apply to result, Filter bit symbol", r2, sep=":") - - -if __name__ == '__main__': - print("testing consolidate pair") - test_consolidated_pair() - print("\ntesting split symbol") - test_split_symbol() - print("\ntesting filters") - test_filters() diff --git a/dexbot/strategies/external_feeds/tests/price_feed_test.py b/dexbot/strategies/external_feeds/tests/price_feed_test.py new file mode 100644 index 000000000..eb0eb117a --- /dev/null +++ b/dexbot/strategies/external_feeds/tests/price_feed_test.py @@ -0,0 +1,24 @@ +from dexbot.strategies.external_feeds.price_feed import PriceFeed + +""" +Note from Marko Paasila, In DEXBot: +unit of measure = BASE +asset of interest = QUOTE +""" + + +if __name__ == '__main__': + center_price = None + exchanges = ['gecko', 'bitfinex', 'kraken', 'gdax', 'binance', 'waves'] + symbol = 'BTC/USDT' + + for exchange in exchanges: + symbol = 'BTC/USD' + pf = PriceFeed(exchange, symbol) + pf.filter_symbols() + center_price = pf.get_center_price(None) + print("center price: ", center_price) + if center_price is None: # try USDT + center_price = pf.get_center_price("USDT") + print("s/usd/usdt, center price: ", center_price) + diff --git a/dexbot/strategies/external_feeds/tests/process_pair_test.py b/dexbot/strategies/external_feeds/tests/process_pair_test.py new file mode 100644 index 000000000..74b82c965 --- /dev/null +++ b/dexbot/strategies/external_feeds/tests/process_pair_test.py @@ -0,0 +1,37 @@ +from dexbot.strategies.external_feeds.process_pair import split_pair, get_consolidated_pair, filter_prefix_symbol, filter_bit_symbol + +# Unit Tests +def test_consolidated_pair(): + symbol = 'STEEM:BTS' # pair = 'STEEM:BTS' or STEEM/BTS' + pair = split_pair(symbol) + pair1, pair2 = get_consolidated_pair(pair[1], pair[0]) + print(symbol, '=', pair1, pair2, sep=' ') + + +def test_split_symbol(): + try: + group = ['BTC:USD', 'STEEM/USD'] + pair = [split_pair(symbol) for symbol in group] + print('original:', group, 'result:', pair, sep=' ') + except Exception as e: + pass + + +def test_filters(): + test_symbols = ['USDT', 'bridge.USD', 'Rudex.USD', 'open.USD', + 'GDEX.USD', 'Spark.USD', 'bridge.BTC', 'BTC', 'LTC', + 'bitUSD', 'bitEUR', 'bitHKD'] + print("Test Symbols", test_symbols, sep=":") + r = [filter_prefix_symbol(i) for i in test_symbols] + print("Filter prefix symbol", r, sep=":") + r2 = [filter_bit_symbol(i) for i in r] + print("Apply to result, Filter bit symbol", r2, sep=":") + + +if __name__ == '__main__': + print("testing consolidate pair") + test_consolidated_pair() + print("\ntesting split symbol") + test_split_symbol() + print("\ntesting filters") + test_filters() From 242ffcb56d81d6ec11891fa0f4bfa13af906d923 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 22 Nov 2018 22:15:39 -0800 Subject: [PATCH 1015/1846] remove comments --- dexbot/strategies/external_feeds/tests/ccxt_test.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/dexbot/strategies/external_feeds/tests/ccxt_test.py b/dexbot/strategies/external_feeds/tests/ccxt_test.py index 4c4e10281..94a2ca5ac 100644 --- a/dexbot/strategies/external_feeds/tests/ccxt_test.py +++ b/dexbot/strategies/external_feeds/tests/ccxt_test.py @@ -4,7 +4,6 @@ if __name__ == '__main__': - # result = asyncio.get_event_loop().run_until_complete(print_ticker(symbol, 'bitfinex')) exchanges =['bitfinex', 'kraken', 'binance', 'gdax'] symbol = 'BTC/USDT' @@ -17,8 +16,6 @@ print(all_symbols) # testing get center price - # center_price = [asyncio.get_event_loop().run_until_complete(get_ccxt_price(symbol, e)) for e in exchanges] - center_price = [get_ccxt_price(symbol, e) for e in exchanges] print(' exchange: ', exchanges, ' symbol: ', symbol, sep=':') print(' center_price: ', center_price) From 618a4327417c2be202e803c53098e7fce152aaf7 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 23 Nov 2018 01:12:53 +0500 Subject: [PATCH 1016/1846] Improve debug message on filled order --- dexbot/strategies/staggered_orders.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 3bec46f82..120f8b71c 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -972,12 +972,19 @@ def check_partial_fill(self, order, fill_threshold=None): if fill_threshold is None: fill_threshold = self.partial_fill_threshold + if self.is_buy_order(order): + order_type = 'buy' + price = order['price'] + else: + order_type = 'sell' + price = order['price'] ** -1 + if order['for_sale']['amount'] != order['base']['amount']: diff_abs = order['base']['amount'] - order['for_sale']['amount'] diff_rel = diff_abs / order['base']['amount'] if diff_rel > fill_threshold: - self.log.debug('Partially filled order: {} @ {:.8f}, filled: {:.2%}'.format( - order['base']['amount'], order['price'], diff_rel)) + self.log.debug('Partially filled {} order: {} {} @ {:.8f}, filled: {:.2%}'.format( + order_type, order['base']['amount'], order['base']['symbol'], price, diff_rel)) return False return True From ed83139a480c5196c82a426dc560577bb36eec77 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 23 Nov 2018 12:31:12 +0500 Subject: [PATCH 1017/1846] Adjust instant fill check in place_closer_order() Instant fill check in place_closer_order() should be performed only when place_order=True. We don't need this check if we are not actually placing an order but just want to obtain amount and price for the next order. --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 120f8b71c..ad4c6078b 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1052,12 +1052,12 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False # Check for instant fill if asset == 'base': price = order['price'] * (1 + self.increment) - if not self.is_instant_fill_enabled and price > float(self.ticker().get('lowestAsk')): + if not self.is_instant_fill_enabled and price > float(self.ticker().get('lowestAsk')) and place_order: self.log.info('Refusing to place an order which crosses lowest ask') return None elif asset == 'quote': price = (order['price'] ** -1) / (1 + self.increment) - if not self.is_instant_fill_enabled and price < float(self.ticker().get('highestBid')): + if not self.is_instant_fill_enabled and price < float(self.ticker().get('highestBid')) and place_order: self.log.info('Refusing to place an order which crosses highest bid') return None From 43ca83ad1fa39ada57744a3e4f43d45cffafa7b7 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 23 Nov 2018 12:37:22 +0500 Subject: [PATCH 1018/1846] Remove unused variables --- dexbot/strategies/staggered_orders.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index ad4c6078b..c756a9369 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -765,14 +765,12 @@ def increase_order_sizes(self, asset, asset_balance, orders): # Closer order is an order which one-step closer to the center closer_order = orders[order_index + 1] closer_order_bound = closer_order['base']['amount'] - is_closest_order = False else: """ Special processing for the closest order. Calculate new order amount based on orders count, but do not allow to perform too small increase rounds. New lowest buy / highest sell should be higher by at least one increment. """ - is_closest_order = True closer_order_bound = closest_order_bound new_amount = (total_balance / orders_count) / (1 + self.increment / 100) if furthest_order_bound < new_amount > closer_order_bound: @@ -861,9 +859,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): orders_count = len(orders) orders = list(reversed(orders)) - closest_order = orders[-1] - closest_order_bound = closest_order['base']['amount'] * math.sqrt(1 + self.increment) for order in orders: order_index = orders.index(order) From 9f5a872982f4ae1bfb4bed2b25142d8ec6e217ab Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 23 Nov 2018 10:23:10 +0200 Subject: [PATCH 1019/1846] Change dexbot version number to 0.7.25 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index e86f81695..ecbb3eb69 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.24' +VERSION = '0.7.25' AUTHOR = 'Codaone Oy' __version__ = VERSION From 5c91edfe7f2214f223aab980cf7c7b8f23663238 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Fri, 23 Nov 2018 19:02:05 -0800 Subject: [PATCH 1020/1846] clean up unit tests, add consolidated quote --- dexbot/strategies/base.py | 21 +++----- dexbot/strategies/external_feeds/ccxt_feed.py | 1 - .../strategies/external_feeds/gecko_feed.py | 32 ++++++------ .../strategies/external_feeds/price_feed.py | 25 +++++---- .../strategies/external_feeds/process_pair.py | 2 + .../external_feeds/tests/async_feeds_test.py | 4 ++ .../external_feeds/tests/ccxt_test.py | 5 ++ .../external_feeds/tests/gecko_test.py | 6 +-- .../external_feeds/tests/price_feed_test.py | 52 ++++++++++++++++--- .../external_feeds/tests/process_pair_test.py | 4 ++ .../external_feeds/tests/pywaves_test.py | 5 +- .../strategies/external_feeds/tests/styles.py | 3 ++ .../external_feeds/tests/waves_test.py | 29 ++++++++--- .../strategies/external_feeds/waves_feed.py | 23 ++++---- 14 files changed, 140 insertions(+), 72 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index ee5bd7026..deac41e1a 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -612,25 +612,20 @@ def get_lowest_own_sell_order(self, orders=None): def get_external_market_center_price(self): center_price = None - print("inside get_emcp, exchange: ", self.external_price_source, sep=':') # debug + self.log.debug('inside get_external_mcp, exchange: {} '.format(self.external_price_source)) market = self.market.get_string('/') - print("market:", market, sep=' ') # debug - -# market = 'BTC/USD' # override for testing debut -# print("dummy market:", market, sep=' ') # debug - - print("exchange:", self.external_price_source, sep=' ') + self.log.debug('market: {} '.format(market)) + self.log.debug('exchange: {}'.format(self.external_price_source)) pf = PriceFeed(self.external_price_source, market) pf.filter_symbols() center_price = pf.get_center_price(None) - print(" PriceFeed ", center_price, sep=':') + self.log.debug('PriceFeed: {}'.format(center_price)) if center_price is None: # try USDT center_price = pf.get_center_price("USDT") - print("s/USD/USDT, center price: ", center_price) -# center_price = 0.10778888 # Dummy price for now -# print("external dummy price", center_price, sep=' ') + self.log.debug('Try Substitute USD/USDT center price: {}'.format(center_price)) return center_price + def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors=False): """ Returns the center price of market including own orders. @@ -656,9 +651,9 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors self.disabled = True return None # Calculate and return market center price. make sure buy_price has value - if buy_price is not None: + if buy_price: center_price = buy_price * math.sqrt(sell_price / buy_price) - print("inside get_market_center_price : " , center_price, sep=' ') # debug + self.log.debug('Inside get_market_center_price: {} '.format(center_price)) return center_price def get_market_buy_price(self, quote_amount=0, base_amount=0, exclude_own_orders=True): diff --git a/dexbot/strategies/external_feeds/ccxt_feed.py b/dexbot/strategies/external_feeds/ccxt_feed.py index 52884becf..a61aaf845 100644 --- a/dexbot/strategies/external_feeds/ccxt_feed.py +++ b/dexbot/strategies/external_feeds/ccxt_feed.py @@ -1,7 +1,6 @@ import json import asyncio import ccxt.async_support as accxt -from pprint import pprint async def print_ticker(symbol, id): diff --git a/dexbot/strategies/external_feeds/gecko_feed.py b/dexbot/strategies/external_feeds/gecko_feed.py index 57c9aab72..09b339076 100644 --- a/dexbot/strategies/external_feeds/gecko_feed.py +++ b/dexbot/strategies/external_feeds/gecko_feed.py @@ -1,4 +1,5 @@ import requests +import asyncio from dexbot.strategies.external_feeds.process_pair import split_pair, debug """ @@ -10,28 +11,20 @@ GECKO_COINS_URL = 'https://api.coingecko.com/api/v3/coins/' -def get_json(url): +async def get_json(url): r = requests.get(url) json_obj = r.json() return json_obj -def check_gecko_symbol_exists(coin_list, symbol): - try: - symbol_name = [obj for obj in coin_list if obj['symbol'] == symbol][0]['id'] - return symbol_name - except IndexError: - return None - - -def get_market_price(base, quote): +def _get_market_price(base, quote): try: - coin_list = get_json(GECKO_COINS_URL+'list') + coin_list = asyncio.get_event_loop().run_until_complete(get_json(GECKO_COINS_URL+'list')) quote_name = check_gecko_symbol_exists(coin_list, quote.lower()) lookup_pair = "?vs_currency="+base.lower()+"&ids="+quote_name market_url = GECKO_COINS_URL+'markets'+lookup_pair debug(market_url) - ticker = get_json(market_url) + ticker = asyncio.get_event_loop().run_until_complete(get_json(market_url)) current_price = None for entry in ticker: current_price = entry['current_price'] @@ -43,19 +36,28 @@ def get_market_price(base, quote): return None +def check_gecko_symbol_exists(coin_list, symbol): + try: + symbol_name = [obj for obj in coin_list if obj['symbol'] == symbol][0]['id'] + return symbol_name + except IndexError: + return None + + def get_gecko_price_by_pair(pair): current_price = None try: quote = pair[0] base = pair[1] - current_price = get_market_price(base, quote) + current_price = _get_market_price(base, quote) if current_price is None: # Try inverted version - debug(" Trying pair inversion...") - current_price = get_market_price(quote, base) + debug("Trying pair inversion...") + current_price = _get_market_price(quote, base) debug(base + '/' + quote, str(current_price)) if current_price is not None: # Re-invert price actual_price = 1/current_price debug(quote + '/' + base, str(actual_price)) + current_price= actual_price else: debug(pair, current_price) except Exception as e: diff --git a/dexbot/strategies/external_feeds/price_feed.py b/dexbot/strategies/external_feeds/price_feed.py index 5f4d62b7b..6e5fa832c 100755 --- a/dexbot/strategies/external_feeds/price_feed.py +++ b/dexbot/strategies/external_feeds/price_feed.py @@ -5,18 +5,18 @@ import re - class PriceFeed: """ - price feed class to handle price feed + price feed class, which handles all data requests for external center price """ + def __init__(self, exchange, symbol): self._alt_exchanges = ['gecko', 'waves'] # assume all other exchanges are ccxt self._exchange= exchange self._symbol=symbol self._pair= split_pair(symbol) - - + + @property def symbol(self): return self._symbol @@ -55,26 +55,20 @@ def filter_symbols(self): debug(self._pair) - def set_alt_usd_pair(self): + def set_alt_usd_pair(self, type): """ get center price by search and replace for USD with USDT only - extend this method in the future for other usdt like options, e.g. USDC, TUSD,etc + todo: extend this method in the future for others, e.g. USDC, TUSD,etc """ alt_usd_pair = self._pair i = 0 while i < 2: if re.match(r'^USD$', self._pair[i], re.I): - alt_usd_pair[i] = re.sub(r'USD','USDT', self._pair[i]) + alt_usd_pair[i] = re.sub(r'USD', type, self._pair[i]) i = i+1 self._pair = alt_usd_pair self._symbol = join_pair(self._pair) - - def get_center_price(self, type): - if type == "USDT": - self.set_alt_usd_pair() - return self._get_center_price() - def _get_center_price(self): symbol = self._symbol @@ -91,4 +85,9 @@ def _get_center_price(self): return price + def get_center_price(self, type): + if type is not None: + self.set_alt_usd_pair(type) + return self._get_center_price() + diff --git a/dexbot/strategies/external_feeds/process_pair.py b/dexbot/strategies/external_feeds/process_pair.py index da5dd2359..e2cd49f71 100644 --- a/dexbot/strategies/external_feeds/process_pair.py +++ b/dexbot/strategies/external_feeds/process_pair.py @@ -2,6 +2,7 @@ isDebug = True + def debug(*args): if isDebug: print(' '.join([str(arg) for arg in args])) @@ -43,6 +44,7 @@ def join_pair(pair): def get_consolidated_pair(base, quote): # Split into two USD pairs, STEEM/BTS=(BTS/USD * USD/STEEM) + # todo pair1 = [base, 'USD'] # BTS/USD pair=[quote, base] pair2 = ['USD', quote] return pair1, pair2 diff --git a/dexbot/strategies/external_feeds/tests/async_feeds_test.py b/dexbot/strategies/external_feeds/tests/async_feeds_test.py index 1c4339a3c..3725c7457 100644 --- a/dexbot/strategies/external_feeds/tests/async_feeds_test.py +++ b/dexbot/strategies/external_feeds/tests/async_feeds_test.py @@ -1,6 +1,10 @@ import asyncio import aiohttp +""" +This is the unit test for testing async with ccxt only with multiple urls. +""" + gecko_coins_url = 'https://api.coingecko.com/api/v3/coins/' waves_symbols = 'http://marketdata.wavesplatform.com/api/symbols' cwatch_assets = 'https://api.cryptowat.ch/assets' diff --git a/dexbot/strategies/external_feeds/tests/ccxt_test.py b/dexbot/strategies/external_feeds/tests/ccxt_test.py index 94a2ca5ac..c79309b4b 100644 --- a/dexbot/strategies/external_feeds/tests/ccxt_test.py +++ b/dexbot/strategies/external_feeds/tests/ccxt_test.py @@ -2,8 +2,13 @@ import asyncio from dexbot.strategies.external_feeds.ccxt_feed import get_ccxt_load_markets, get_ccxt_price +""" +This is the unit test for getting external feed data from CCXT using ccxt_feed module. +""" + if __name__ == '__main__': + exchanges =['bitfinex', 'kraken', 'binance', 'gdax'] symbol = 'BTC/USDT' diff --git a/dexbot/strategies/external_feeds/tests/gecko_test.py b/dexbot/strategies/external_feeds/tests/gecko_test.py index 4a8c4376a..fd6b25d7b 100644 --- a/dexbot/strategies/external_feeds/tests/gecko_test.py +++ b/dexbot/strategies/external_feeds/tests/gecko_test.py @@ -21,13 +21,11 @@ def test_feed(symbol): """ [symbol] Symbol example: btc/usd or btc:usd """ - try: + try: price = get_gecko_price(symbol_=symbol) - print(price) - + print(price) pair = split_pair(symbol) price = get_gecko_price(pair_=pair) - except Exception as e: print_usage() print(type(e).__name__, e.args, str(e)) diff --git a/dexbot/strategies/external_feeds/tests/price_feed_test.py b/dexbot/strategies/external_feeds/tests/price_feed_test.py index eb0eb117a..a704dbafe 100644 --- a/dexbot/strategies/external_feeds/tests/price_feed_test.py +++ b/dexbot/strategies/external_feeds/tests/price_feed_test.py @@ -1,16 +1,19 @@ from dexbot.strategies.external_feeds.price_feed import PriceFeed +from dexbot.strategies.external_feeds.process_pair import get_consolidated_pair + """ -Note from Marko Paasila, In DEXBot: -unit of measure = BASE -asset of interest = QUOTE +This is the unit test for testing price_feed module. +Run this test first to cover everything in external feeds + +Note from Marko, In DEXBot: unit of measure = BASE, asset of interest = QUOTE """ -if __name__ == '__main__': +def test_exchanges(): center_price = None - exchanges = ['gecko', 'bitfinex', 'kraken', 'gdax', 'binance', 'waves'] symbol = 'BTC/USDT' + exchanges = ['gecko', 'bitfinex', 'kraken', 'gdax', 'binance', 'waves'] for exchange in exchanges: symbol = 'BTC/USD' @@ -18,7 +21,40 @@ pf.filter_symbols() center_price = pf.get_center_price(None) print("center price: ", center_price) - if center_price is None: # try USDT - center_price = pf.get_center_price("USDT") - print("s/usd/usdt, center price: ", center_price) + if center_price is None: # try USDT + center_price = pf.get_center_price('USDT') + print("try again, s/USD/USDT, center price: ", center_price) + + +def test_consolidated_pair(): + center_price = None + try: + symbol2 = 'STEEM/BTS' # STEEM/USD * USD/BTS = STEEM/BTS + pf = PriceFeed('gecko', symbol2) + pair1, pair2 = get_consolidated_pair('STEEM', 'BTS') + print(pair1, pair2) + pf.pair = pair1 + p1_price = pf.get_center_price(None) + print("pair1 price", p1_price, sep=':') + pf.pair = pair2 + p2_price = pf.get_center_price(None) + print("pair2 price", p2_price, sep='=') + + if p1_price and p2_price: + center_price = p1_price * p2_price + print(symbol2, "price is ", center_price) + except Exception as e: + print(type(e).__name__, e.args, 'Error') + + +def test_alternative_usd(): + alternative_usd = ['USDT', 'USDC', 'TUSD', 'GUSD'] + # todo - refactor price_feed to try alt USD options, but only if they exist + + + +if __name__ == '__main__': + + test_exchanges() + test_consolidated_pair() diff --git a/dexbot/strategies/external_feeds/tests/process_pair_test.py b/dexbot/strategies/external_feeds/tests/process_pair_test.py index 74b82c965..39ae17966 100644 --- a/dexbot/strategies/external_feeds/tests/process_pair_test.py +++ b/dexbot/strategies/external_feeds/tests/process_pair_test.py @@ -1,5 +1,9 @@ from dexbot.strategies.external_feeds.process_pair import split_pair, get_consolidated_pair, filter_prefix_symbol, filter_bit_symbol +""" +This is the unit test for filters in process_pair module. +""" + # Unit Tests def test_consolidated_pair(): symbol = 'STEEM:BTS' # pair = 'STEEM:BTS' or STEEM/BTS' diff --git a/dexbot/strategies/external_feeds/tests/pywaves_test.py b/dexbot/strategies/external_feeds/tests/pywaves_test.py index 8b76d7a6b..60e8921b9 100644 --- a/dexbot/strategies/external_feeds/tests/pywaves_test.py +++ b/dexbot/strategies/external_feeds/tests/pywaves_test.py @@ -1,5 +1,8 @@ import pywaves as pw - +""" +pywaves is an open source library for waves. While it is not as stable as REST API, +we leave the test here if integration is desired for future dexbot cross-exchange strategies. +""" if __name__ == '__main__': diff --git a/dexbot/strategies/external_feeds/tests/styles.py b/dexbot/strategies/external_feeds/tests/styles.py index ca5d0e300..686e8c88f 100644 --- a/dexbot/strategies/external_feeds/tests/styles.py +++ b/dexbot/strategies/external_feeds/tests/styles.py @@ -1,5 +1,8 @@ import os +""" +This is the unit test for print styles +""" def style(s, style): return style + s + '\033[0m' diff --git a/dexbot/strategies/external_feeds/tests/waves_test.py b/dexbot/strategies/external_feeds/tests/waves_test.py index af47dcaa3..2d5e98f5d 100644 --- a/dexbot/strategies/external_feeds/tests/waves_test.py +++ b/dexbot/strategies/external_feeds/tests/waves_test.py @@ -1,18 +1,31 @@ from dexbot.strategies.external_feeds.process_pair import split_pair, filter_prefix_symbol, filter_bit_symbol -from dexbot.strategies.external_feeds.waves_feed import get_waves_price +from dexbot.strategies.external_feeds.waves_feed import get_waves_price, get_waves_symbols + +""" +This is the unit test for getting external feed data from waves DEX. +""" + if __name__ == '__main__': - symbol = 'BTC/USD' # quote/base for external exchanges + symbol = 'BTC/USD' # quote/base for external exchanges print(symbol, "=") raw_pair = split_pair(symbol) pair = [filter_bit_symbol(j) for j in [filter_prefix_symbol(i) for i in raw_pair]] - + + # test symbol and pair options for getting price pair_price = get_waves_price(pair_=pair) - if pair_price is not None: - print("pair price", pair_price, sep=":") + if pair_price: + print("pair price ", pair_price, sep=":") current_price = get_waves_price(symbol_=symbol) - if current_price is not None: - print("symbol price", current_price, sep=":") - + if current_price: + print("symbol price ", current_price, sep=":") + + + # get entire symbol list + print("\n") + # symbol_list = get_waves_symbols() + # print(symbol_list) + + diff --git a/dexbot/strategies/external_feeds/waves_feed.py b/dexbot/strategies/external_feeds/waves_feed.py index 10ce9399b..7736c55e5 100644 --- a/dexbot/strategies/external_feeds/waves_feed.py +++ b/dexbot/strategies/external_feeds/waves_feed.py @@ -1,49 +1,54 @@ +from dexbot.strategies.external_feeds.process_pair import split_pair, debug import requests +import asyncio WAVES_URL = 'https://marketdata.wavesplatform.com/api/' SYMBOLS_URL = "/symbols" MARKET_URL = "/ticker/" -def get_json(url): +async def get_json(url): r = requests.get(url) json_obj = r.json() return json_obj -def get_waves_symbols(): - symbol_list = get_json(WAVES_URL + SYMBOLS_URL) - return symbol_list - - def get_last_price(base, quote): current_price = None try: market_bq = MARKET_URL + quote +'/'+ base # external exchange format - ticker = get_json(WAVES_URL + market_bq) + ticker = asyncio.get_event_loop().run_until_complete(get_json(WAVES_URL + market_bq)) current_price = ticker['24h_close'] except Exception as e: pass # No pair found on waves dex for external price. return current_price +def get_waves_symbols(): + symbol_list = asyncio.get_event_loop().run_until_complete(get_json(WAVES_URL + SYMBOLS_URL)) + return symbol_list + + def get_waves_by_pair(pair): current_price = get_last_price(pair[1], pair[0]) # base, quote if current_price is None: # try inversion price = get_last_price(pair[0], pair[1]) - current_price = 1/float(price) + if price is not None: + current_price = 1/float(price) return current_price def get_waves_price(**kwargs): price = None for key, value in list(kwargs.items()): - print("The value of {} is {}".format(key, value)) + debug("The value of {} is {}".format(key, value)) if key == "pair_": price = get_waves_by_pair(value) + debug(value, price) elif key == "symbol_": pair = split_pair(value) price = get_waves_by_pair(pair) + debug(pair, price) return price From 3fe753e8a309190cabcdcb4322e048a0f006663a Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Fri, 23 Nov 2018 22:13:15 -0800 Subject: [PATCH 1021/1846] move print to debug, add consolidated to base.py --- dexbot/strategies/base.py | 6 ++- .../strategies/external_feeds/price_feed.py | 31 ++++++++++-- .../external_feeds/tests/price_feed_test.py | 47 +++++++++---------- dexbot/strategies/relative_orders.py | 2 - 4 files changed, 52 insertions(+), 34 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index deac41e1a..c9711a6d5 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -615,14 +615,16 @@ def get_external_market_center_price(self): self.log.debug('inside get_external_mcp, exchange: {} '.format(self.external_price_source)) market = self.market.get_string('/') self.log.debug('market: {} '.format(market)) - self.log.debug('exchange: {}'.format(self.external_price_source)) pf = PriceFeed(self.external_price_source, market) pf.filter_symbols() center_price = pf.get_center_price(None) self.log.debug('PriceFeed: {}'.format(center_price)) if center_price is None: # try USDT center_price = pf.get_center_price("USDT") - self.log.debug('Try Substitute USD/USDT center price: {}'.format(center_price)) + self.log.debug('Substitute USD/USDT center price: {}'.format(center_price)) + if center_price is None: # try consolidated + center_price = pf.get_consolidated_price() + self.log.debug('Consolidated center price: {}'.format(center_price)) return center_price diff --git a/dexbot/strategies/external_feeds/price_feed.py b/dexbot/strategies/external_feeds/price_feed.py index 6e5fa832c..490dfc5f3 100755 --- a/dexbot/strategies/external_feeds/price_feed.py +++ b/dexbot/strategies/external_feeds/price_feed.py @@ -1,7 +1,7 @@ from dexbot.strategies.external_feeds.ccxt_feed import get_ccxt_price from dexbot.strategies.external_feeds.waves_feed import get_waves_price from dexbot.strategies.external_feeds.gecko_feed import get_gecko_price -from dexbot.strategies.external_feeds.process_pair import split_pair, join_pair, filter_prefix_symbol, filter_bit_symbol, debug +from dexbot.strategies.external_feeds.process_pair import split_pair, join_pair, filter_prefix_symbol, filter_bit_symbol, get_consolidated_pair, debug import re @@ -54,11 +54,32 @@ def filter_symbols(self): self._pair = [filter_bit_symbol(j) for j in [filter_prefix_symbol(i) for i in raw_pair]] debug(self._pair) + + def get_consolidated_price(self): + """ + assumes XXX/YYY must be broken into XXX/USD * USD/YYY + """ + center_price = None + original_pair = self.pair + try: + pair1, pair2 = get_consolidated_pair(self.pair[0], self.pair[1]) + self.pair = pair1 + p1_price = self.get_center_price(None) + self.pair = pair2 + p2_price = self.get_center_price(None) + if p1_price and p2_price: + center_price = p1_price * p2_price + print(original_pair, "price is ", center_price) + self.pair = original_pair # put original pair back + except Exception as e: + print(type(e).__name__, e.args, 'Error') + return center_price + def set_alt_usd_pair(self, type): """ get center price by search and replace for USD with USDT only - todo: extend this method in the future for others, e.g. USDC, TUSD,etc + todo: extend in PriceFeed or base.py for other alts, e.g. USDC, TUSD,etc """ alt_usd_pair = self._pair i = 0 @@ -74,14 +95,14 @@ def _get_center_price(self): symbol = self._symbol price = None if self._exchange not in self._alt_exchanges: - print("use ccxt exchange ", self._exchange, ' symbol ', symbol, sep=":") price = get_ccxt_price(symbol, self._exchange) + debug("use ccxt exchange ", self._exchange, ' symbol ', symbol, ' price:', price) elif self._exchange == 'gecko': - print("gecko exchange - ", self._exchange, ' symbol ', symbol, sep=":") price = get_gecko_price(symbol_=symbol) + debug("gecko exchange - ", self._exchange, ' symbol ', symbol, ' price:', price) elif self._exchange == 'waves': - print("use waves -", self._exchange, ' symbol ', symbol, sep=":") price = get_waves_price(symbol_=symbol) + debug("use waves -", self._exchange, ' symbol ', symbol, ' price:', price) return price diff --git a/dexbot/strategies/external_feeds/tests/price_feed_test.py b/dexbot/strategies/external_feeds/tests/price_feed_test.py index a704dbafe..816450da5 100644 --- a/dexbot/strategies/external_feeds/tests/price_feed_test.py +++ b/dexbot/strategies/external_feeds/tests/price_feed_test.py @@ -12,11 +12,10 @@ def test_exchanges(): center_price = None - symbol = 'BTC/USDT' + symbol = 'BTC/USD' exchanges = ['gecko', 'bitfinex', 'kraken', 'gdax', 'binance', 'waves'] for exchange in exchanges: - symbol = 'BTC/USD' pf = PriceFeed(exchange, symbol) pf.filter_symbols() center_price = pf.get_center_price(None) @@ -27,34 +26,32 @@ def test_exchanges(): def test_consolidated_pair(): - center_price = None - try: - symbol2 = 'STEEM/BTS' # STEEM/USD * USD/BTS = STEEM/BTS - pf = PriceFeed('gecko', symbol2) - pair1, pair2 = get_consolidated_pair('STEEM', 'BTS') - print(pair1, pair2) - pf.pair = pair1 - p1_price = pf.get_center_price(None) - print("pair1 price", p1_price, sep=':') - pf.pair = pair2 - p2_price = pf.get_center_price(None) - print("pair2 price", p2_price, sep='=') - - if p1_price and p2_price: - center_price = p1_price * p2_price - print(symbol2, "price is ", center_price) - except Exception as e: - print(type(e).__name__, e.args, 'Error') - + symbol2 = 'STEEM/BTS' # STEEM/USD * USD/BTS = STEEM/BTS + pf = PriceFeed('gecko', symbol2) + center_price = pf.get_consolidated_price() + print(center_price) + def test_alternative_usd(): + # todo - refactor price_feed to handle alt USD options. alternative_usd = ['USDT', 'USDC', 'TUSD', 'GUSD'] - # todo - refactor price_feed to try alt USD options, but only if they exist - - + exchanges = ['bittrex', 'poloniex', 'gemini', 'bitfinex', 'kraken', 'binance', 'okex'] + symbol = 'BTC/USD' # replace with alt usd + + for exchange in exchanges: + for alt in alternative_usd: + pf = PriceFeed(exchange, symbol) + center_price = pf.get_center_price(None) + if center_price: + print(symbol,' using alt:', alt, center_price, "\n", sep=' ') + else: + center_price = pf.get_center_price(alt) + if center_price: + print(symbol,' using alt:', alt, center_price, "\n", sep=' ') + if __name__ == '__main__': test_exchanges() test_consolidated_pair() - + test_alternative_usd() diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index cced0e1b2..c736621bb 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -92,10 +92,8 @@ def __init__(self, *args, **kwargs): self.center_price = self.get_external_market_center_price() if self.center_price is None: self.center_price = self.worker["center_price"] # set as manual - print("inside relative orders, get external center price", self.center_price, sep=' ') # debug else: self.center_price = self.worker["center_price"] - print("inside relative orders, no external center price", self.center_price, sep=' ') # debug self.is_relative_order_size = self.worker.get('relative_order_size', False) self.is_asset_offset = self.worker.get('center_price_offset', False) From f953bb53402b19d541e58582a833dc1c48d323f4 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Fri, 23 Nov 2018 22:27:21 -0800 Subject: [PATCH 1022/1846] amend name to exchange_id --- dexbot/strategies/external_feeds/ccxt_feed.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/external_feeds/ccxt_feed.py b/dexbot/strategies/external_feeds/ccxt_feed.py index a61aaf845..c43f9f46e 100644 --- a/dexbot/strategies/external_feeds/ccxt_feed.py +++ b/dexbot/strategies/external_feeds/ccxt_feed.py @@ -1,11 +1,11 @@ -import json import asyncio + import ccxt.async_support as accxt -async def print_ticker(symbol, id): +async def print_ticker(symbol, exchange_id): # Verbose mode will show the order of execution to verify concurrency - exchange = getattr(accxt, id)({'verbose': True}) + exchange = getattr(accxt, exchange_id)({'verbose': True}) await exchange.fetch_ticker(symbol) await exchange.close() From dc8c37d79d99dc1dc90256d014744bd30938fe85 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Fri, 23 Nov 2018 22:29:37 -0800 Subject: [PATCH 1023/1846] PEP8 --- .../strategies/external_feeds/price_feed.py | 48 +++++++------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/dexbot/strategies/external_feeds/price_feed.py b/dexbot/strategies/external_feeds/price_feed.py index 490dfc5f3..a8081d1c7 100755 --- a/dexbot/strategies/external_feeds/price_feed.py +++ b/dexbot/strategies/external_feeds/price_feed.py @@ -1,7 +1,8 @@ -from dexbot.strategies.external_feeds.ccxt_feed import get_ccxt_price +import dexbot.strategies.external_feeds.ccxt_feed from dexbot.strategies.external_feeds.waves_feed import get_waves_price from dexbot.strategies.external_feeds.gecko_feed import get_gecko_price -from dexbot.strategies.external_feeds.process_pair import split_pair, join_pair, filter_prefix_symbol, filter_bit_symbol, get_consolidated_pair, debug +from dexbot.strategies.external_feeds.process_pair import split_pair, join_pair, filter_prefix_symbol, \ + filter_bit_symbol, get_consolidated_pair, debug import re @@ -9,51 +10,43 @@ class PriceFeed: """ price feed class, which handles all data requests for external center price """ - + def __init__(self, exchange, symbol): - self._alt_exchanges = ['gecko', 'waves'] # assume all other exchanges are ccxt - self._exchange= exchange - self._symbol=symbol - self._pair= split_pair(symbol) + self._alt_exchanges = ['gecko', 'waves'] # assume all other exchanges are ccxt + self._exchange = exchange + self._symbol = symbol + self._pair = split_pair(symbol) - @property def symbol(self): return self._symbol - @symbol.setter def symbol(self, symbol): self._symbol = symbol self._pair = split_pair(self._symbol) - @property def pair(self): return self._pair - @pair.setter - def pair(self, pair): + def pair(self, pair): self._pair = pair self._symbol = join_pair(pair) - @property def exchange(self): return self._exchange - @exchange.setter def exchange(self, exchange): self._exchange = exchange - def filter_symbols(self): raw_pair = self._pair self._pair = [filter_bit_symbol(j) for j in [filter_prefix_symbol(i) for i in raw_pair]] - debug(self._pair) - + debug(self._pair) def get_consolidated_price(self): """ @@ -70,12 +63,11 @@ def get_consolidated_price(self): if p1_price and p2_price: center_price = p1_price * p2_price print(original_pair, "price is ", center_price) - self.pair = original_pair # put original pair back + self.pair = original_pair # put original pair back except Exception as e: print(type(e).__name__, e.args, 'Error') return center_price - - + def set_alt_usd_pair(self, type): """ get center price by search and replace for USD with USDT only @@ -86,29 +78,25 @@ def set_alt_usd_pair(self, type): while i < 2: if re.match(r'^USD$', self._pair[i], re.I): alt_usd_pair[i] = re.sub(r'USD', type, self._pair[i]) - i = i+1 + i = i + 1 self._pair = alt_usd_pair self._symbol = join_pair(self._pair) - - + def _get_center_price(self): symbol = self._symbol price = None if self._exchange not in self._alt_exchanges: - price = get_ccxt_price(symbol, self._exchange) + price = dexbot.strategies.external_feeds.ccxt_feed.get_ccxt_price(symbol, self._exchange) debug("use ccxt exchange ", self._exchange, ' symbol ', symbol, ' price:', price) elif self._exchange == 'gecko': price = get_gecko_price(symbol_=symbol) - debug("gecko exchange - ", self._exchange, ' symbol ', symbol, ' price:', price) + debug("gecko exchange - ", self._exchange, ' symbol ', symbol, ' price:', price) elif self._exchange == 'waves': price = get_waves_price(symbol_=symbol) - debug("use waves -", self._exchange, ' symbol ', symbol, ' price:', price) + debug("use waves -", self._exchange, ' symbol ', symbol, ' price:', price) return price - def get_center_price(self, type): if type is not None: self.set_alt_usd_pair(type) - return self._get_center_price() - - + return self._get_center_price() From 99c9b2e6fe8e7d71a7d367386854771a0fba3f01 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Fri, 23 Nov 2018 22:30:18 -0800 Subject: [PATCH 1024/1846] PEP8 --- dexbot/strategies/external_feeds/waves_feed.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dexbot/strategies/external_feeds/waves_feed.py b/dexbot/strategies/external_feeds/waves_feed.py index 7736c55e5..a63e8cfc7 100644 --- a/dexbot/strategies/external_feeds/waves_feed.py +++ b/dexbot/strategies/external_feeds/waves_feed.py @@ -1,4 +1,4 @@ -from dexbot.strategies.external_feeds.process_pair import split_pair, debug +import dexbot.strategies.external_feeds.process_pair import requests import asyncio @@ -41,14 +41,14 @@ def get_waves_by_pair(pair): def get_waves_price(**kwargs): price = None for key, value in list(kwargs.items()): - debug("The value of {} is {}".format(key, value)) + dexbot.strategies.external_feeds.process_pair.debug("The value of {} is {}".format(key, value)) if key == "pair_": price = get_waves_by_pair(value) - debug(value, price) + dexbot.strategies.external_feeds.process_pair.debug(value, price) elif key == "symbol_": - pair = split_pair(value) + pair = dexbot.strategies.external_feeds.process_pair.split_pair(value) price = get_waves_by_pair(pair) - debug(pair, price) + dexbot.strategies.external_feeds.process_pair.debug(pair, price) return price From 41b72da02687a37e5f89bd79555d75f29d448391 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Fri, 23 Nov 2018 22:33:20 -0800 Subject: [PATCH 1025/1846] PEP8 --- dexbot/strategies/external_feeds/ccxt_feed.py | 8 ++--- .../strategies/external_feeds/gecko_feed.py | 29 +++++++++---------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/dexbot/strategies/external_feeds/ccxt_feed.py b/dexbot/strategies/external_feeds/ccxt_feed.py index c43f9f46e..08c437f15 100644 --- a/dexbot/strategies/external_feeds/ccxt_feed.py +++ b/dexbot/strategies/external_feeds/ccxt_feed.py @@ -12,7 +12,7 @@ async def print_ticker(symbol, exchange_id): async def get_ccxt_load_markets(exchange_id): exchange = getattr(accxt, exchange_id)({'verbose': False}) - symbols = await exchange.load_markets() + symbols = await exchange.load_markets() await exchange.close() return symbols @@ -34,10 +34,8 @@ async def fetch_ticker(exchange, symbol): def get_ccxt_price(symbol, exchange_name): """ Get all tickers from multiple exchanges using async """ center_price = None - exchange = getattr(accxt, exchange_name)({'verbose':False}) + exchange = getattr(accxt, exchange_name)({'verbose': False}) ticker = asyncio.get_event_loop().run_until_complete(fetch_ticker(exchange, symbol)) if ticker: - center_price = (ticker['bid'] + ticker['ask'])/2 + center_price = (ticker['bid'] + ticker['ask']) / 2 return center_price - - diff --git a/dexbot/strategies/external_feeds/gecko_feed.py b/dexbot/strategies/external_feeds/gecko_feed.py index 09b339076..e1f46196a 100644 --- a/dexbot/strategies/external_feeds/gecko_feed.py +++ b/dexbot/strategies/external_feeds/gecko_feed.py @@ -19,10 +19,10 @@ async def get_json(url): def _get_market_price(base, quote): try: - coin_list = asyncio.get_event_loop().run_until_complete(get_json(GECKO_COINS_URL+'list')) + coin_list = asyncio.get_event_loop().run_until_complete(get_json(GECKO_COINS_URL + 'list')) quote_name = check_gecko_symbol_exists(coin_list, quote.lower()) - lookup_pair = "?vs_currency="+base.lower()+"&ids="+quote_name - market_url = GECKO_COINS_URL+'markets'+lookup_pair + lookup_pair = "?vs_currency=" + base.lower() + "&ids=" + quote_name + market_url = GECKO_COINS_URL + 'markets' + lookup_pair debug(market_url) ticker = asyncio.get_event_loop().run_until_complete(get_json(market_url)) current_price = None @@ -43,21 +43,21 @@ def check_gecko_symbol_exists(coin_list, symbol): except IndexError: return None - + def get_gecko_price_by_pair(pair): current_price = None try: quote = pair[0] - base = pair[1] - current_price = _get_market_price(base, quote) - if current_price is None: # Try inverted version + base = pair[1] + current_price = _get_market_price(base, quote) + if current_price is None: # Try inverted version debug("Trying pair inversion...") current_price = _get_market_price(quote, base) - debug(base + '/' + quote, str(current_price)) - if current_price is not None: # Re-invert price - actual_price = 1/current_price + debug(base + '/' + quote, str(current_price)) + if current_price is not None: # Re-invert price + actual_price = 1 / current_price debug(quote + '/' + base, str(actual_price)) - current_price= actual_price + current_price = actual_price else: debug(pair, current_price) except Exception as e: @@ -68,11 +68,10 @@ def get_gecko_price_by_pair(pair): def get_gecko_price(**kwargs): price = None for key, value in list(kwargs.items()): - debug("The value of {} is {}".format(key, value)) # debug + debug("The value of {} is {}".format(key, value)) # debug if key == "pair_": price = get_gecko_price_by_pair(value) elif key == "symbol_": - pair = split_pair(value) # pair=[quote, base] - price = get_gecko_price_by_pair(pair) + pair = split_pair(value) # pair=[quote, base] + price = get_gecko_price_by_pair(pair) return price - From 58cd9dd2f58bc79df1a4f956f6f96b72175e8f42 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Fri, 23 Nov 2018 22:35:41 -0800 Subject: [PATCH 1026/1846] PEP8 --- dexbot/strategies/external_feeds/process_pair.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/dexbot/strategies/external_feeds/process_pair.py b/dexbot/strategies/external_feeds/process_pair.py index e2cd49f71..c4888c613 100644 --- a/dexbot/strategies/external_feeds/process_pair.py +++ b/dexbot/strategies/external_feeds/process_pair.py @@ -11,7 +11,7 @@ def debug(*args): def print_args(*args): print(' '.join([str(arg) for arg in args])) - + def filter_prefix_symbol(symbol): # Example open.USD or bridge.USD, remove leading bit up to . base = '' @@ -38,15 +38,12 @@ def split_pair(symbol): def join_pair(pair): - symbol = pair[0]+'/'+pair[1] + symbol = pair[0] + '/' + pair[1] return symbol def get_consolidated_pair(base, quote): # Split into two USD pairs, STEEM/BTS=(BTS/USD * USD/STEEM) - # todo pair1 = [base, 'USD'] # BTS/USD pair=[quote, base] pair2 = ['USD', quote] return pair1, pair2 - - From e946624d6945bf568d89b24756d873449d8bb18e Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Fri, 23 Nov 2018 22:35:57 -0800 Subject: [PATCH 1027/1846] PEP8 --- dexbot/strategies/external_feeds/waves_feed.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/dexbot/strategies/external_feeds/waves_feed.py b/dexbot/strategies/external_feeds/waves_feed.py index a63e8cfc7..1924e5f93 100644 --- a/dexbot/strategies/external_feeds/waves_feed.py +++ b/dexbot/strategies/external_feeds/waves_feed.py @@ -15,11 +15,11 @@ async def get_json(url): def get_last_price(base, quote): current_price = None - try: - market_bq = MARKET_URL + quote +'/'+ base # external exchange format + try: + market_bq = MARKET_URL + quote + '/' + base # external exchange format ticker = asyncio.get_event_loop().run_until_complete(get_json(WAVES_URL + market_bq)) - current_price = ticker['24h_close'] - except Exception as e: + current_price = ticker['24h_close'] + except Exception as e: pass # No pair found on waves dex for external price. return current_price @@ -30,11 +30,11 @@ def get_waves_symbols(): def get_waves_by_pair(pair): - current_price = get_last_price(pair[1], pair[0]) # base, quote - if current_price is None: # try inversion + current_price = get_last_price(pair[1], pair[0]) # base, quote + if current_price is None: # try inversion price = get_last_price(pair[0], pair[1]) if price is not None: - current_price = 1/float(price) + current_price = 1 / float(price) return current_price @@ -50,5 +50,3 @@ def get_waves_price(**kwargs): price = get_waves_by_pair(pair) dexbot.strategies.external_feeds.process_pair.debug(pair, price) return price - - From 53810c560ed882b83a084f8f29c020324deea28f Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Fri, 23 Nov 2018 22:36:45 -0800 Subject: [PATCH 1028/1846] PEP8 --- .../external_feeds/tests/waves_test.py | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/dexbot/strategies/external_feeds/tests/waves_test.py b/dexbot/strategies/external_feeds/tests/waves_test.py index 2d5e98f5d..857c9bf9b 100644 --- a/dexbot/strategies/external_feeds/tests/waves_test.py +++ b/dexbot/strategies/external_feeds/tests/waves_test.py @@ -5,27 +5,23 @@ This is the unit test for getting external feed data from waves DEX. """ - if __name__ == '__main__': - symbol = 'BTC/USD' # quote/base for external exchanges - print(symbol, "=") - raw_pair = split_pair(symbol) - pair = [filter_bit_symbol(j) for j in [filter_prefix_symbol(i) for i in raw_pair]] - - # test symbol and pair options for getting price - pair_price = get_waves_price(pair_=pair) - if pair_price: - print("pair price ", pair_price, sep=":") - - current_price = get_waves_price(symbol_=symbol) - if current_price: - print("symbol price ", current_price, sep=":") + symbol = 'BTC/USD' # quote/base for external exchanges + print(symbol, "=") + raw_pair = split_pair(symbol) + pair = [filter_bit_symbol(j) for j in [filter_prefix_symbol(i) for i in raw_pair]] - - # get entire symbol list - print("\n") - # symbol_list = get_waves_symbols() - # print(symbol_list) + # test symbol and pair options for getting price + pair_price = get_waves_price(pair_=pair) + if pair_price: + print("pair price ", pair_price, sep=":") + current_price = get_waves_price(symbol_=symbol) + if current_price: + print("symbol price ", current_price, sep=":") + # get entire symbol list + print("\n") + # symbol_list = get_waves_symbols() + # print(symbol_list) From 4393855c733f9b490a1b183ee7a9f26ab98c71ef Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Fri, 23 Nov 2018 22:36:57 -0800 Subject: [PATCH 1029/1846] PEP8 --- .../external_feeds/tests/pywaves_test.py | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/dexbot/strategies/external_feeds/tests/pywaves_test.py b/dexbot/strategies/external_feeds/tests/pywaves_test.py index 60e8921b9..54ff6064f 100644 --- a/dexbot/strategies/external_feeds/tests/pywaves_test.py +++ b/dexbot/strategies/external_feeds/tests/pywaves_test.py @@ -1,33 +1,33 @@ import pywaves as pw + """ pywaves is an open source library for waves. While it is not as stable as REST API, we leave the test here if integration is desired for future dexbot cross-exchange strategies. """ - if __name__ == '__main__': - try: - # set the asset pair - WAVES_BTC = pw.AssetPair(pw.WAVES, pw.BTC) - - # get last price and volume - print(WAVES_BTC.last(), WAVES_BTC.volume(), sep=' ') - - # get ticker - ticker = WAVES_BTC.ticker() - print(ticker['24h_open']) - print(ticker['24h_vwap']) - - # get last 10 trades - trades = WAVES_BTC.trades(10) - for t in trades: - print(t['buyer'], t['seller'], t['price'], t['amount'], sep=' ') - - # get last 10 daily OHLCV candles - ohlcv = WAVES_BTC.candles(1440, 10) - for t in ohlcv: - print(t['open'], t['high'], t['low'], t['close'], t['volume'], sep=' ') - - except Exception as e: - print(type(e).__name__, e.args, 'Exchange Error (ignoring)') + try: + # set the asset pair + WAVES_BTC = pw.AssetPair(pw.WAVES, pw.BTC) + + # get last price and volume + print(WAVES_BTC.last(), WAVES_BTC.volume(), sep=' ') + + # get ticker + ticker = WAVES_BTC.ticker() + print(ticker['24h_open']) + print(ticker['24h_vwap']) + + # get last 10 trades + trades = WAVES_BTC.trades(10) + for t in trades: + print(t['buyer'], t['seller'], t['price'], t['amount'], sep=' ') + + # get last 10 daily OHLCV candles + ohlcv = WAVES_BTC.candles(1440, 10) + for t in ohlcv: + print(t['open'], t['high'], t['low'], t['close'], t['volume'], sep=' ') + + except Exception as e: + print(type(e).__name__, e.args, 'Exchange Error (ignoring)') From f66c21ec3f3e569bb8431240613fc37bb79357a8 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Fri, 23 Nov 2018 22:37:08 -0800 Subject: [PATCH 1030/1846] PEP8 --- dexbot/strategies/external_feeds/tests/process_pair_test.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/external_feeds/tests/process_pair_test.py b/dexbot/strategies/external_feeds/tests/process_pair_test.py index 39ae17966..59189de84 100644 --- a/dexbot/strategies/external_feeds/tests/process_pair_test.py +++ b/dexbot/strategies/external_feeds/tests/process_pair_test.py @@ -1,9 +1,11 @@ -from dexbot.strategies.external_feeds.process_pair import split_pair, get_consolidated_pair, filter_prefix_symbol, filter_bit_symbol +from dexbot.strategies.external_feeds.process_pair import split_pair, get_consolidated_pair, filter_prefix_symbol, \ + filter_bit_symbol """ This is the unit test for filters in process_pair module. """ + # Unit Tests def test_consolidated_pair(): symbol = 'STEEM:BTS' # pair = 'STEEM:BTS' or STEEM/BTS' @@ -16,7 +18,7 @@ def test_split_symbol(): try: group = ['BTC:USD', 'STEEM/USD'] pair = [split_pair(symbol) for symbol in group] - print('original:', group, 'result:', pair, sep=' ') + print('original:', group, 'result:', pair, sep=' ') except Exception as e: pass From c873d1d801197aa15ec61265da50af46b16b2d57 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Fri, 23 Nov 2018 22:37:18 -0800 Subject: [PATCH 1031/1846] PEP8 --- dexbot/strategies/external_feeds/tests/gecko_test.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/external_feeds/tests/gecko_test.py b/dexbot/strategies/external_feeds/tests/gecko_test.py index fd6b25d7b..dbbe43d16 100644 --- a/dexbot/strategies/external_feeds/tests/gecko_test.py +++ b/dexbot/strategies/external_feeds/tests/gecko_test.py @@ -21,16 +21,15 @@ def test_feed(symbol): """ [symbol] Symbol example: btc/usd or btc:usd """ - try: + try: price = get_gecko_price(symbol_=symbol) - print(price) + print(price) pair = split_pair(symbol) price = get_gecko_price(pair_=pair) except Exception as e: print_usage() print(type(e).__name__, e.args, str(e)) - + if __name__ == '__main__': main() - From 7ff0e7593ccd1e829a5a5a516ea6b5b1fd911842 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Fri, 23 Nov 2018 22:37:29 -0800 Subject: [PATCH 1032/1846] PEP8 --- .../external_feeds/tests/price_feed_test.py | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/dexbot/strategies/external_feeds/tests/price_feed_test.py b/dexbot/strategies/external_feeds/tests/price_feed_test.py index 816450da5..61ced584a 100644 --- a/dexbot/strategies/external_feeds/tests/price_feed_test.py +++ b/dexbot/strategies/external_feeds/tests/price_feed_test.py @@ -1,7 +1,6 @@ from dexbot.strategies.external_feeds.price_feed import PriceFeed from dexbot.strategies.external_feeds.process_pair import get_consolidated_pair - """ This is the unit test for testing price_feed module. Run this test first to cover everything in external feeds @@ -14,44 +13,43 @@ def test_exchanges(): center_price = None symbol = 'BTC/USD' exchanges = ['gecko', 'bitfinex', 'kraken', 'gdax', 'binance', 'waves'] - + for exchange in exchanges: pf = PriceFeed(exchange, symbol) pf.filter_symbols() center_price = pf.get_center_price(None) - print("center price: ", center_price) - if center_price is None: # try USDT + print("center price: ", center_price) + if center_price is None: # try USDT center_price = pf.get_center_price('USDT') print("try again, s/USD/USDT, center price: ", center_price) - + def test_consolidated_pair(): - symbol2 = 'STEEM/BTS' # STEEM/USD * USD/BTS = STEEM/BTS + symbol2 = 'STEEM/BTS' # STEEM/USD * USD/BTS = STEEM/BTS pf = PriceFeed('gecko', symbol2) center_price = pf.get_consolidated_price() print(center_price) - + def test_alternative_usd(): # todo - refactor price_feed to handle alt USD options. alternative_usd = ['USDT', 'USDC', 'TUSD', 'GUSD'] exchanges = ['bittrex', 'poloniex', 'gemini', 'bitfinex', 'kraken', 'binance', 'okex'] - symbol = 'BTC/USD' # replace with alt usd + symbol = 'BTC/USD' # replace with alt usd for exchange in exchanges: for alt in alternative_usd: pf = PriceFeed(exchange, symbol) center_price = pf.get_center_price(None) if center_price: - print(symbol,' using alt:', alt, center_price, "\n", sep=' ') + print(symbol, ' using alt:', alt, center_price, "\n", sep=' ') else: center_price = pf.get_center_price(alt) if center_price: - print(symbol,' using alt:', alt, center_price, "\n", sep=' ') - + print(symbol, ' using alt:', alt, center_price, "\n", sep=' ') + if __name__ == '__main__': - test_exchanges() test_consolidated_pair() test_alternative_usd() From 58c3c4b36792194d48dacca721250a8a851b077c Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Fri, 23 Nov 2018 22:37:40 -0800 Subject: [PATCH 1033/1846] PEP8 --- dexbot/strategies/external_feeds/tests/ccxt_test.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/dexbot/strategies/external_feeds/tests/ccxt_test.py b/dexbot/strategies/external_feeds/tests/ccxt_test.py index c79309b4b..adddc7261 100644 --- a/dexbot/strategies/external_feeds/tests/ccxt_test.py +++ b/dexbot/strategies/external_feeds/tests/ccxt_test.py @@ -6,23 +6,20 @@ This is the unit test for getting external feed data from CCXT using ccxt_feed module. """ - if __name__ == '__main__': - exchanges =['bitfinex', 'kraken', 'binance', 'gdax'] + exchanges = ['bitfinex', 'kraken', 'binance', 'gdax'] symbol = 'BTC/USDT' - + # testing get all pairs. for exchange_id in exchanges: print("\n\n\n") print(exchange_id) result = asyncio.get_event_loop().run_until_complete(get_ccxt_load_markets(exchange_id)) - all_symbols= [obj for obj in result] + all_symbols = [obj for obj in result] print(all_symbols) - + # testing get center price center_price = [get_ccxt_price(symbol, e) for e in exchanges] print(' exchange: ', exchanges, ' symbol: ', symbol, sep=':') print(' center_price: ', center_price) - - From 6cf6a68d996695ce70a4e64ba47ef46c848ac6fe Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Fri, 23 Nov 2018 22:38:24 -0800 Subject: [PATCH 1034/1846] PEP8 --- dexbot/strategies/external_feeds/tests/price_feed_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/external_feeds/tests/price_feed_test.py b/dexbot/strategies/external_feeds/tests/price_feed_test.py index 61ced584a..2223e7a5b 100644 --- a/dexbot/strategies/external_feeds/tests/price_feed_test.py +++ b/dexbot/strategies/external_feeds/tests/price_feed_test.py @@ -5,7 +5,7 @@ This is the unit test for testing price_feed module. Run this test first to cover everything in external feeds -Note from Marko, In DEXBot: unit of measure = BASE, asset of interest = QUOTE +In DEXBot: unit of measure = BASE, asset of interest = QUOTE """ From bd39424e097a98cc7812e05c7576cad1e159000c Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 26 Nov 2018 16:08:54 +0500 Subject: [PATCH 1035/1846] Fix 'NoneType' in allocate_asset() The error occured when bootstrap was turned off but there is no opposite orders. Add check for such situation. Bootstrap should not be turned off if there is no opposite 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 c756a9369..997f1f4e8 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -481,7 +481,8 @@ def allocate_asset(self, asset, asset_balance): if (self.bootstrapping and self.base_balance_history[2] == self.base_balance_history[0] and - self.quote_balance_history[2] == self.quote_balance_history[0]): + self.quote_balance_history[2] == self.quote_balance_history[0] and + opposite_orders): # Turn off bootstrap mode whether we're didn't allocated assets during previous 3 maintenance self.log.debug('Turning bootstrapping off: actual_spread > target_spread, we have free ' 'balances and cannot allocate them normally 3 times in a row') From 9e673c6e04c74e6fe1c5338ad827532ec204409b Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 27 Nov 2018 10:18:03 +0200 Subject: [PATCH 1036/1846] Change dexbot version number to 0.7.26 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index ecbb3eb69..9023e8369 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.25' +VERSION = '0.7.26' AUTHOR = 'Codaone Oy' __version__ = VERSION From 46a8f331bc8daea20985d02cf64e4a4830ed2f47 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Wed, 28 Nov 2018 14:05:33 +0200 Subject: [PATCH 1037/1846] Made the readme more descriptive and inviting --- README.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e7f564738..113ced2aa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,24 @@ # DEXBot -Trading Bot for the BitShares Decentralized Exchange (DEX). +A Trading Bot provided with two very flexible Market Making strategies. Works on "vanilla" BitShares and all exchanges built upon it. Can be customized with additional strategies written in Python3. + +DEXBot can be installed from source or by using the excecutable packages for Windows, OSX, and Linux. Packages include the GUI version, but installation from source provides also the CLI version, which can be used on headless servers and Raspberry Pi's. + +The provided strategies can be used to bootstrap new markets, to increase liquidity of an asset, or to try to make profits. +The _Relative Orders_ strategy is the one most think of when speaking of _Market Making_. In most markets it requires tweaking and active monitoring, and is most suitable for sideways markets or _Arbitrage Enabling_ markets (between stable or otherwise equivalent assets). _Staggered Orders_ is a "set and forget" strategy, which thrives in uncertain conditions (before price discovery or otherwise volatile conditions). It requires a long time to realize profits, but is likely to do so if it isn't touched in the mean time. It requires little monitoring and no tweaking. New markets and assets should be bootstrapped with _Staggered Orders_ and later improved with _Relative Orders_. + +**Make sure to read strategy documentation from the wiki.** [Here](https://link.medium.com/gXkfewn6XR) is a step-by-step guide to get started + +Then the most interesting question: +> Does it make profit? +If you properly predict future market conditions, you can manage to make profit. All strategies rely on assumptions. The strategies that rely on less assumptions are less risky but likely also provide less profit. During long declines the effect is decreased losses - not actual profits. + +## Getting help +Join the [Telegram Chat for DEXBot](https://t.me/DEXBOTbts). + +## Installing and running the software + +See instructions in the [Wiki](https://github.com/Codaone/DEXBot/wiki) for [Linux](https://github.com/Codaone/DEXBot/wiki/Setup-Guide-for-Linux), [Windows](https://github.com/Codaone/DEXBot/wiki/Setup-Guide-for-Windows), [OSX](https://github.com/Codaone/DEXBot/wiki/Setup-Guide-for-Mac-OS-X). [Raspberry Pi](https://github.com/Codaone/DEXBot/wiki/Setup-guide-for-Raspberry-Pi). Other users can try downloading the package or following the Linux guide. ## Build status @@ -10,9 +28,7 @@ master: **Warning**: This is highly experimental code! Use at your OWN risk! -## Installing and running the software -See instructions in the [Wiki](https://github.com/Codaone/DEXBot/wiki) for [Linux](https://github.com/Codaone/DEXBot/wiki/Setup-Guide-for-Linux), [Windows](https://github.com/Codaone/DEXBot/wiki/Setup-Guide-for-Windows), [OSX](https://github.com/Codaone/DEXBot/wiki/Setup-Guide-for-Mac-OS-X). [Raspberry Pi](https://github.com/Codaone/DEXBot/wiki/Setup-guide-for-Raspberry-Pi). Other users can try downloading the package or following the Linux guide. ## Contributing From 9e9424387f74cccc50bfe69e9aac7299f2ffa079 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Wed, 28 Nov 2018 14:09:22 +0200 Subject: [PATCH 1038/1846] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 113ced2aa..a2eef5eb8 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,7 @@ The _Relative Orders_ strategy is the one most think of when speaking of _Market **Make sure to read strategy documentation from the wiki.** [Here](https://link.medium.com/gXkfewn6XR) is a step-by-step guide to get started -Then the most interesting question: -> Does it make profit? +## Does it make profit? If you properly predict future market conditions, you can manage to make profit. All strategies rely on assumptions. The strategies that rely on less assumptions are less risky but likely also provide less profit. During long declines the effect is decreased losses - not actual profits. ## Getting help From 9f62637b73a8dfba709a0aa0ea67aef19caedfb6 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Wed, 28 Nov 2018 14:21:18 +0200 Subject: [PATCH 1039/1846] Added pictures --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a2eef5eb8..4960c7639 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # DEXBot +![GUI](https://i.imgur.com/rW8XKQ4.png)The Dashboard of the GUI version of DEXBot + +![CLI](https://i.imgur.com/H1N96nI.png)The CLI version of DEXBot in configuration dialog + A Trading Bot provided with two very flexible Market Making strategies. Works on "vanilla" BitShares and all exchanges built upon it. Can be customized with additional strategies written in Python3. DEXBot can be installed from source or by using the excecutable packages for Windows, OSX, and Linux. Packages include the GUI version, but installation from source provides also the CLI version, which can be used on headless servers and Raspberry Pi's. @@ -10,7 +14,7 @@ The _Relative Orders_ strategy is the one most think of when speaking of _Market **Make sure to read strategy documentation from the wiki.** [Here](https://link.medium.com/gXkfewn6XR) is a step-by-step guide to get started ## Does it make profit? -If you properly predict future market conditions, you can manage to make profit. All strategies rely on assumptions. The strategies that rely on less assumptions are less risky but likely also provide less profit. During long declines the effect is decreased losses - not actual profits. +If you properly predict future market conditions, you can manage to make profit. All strategies rely on assumptions. The strategies that rely on less assumptions are less risky, and more risky strategies _can_ make more profit. During long declines the effect is decreased losses - not actual profits. So we can only say that it can make profit, without forgetting that it can also make losses. Good luck. ## Getting help Join the [Telegram Chat for DEXBot](https://t.me/DEXBOTbts). From 236703105854c725eed33607c20ee69764fa9191 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Wed, 28 Nov 2018 15:29:01 +0200 Subject: [PATCH 1040/1846] Added some text --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4960c7639..f95303cc5 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ A Trading Bot provided with two very flexible Market Making strategies. Works on "vanilla" BitShares and all exchanges built upon it. Can be customized with additional strategies written in Python3. +DEXBot was paid for by the BitShares blockchain (by means of a Worker Proposal), and managed by "The Cabinet", consisting of 6 active BitShares community members. All spending was controlled by an account which requires 3/5 approvals (multisig scheme). + DEXBot can be installed from source or by using the excecutable packages for Windows, OSX, and Linux. Packages include the GUI version, but installation from source provides also the CLI version, which can be used on headless servers and Raspberry Pi's. The provided strategies can be used to bootstrap new markets, to increase liquidity of an asset, or to try to make profits. From 0dc56dcada962a49fc3bee94c225251953d60f2d Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 23 Nov 2018 21:49:38 +0500 Subject: [PATCH 1041/1846] Refactor partially filled orders handling Replace closest partially filled orders only when excess funds was allocated. --- dexbot/strategies/staggered_orders.py | 67 ++++++++++++++++++++------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 997f1f4e8..f0e2305e4 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -430,7 +430,9 @@ def allocate_asset(self, asset, asset_balance): own_orders = [] own_threshold = 0 own_symbol = '' + own_precision = 0 opposite_symbol = '' + increase_finished = False if asset == 'base': order_type = 'buy' @@ -439,6 +441,7 @@ def allocate_asset(self, asset, asset_balance): own_orders = self.buy_orders opposite_orders = self.sell_orders own_threshold = self.base_asset_threshold + own_precision = self.market['base']['precision'] elif asset == 'quote': order_type = 'sell' own_symbol = self.quote_balance['symbol'] @@ -446,6 +449,7 @@ def allocate_asset(self, asset, asset_balance): own_orders = self.sell_orders opposite_orders = self.buy_orders own_threshold = self.quote_asset_threshold + own_precision = self.market['quote']['precision'] if own_orders: # Get currently the furthest and closest orders @@ -530,19 +534,22 @@ def allocate_asset(self, asset, asset_balance): order_type, opposite_asset_limit, opposite_symbol)) self.place_closer_order(asset, closest_own_order, own_asset_limit=own_asset_limit, opposite_asset_limit=opposite_asset_limit, allow_partial=False) - elif not self.check_partial_fill(closest_own_order) and not self.check_partial_fill(closest_opposite_order): - """ Replace closest own order if `fill % > default threshold` and only when opposite order is also - partially filled. This would prevent an abuse case when we are operationg on inactive market. An - attacker can massively dump the price and then he can buy back the asset cheaper. Only applicable - when sides are massively imbalanced. Though in the normal market a similar condition may happen - naturally on significant price drops. This check helps to redistribute funds more smoothly. - """ - self.replace_partially_filled_order(closest_own_order) - return + else: + self.log.warning('Boostrap is off, but there is no opposite orders, not placing closer order') elif not opposite_orders: # Do not try to do anything than placing closer order whether there is no opposite orders return else: + # Target spread is reached, let's allocate remaining funds + if not self.check_partial_fill(closest_own_order, fill_threshold=0): + """ Detect partially filled order on the own side and reserve funds to replace order in case + opposite oreder will be fully filled. + """ + funds_to_reserve = closest_own_order['base']['amount'] + self.log.debug('Partially filled order on own side, reserving funds to replace: ' + '{:.{prec}f} {}'.format(funds_to_reserve, own_symbol, prec=own_precision)) + asset_balance -= funds_to_reserve + if not self.check_partial_fill(closest_opposite_order, fill_threshold=0): """ Detect partially filled order on the opposite side and reserve appropriate amount to place closer order. We adding some additional reserve to be able to place next order whether @@ -557,9 +564,12 @@ def allocate_asset(self, asset, asset_balance): elif asset == 'quote': funds_to_reserve = closer_own_order['amount'] * additional_reserve self.log.debug('Partially filled order on opposite side, reserving funds for next {} order: ' - '{:.8f} {}'.format(order_type, funds_to_reserve, own_symbol)) + '{:.{prec}f} {}'.format(order_type, funds_to_reserve, own_symbol, + prec=own_precision)) asset_balance -= funds_to_reserve + if asset_balance > own_threshold: + # Allocate excess funds if ((asset == 'base' and furthest_own_order_price / (1 + self.increment) < self.lower_bound) or (asset == 'quote' and furthest_own_order_price * @@ -567,12 +577,32 @@ def allocate_asset(self, asset, asset_balance): # Lower/upper bound has been reached and now will start allocating rest of the balance. self.bootstrapping = False self.log.debug('Increasing sizes of {} orders'.format(order_type)) - self.increase_order_sizes(asset, asset_balance, own_orders) + increase_finished = self.increase_order_sizes(asset, asset_balance, own_orders) else: # Range bound is not reached, we need to add additional orders at the extremes self.bootstrapping = False self.log.debug('Placing further order than current furthest {} order'.format(order_type)) self.place_further_order(asset, furthest_own_order, allow_partial=True) + else: + increase_finished = True + + if (increase_finished and not self.check_partial_fill(closest_own_order) + and not self.check_partial_fill(closest_opposite_order, fill_threshold=0)): + """ Replace partially filled closest orders only when allocation of excess funds was finished. This + would prevent an abuse case when we are operating inactive market. An attacker can massively dump + the price and then he can buy back the asset cheaper. Similar case may happen on the "normal" market + on significant price drops or spikes. + + The logic how it works is following: + 1. If we have partially filled closest orders, reserve fuds to replace them later + 2. If we have excess funds, allocate them by increasing order sizes or expand bounds if needed + 3. When increase is finished, replace partially filled closest orders + + Thus we are don't need to precisely count how much was filled on closest orders. + """ + # Refresh balances to make "reserved" funds available + self.refresh_balances(use_cached_orders=True) + self.replace_partially_filled_order(closest_own_order) else: # Place first buy order as close to the lower bound as possible self.bootstrapping = True @@ -614,7 +644,8 @@ def increase_order_sizes(self, asset, asset_balance, orders): :param str | asset: 'base' or 'quote', depending if checking sell or buy :param Amount | asset_balance: Balance of the account :param list | orders: List of buy or sell orders - :return None + :return boot | True = all available funds was allocated + False = not all funds was allocated, can increase more orders next time """ total_balance = 0 order_type = '' @@ -716,7 +747,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): # Balance should be enough to replace partially filled order self.log.debug('Not enough balance to increase {} order at price {:.8f}' .format(order_type, price)) - return + return True self.log.info('Increasing {} order at price {:.8f} from {:.{prec}f} to {:.{prec}f} {}' .format(order_type, price, order_amount, new_order_amount, symbol, prec=precision)) @@ -728,7 +759,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): elif asset == 'base': self.place_market_buy_order(quote_amount, price) # Only one increase at a time. This prevents running more than one increment round simultaneously - return + return False elif (self.mode == 'valley' or (self.mode == 'buy_slope' and asset == 'base') or (self.mode == 'sell_slope' and asset == 'quote')): @@ -831,7 +862,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): # Balance should be enough to replace partially filled order self.log.debug('Not enough balance to increase {} order at price {:.8f}' .format(order_type, price)) - return + return True self.log.info('Increasing {} order at price {:.8f} from {:.{prec}f} to {:.{prec}f} {}' .format(order_type, price, order_amount, new_order_amount, symbol, prec=precision)) @@ -843,7 +874,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): elif asset == 'base': self.place_market_buy_order(quote_amount, price) # One increase at a time. This prevents running more than one increment round simultaneously. - return + return False elif self.mode == 'neutral': """ Starting from the furthest order, for each order, see if it is approximately @@ -941,7 +972,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): # Balance should be enough to replace partially filled order self.log.debug('Not enough balance to increase {} order at price {:.8f}' .format(order_type, price)) - return + return True self.log.info('Increasing {} order at price {:.8f} from {:.{prec}f} to {:.{prec}f} {}' .format(order_type, price, order_amount, new_order_amount, symbol, prec=precision)) @@ -954,7 +985,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): elif asset == 'base': self.place_market_buy_order(quote_amount, price) # One increase at a time. This prevents running more than one increment round simultaneously. - return + return False return None From f02bf766862e3c7dbbb8aa52914899bb486aec00 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 24 Nov 2018 15:27:46 +0500 Subject: [PATCH 1042/1846] Update debug message --- dexbot/strategies/staggered_orders.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index f0e2305e4..f6413941f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1025,9 +1025,11 @@ def replace_partially_filled_order(self, order): if order['base']['symbol'] == self.market['base']['symbol']: asset_balance = self.base_balance order_type = 'buy' + precision = self.market['base']['precision'] else: asset_balance = self.quote_balance order_type = 'sell' + precision = self.market['quote']['precision'] # Make sure we have enough balance to replace partially filled order if asset_balance + order['for_sale']['amount'] >= order['base']['amount']: @@ -1042,8 +1044,10 @@ def replace_partially_filled_order(self, order): if self.returnOrderId: self.refresh_balances(total_balances=False) else: - self.log.debug('Not replacing partially filled {} order because there is not enough funds' - .format(order_type)) + needed = order['base']['amount'] - order['for_sale']['amount'] + self.log.debug('Unable to replace partially filled {} order: avail/needed: {:.{prec}f}/{:.{prec}f} {}' + .format(order_type, asset_balance['amount'], needed, order['base']['symbol'], + prec=precision)) def place_closer_order(self, asset, order, place_order=True, allow_partial=False, own_asset_limit=None, opposite_asset_limit=None): From 2d251aa6c58053bbafac1e006cfd75e5d59556d1 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 24 Nov 2018 23:34:42 +0500 Subject: [PATCH 1043/1846] Allow to override returnOrderId via kwarg --- dexbot/strategies/base.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 17efede38..2d922d868 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1022,6 +1022,7 @@ def place_market_buy_order(self, amount, price, return_none=False, *args, **kwar symbol = self.market['base']['symbol'] precision = self.market['base']['precision'] base_amount = truncate(price * amount, precision) + return_order_id = kwargs.pop('returnOrderId', self.returnOrderId) # Don't try to place an order of size 0 if not base_amount: @@ -1030,7 +1031,7 @@ def place_market_buy_order(self, amount, price, return_none=False, *args, **kwar return None # Make sure we have enough balance for the order - if self.returnOrderId and self.balance(self.market['base']) < base_amount: + if return_order_id and self.balance(self.market['base']) < base_amount: self.log.critical("Insufficient buy balance, needed {} {}".format(base_amount, symbol)) self.disabled = True return None @@ -1045,14 +1046,14 @@ def place_market_buy_order(self, amount, price, return_none=False, *args, **kwar Amount(amount=amount, asset=self.market["quote"]), account=self.account.name, expiration=self.expiration, - returnOrderId=self.returnOrderId, + returnOrderId=return_order_id, fee_asset=self.fee_asset['id'], *args, **kwargs ) self.log.debug('Placed buy order {}'.format(buy_transaction)) - if self.returnOrderId: + if return_order_id: buy_order = self.get_order(buy_transaction['orderid'], return_none=return_none) if buy_order and buy_order['deleted']: # The API doesn't return data on orders that don't exist @@ -1076,6 +1077,7 @@ def place_market_sell_order(self, amount, price, return_none=False, *args, **kwa symbol = self.market['quote']['symbol'] precision = self.market['quote']['precision'] quote_amount = truncate(amount, precision) + return_order_id = kwargs.pop('returnOrderId', self.returnOrderId) # Don't try to place an order of size 0 if not quote_amount: @@ -1084,7 +1086,7 @@ def place_market_sell_order(self, amount, price, return_none=False, *args, **kwa return None # Make sure we have enough balance for the order - if self.returnOrderId and self.balance(self.market['quote']) < quote_amount: + if return_order_id and self.balance(self.market['quote']) < quote_amount: self.log.critical("Insufficient sell balance, needed {} {}".format(amount, symbol)) self.disabled = True return None @@ -1099,14 +1101,14 @@ def place_market_sell_order(self, amount, price, return_none=False, *args, **kwa Amount(amount=amount, asset=self.market["quote"]), account=self.account.name, expiration=self.expiration, - returnOrderId=self.returnOrderId, + returnOrderId=return_order_id, fee_asset=self.fee_asset['id'], *args, **kwargs ) self.log.debug('Placed sell order {}'.format(sell_transaction)) - if self.returnOrderId: + if return_order_id: sell_order = self.get_order(sell_transaction['orderid'], return_none=return_none) if sell_order and sell_order['deleted']: # The API doesn't return data on orders that don't exist, we need to calculate the data on our own From c6fcb014f1794f18569a8a8f91e127ace8273fb3 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 25 Nov 2018 23:31:33 +0500 Subject: [PATCH 1044/1846] Adjust precision in log message --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index f6413941f..28147b058 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -389,14 +389,14 @@ def remove_outside_orders(self, sell_orders, buy_orders): for order in sell_orders: order_price = order['price'] ** -1 if order_price > self.upper_bound: - self.log.info('Cancelling sell order outside range: {}'.format(order_price)) + self.log.info('Cancelling sell order outside range: {:.8f}'.format(order_price)) orders_to_cancel.append(order) # Remove buy orders that exceed boundaries for order in buy_orders: order_price = order['price'] if order_price < self.lower_bound: - self.log.info('Cancelling buy order outside range: {}'.format(order_price)) + self.log.info('Cancelling buy order outside range: {:.8f}'.format(order_price)) orders_to_cancel.append(order) if orders_to_cancel: From f2a495a86980504bdeb89157b9f4e430e6e1b5cd Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 24 Nov 2018 23:52:15 +0500 Subject: [PATCH 1045/1846] Implement depth limit for Staggered Orders Operational depth limit allows to keep only N buy or sell orders and adding more orders if needed (price movements). Reasons for having depth limit: * Decrease strategy maintenance time * Reduce blockchain overhead by decreasing transactions count * Increase initial order placement time Closes: #386 --- dexbot/strategies/staggered_orders.py | 429 ++++++++++++++++++++++---- 1 file changed, 373 insertions(+), 56 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 28147b058..573e7b9e7 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -3,7 +3,9 @@ import traceback import bitsharesapi.exceptions from datetime import datetime, timedelta +from functools import reduce from bitshares.dex import Dex +from bitshares.amount import Amount from dexbot.strategies.base import StrategyBase, ConfigElement, DetailElement from dexbot.qt_queue.idle_queue import idle_add @@ -69,7 +71,10 @@ def configure(cls, return_base_config=True): (0, 1000000000, 8, '')), ConfigElement( 'instant_fill', 'bool', True, 'Allow instant fill', - 'Allow to execute orders by market', None) + 'Allow to execute orders by market', None), + ConfigElement( + 'operational_depth', 'int', 10, 'Operational depth', + 'Order depth to maintain on books', (2, None, None)) ] @classmethod @@ -102,6 +107,7 @@ def __init__(self, *args, **kwargs): self.partial_fill_threshold = 0.15 self.is_instant_fill_enabled = self.worker.get('instant_fill', True) self.is_center_price_dynamic = self.worker['center_price_dynamic'] + self.operational_depth = self.worker.get('operational_depth', 6) if self.is_center_price_dynamic: self.center_price = None @@ -112,12 +118,22 @@ def __init__(self, *args, **kwargs): self.log.error('Spread is more than increment, refusing to work because worker will make losses') self.disabled = True + if self.operational_depth < 2: + self.log.error('Operational depth should be at least 2 orders') + self.disabled = True + # Strategy variables # Assume we are in bootstrap mode by default. This prevents weird things when bootstrap was interrupted self.bootstrapping = True self.market_center_price = None self.buy_orders = [] self.sell_orders = [] + self.real_buy_orders = [] + self.real_sell_orders = [] + self.virtual_orders = [] + self.virtual_buy_orders = [] + self.virtual_sell_orders = [] + self.virtual_orders_restored = False self.actual_spread = self.target_spread + 1 self.quote_total_balance = 0 self.base_total_balance = 0 @@ -197,6 +213,49 @@ def maintain_strategy(self, *args, **kwargs): self.log_maintenance_time() return + # Restore virtual orders on startup if needed + if not self.virtual_orders_restored: + self.restore_virtual_orders() + + if self.virtual_orders_restored: + self.log.info('Virtual orders restored') + self.log_maintenance_time() + return + + # Replace excessive real orders with virtual ones, buy side + if (self.real_buy_orders and len(self.real_buy_orders) > self.operational_depth + 5 and + self.real_buy_orders[-1]['base']['amount'] == self.real_buy_orders[-2]['base']['amount']): + # Note: replace should happen only if next order is same-sized. Otherwise it will break proper allocation + self.replace_real_order_with_virtual(self.real_buy_orders[-1]) + + # Replace excessive real orders with virtual ones, sell side + if (self.real_sell_orders and len(self.real_sell_orders) > self.operational_depth + 5 and + self.real_sell_orders[-1]['base']['amount'] == self.real_sell_orders[-2]['base']['amount']): + self.replace_real_order_with_virtual(self.real_sell_orders[-1]) + + # Check for operational depth, buy side + if (self.virtual_buy_orders and + len(self.real_buy_orders) < self.operational_depth and + not self.bootstrapping): + """ + Note: if boostrap is on and there is nothing to allocate, this check would not work until some orders + will be filled. This means that changing `operational_depth` config param will not work immediately. + + We need to wait until bootstrap is off because during initial orders placement this would start to place + real orders without waiting until all range will be covered. + """ + self.replace_virtual_order_with_real(self.virtual_buy_orders[0]) + self.log_maintenance_time() + return + + # Check for operational depth, sell side + if (self.virtual_sell_orders and + len(self.real_sell_orders) < self.operational_depth and + not self.bootstrapping): + self.replace_virtual_order_with_real(self.virtual_sell_orders[0]) + self.log_maintenance_time() + return + # Prepare to bundle operations into single transaction self.bitshares.bundle = True @@ -315,11 +374,11 @@ def maintain_strategy(self, *args, **kwargs): if side_to_cancel == 'buy': self.log.info('Free balances are not changing, bootstrap is off and target spread is not reached. ' 'Cancelling lowest buy order as a fallback') - self.cancel(self.buy_orders[-1]) + self.cancel_orders_wrapper(self.buy_orders[-1]) elif side_to_cancel == 'sell': self.log.info('Free balances are not changing, bootstrap is off and target spread is not reached. ' 'Cancelling highest sell order as a fallback') - self.cancel(self.sell_orders[-1]) + self.cancel_orders_wrapper(self.sell_orders[-1]) else: self.log.info('Target spread is not reached but cannot determine what furthest order to cancel') @@ -337,6 +396,9 @@ def refresh_balances(self, total_balances=True, use_cached_orders=False): :param bool | total_balances: refresh total balance or skip it :param bool | use_cached_orders: when calculating orders balance, use cached orders from self.cached_orders """ + virtual_orders_base_balance = 0 + virtual_orders_quote_balance = 0 + # Get current account balances account_balances = self.count_asset(order_ids=[], return_asset=True) @@ -353,7 +415,17 @@ def refresh_balances(self, total_balances=True, use_cached_orders=False): elif self.fee_asset['id'] == self.market['quote']['id']: self.quote_balance['amount'] = self.quote_balance['amount'] - fee_reserve + # Exclude balances allocated into virtual orders + if self.virtual_orders: + buy_orders = self.filter_buy_orders(self.virtual_orders) + sell_orders = self.filter_sell_orders(self.virtual_orders, invert=False) + virtual_orders_base_balance = reduce((lambda x, order: x + order['base']['amount']), buy_orders, 0) + virtual_orders_quote_balance = reduce((lambda x, order: x + order['base']['amount']), sell_orders, 0) + self.base_balance['amount'] -= virtual_orders_base_balance + self.quote_balance['amount'] -= virtual_orders_quote_balance + if not total_balances: + # Caller doesn't interesting in balances of real orders return # Balance per asset from orders @@ -365,8 +437,8 @@ def refresh_balances(self, total_balances=True, use_cached_orders=False): orders_balance = self.get_allocated_assets(order_ids) # Total balance per asset (orders balance and available balance) - self.quote_total_balance = orders_balance['quote'] + self.quote_balance['amount'] - self.base_total_balance = orders_balance['base'] + self.base_balance['amount'] + self.quote_total_balance = orders_balance['quote'] + self.quote_balance['amount'] + virtual_orders_quote_balance + self.base_total_balance = orders_balance['base'] + self.base_balance['amount'] + virtual_orders_base_balance def refresh_orders(self): """ Updates buy and sell orders @@ -374,6 +446,17 @@ def refresh_orders(self): orders = self.get_own_orders self.cached_orders = orders + # Sort virtual orders + self.virtual_buy_orders = self.filter_buy_orders(self.virtual_orders, sort='DESC') + self.virtual_sell_orders = self.filter_sell_orders(self.virtual_orders, sort='DESC', invert=False) + + # Sort real orders + self.real_buy_orders = self.filter_buy_orders(orders, sort='DESC') + self.real_sell_orders = self.filter_sell_orders(orders, sort='DESC', invert=False) + + # Concatenate orders and virtual_orders + orders = orders + self.virtual_orders + # Sort orders so that order with index 0 is closest to the center price and -1 is furthers self.buy_orders = self.filter_buy_orders(orders, sort='DESC') self.sell_orders = self.filter_sell_orders(orders, sort='DESC', invert=False) @@ -401,7 +484,7 @@ def remove_outside_orders(self, sell_orders, buy_orders): if orders_to_cancel: # We are trying to cancel all orders in one try - success = self.cancel(orders_to_cancel, batch_only=True) + success = self.cancel_orders_wrapper(orders_to_cancel, batch_only=True) # Refresh orders to prevent orders outside boundaries being in the future comparisons self.refresh_orders() # Batch cancel failed, repeat cancelling only one order @@ -409,12 +492,88 @@ def remove_outside_orders(self, sell_orders, buy_orders): return True else: self.log.debug('Batch cancel failed, failing back to cancelling single order') - self.cancel(orders_to_cancel[0]) + self.cancel_orders_wrapper(orders_to_cancel[0]) # To avoid GUI hanging cancel only one order and let switch to another worker return False return True + def restore_virtual_orders(self): + """ Create virtual further orders in batch manner. This helps to place further orders quickly on startup. + """ + if self.buy_orders: + furthest_order = self.real_buy_orders[-1] + while furthest_order['price'] > self.lower_bound * (1 + self.increment): + furthest_order = self.place_further_order('base', furthest_order, virtual=True) + if not isinstance(furthest_order, VirtualOrder): + # Failed to place order + break + self.virtual_orders_restored = True + + if self.sell_orders: + furthest_order = self.real_sell_orders[-1] + while furthest_order['price'] ** -1 < self.upper_bound / (1 + self.increment): + furthest_order = self.place_further_order('quote', furthest_order, virtual=True) + if not isinstance(furthest_order, VirtualOrder): + # Failed to place order + break + self.virtual_orders_restored = True + + def replace_real_order_with_virtual(self, order): + """ Replace real limit order with virtual order + + :param Order | order: market order to replace + :return bool | True = order replace success + False = order replace failed + + Logic: + 1. Cancel real order + 2. Wait until transaction included in head block + 3. Place virtual order + """ + success = self.cancel_orders(order) + if success and order['base']['symbol'] == self.market['base']['symbol']: + quote_amount = order['quote']['amount'] + price = order['price'] + self.log.info('Replacing real buy order with virtual') + self.place_virtual_buy_order(quote_amount, price) + elif success and order['base']['symbol'] == self.market['quote']['symbol']: + quote_amount = order['base']['amount'] + price = order['price'] ** -1 + self.log.info('Replacing real sell order with virtual') + self.place_virtual_sell_order(quote_amount, price) + else: + return False + + def replace_virtual_order_with_real(self, order): + """ Replace virtual order with real one + + :param Order | order: market order to replace + :return bool | True = order replace success + False = order replace failed + + Logic: + 1. Place real order instead of virtual + 2. Wait until transaction included in head block + 3. Remove existing virtual order + """ + if order['base']['symbol'] == self.market['base']['symbol']: + quote_amount = order['quote']['amount'] + price = order['price'] + self.log.info('Replacing virtual buy order with real order') + new_order = self.place_market_buy_order(quote_amount, price, returnOrderId=True) + else: + quote_amount = order['base']['amount'] + price = order['price'] ** -1 + self.log.info('Replacing virtual sell order with real order') + new_order = self.place_market_sell_order(quote_amount, price, returnOrderId=True) + + if new_order: + # Cancel virtual order + self.cancel_orders_wrapper(order) + return True + return False + def allocate_asset(self, asset, asset_balance): """ Allocates available asset balance as buy or sell orders. @@ -608,9 +767,13 @@ def allocate_asset(self, asset, asset_balance): self.bootstrapping = True self.log.debug('Placing first {} order'.format(order_type)) if asset == 'base': - self.place_lowest_buy_order(asset_balance) + order = self.place_lowest_buy_order(asset_balance) elif asset == 'quote': - self.place_highest_sell_order(asset_balance) + order = self.place_highest_sell_order(asset_balance) + + # Place all virtual orders at once + while isinstance(order, VirtualOrder): + order = self.place_closer_order(asset, order) # Get latest orders only when we are not bundling operations if self.returnOrderId: @@ -644,7 +807,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): :param str | asset: 'base' or 'quote', depending if checking sell or buy :param Amount | asset_balance: Balance of the account :param list | orders: List of buy or sell orders - :return boot | True = all available funds was allocated + :return bool | True = all available funds was allocated False = not all funds was allocated, can increase more orders next time """ total_balance = 0 @@ -753,11 +916,18 @@ def increase_order_sizes(self, asset, asset_balance, orders): .format(order_type, price, order_amount, new_order_amount, symbol, prec=precision)) self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}, amount: {}, price: {:.8f}' .format(order_type, self.mode, order_amount, price)) - self.cancel(order) + self.cancel_orders_wrapper(order) if asset == 'quote': - self.place_market_sell_order(quote_amount, price) + if isinstance(order, VirtualOrder): + self.place_virtual_sell_order(quote_amount, price) + else: + self.place_market_sell_order(quote_amount, price) elif asset == 'base': - self.place_market_buy_order(quote_amount, price) + if isinstance(order, VirtualOrder): + self.place_virtual_buy_order(quote_amount, price) + else: + self.place_market_buy_order(quote_amount, price) + # Only one increase at a time. This prevents running more than one increment round simultaneously return False elif (self.mode == 'valley' or @@ -868,11 +1038,18 @@ def increase_order_sizes(self, asset, asset_balance, orders): .format(order_type, price, order_amount, new_order_amount, symbol, prec=precision)) self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}, amount: {}, price: {:.8f}' .format(order_type, self.mode, order_amount, price)) - self.cancel(order) + self.cancel_orders_wrapper(order) if asset == 'quote': - self.place_market_sell_order(quote_amount, price) + if isinstance(order, VirtualOrder): + self.place_virtual_sell_order(quote_amount, price) + else: + self.place_market_sell_order(quote_amount, price) elif asset == 'base': - self.place_market_buy_order(quote_amount, price) + if isinstance(order, VirtualOrder): + self.place_virtual_buy_order(quote_amount, price) + else: + self.place_market_buy_order(quote_amount, price) + # One increase at a time. This prevents running more than one increment round simultaneously. return False @@ -979,11 +1156,18 @@ def increase_order_sizes(self, asset, asset_balance, orders): self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}' ', amount: {:.8f}, price: {:.8f}' .format(order_type, self.mode, order_amount, price)) - self.cancel(order) + self.cancel_orders_wrapper(order) if asset == 'quote': - self.place_market_sell_order(quote_amount, price) + if isinstance(order, VirtualOrder): + self.place_virtual_sell_order(quote_amount, price) + else: + self.place_market_sell_order(quote_amount, price) elif asset == 'base': - self.place_market_buy_order(quote_amount, price) + if isinstance(order, VirtualOrder): + self.place_virtual_buy_order(quote_amount, price) + else: + self.place_market_buy_order(quote_amount, price) + # One increase at a time. This prevents running more than one increment round simultaneously. return False @@ -1035,7 +1219,7 @@ def replace_partially_filled_order(self, order): if asset_balance + order['for_sale']['amount'] >= order['base']['amount']: # Cancel closest order and immediately replace it with new one. self.log.info('Replacing partially filled {} order'.format(order_type)) - self.cancel(order) + self.cancel_orders_wrapper(order) if order_type == 'buy': self.place_market_buy_order(order['quote']['amount'], order['price']) elif order_type == 'sell': @@ -1066,12 +1250,13 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False self.disabled = True return None - # Define asset-dependent variables balance = 0 order_type = '' quote_amount = 0 symbol = '' + new_order = None + # Define asset-dependent variables if asset == 'base': order_type = 'buy' balance = self.base_balance['amount'] @@ -1148,28 +1333,48 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False quote_amount = balance if place_order and asset == 'base': - self.log.info('Placing closer buy order') - self.place_market_buy_order(quote_amount, price) + virtual_bound = self.market_center_price / math.sqrt(1 + self.target_spread) + orders_count = self.calc_buy_orders_count(virtual_bound, price) + print(orders_count) + if orders_count > self.operational_depth and isinstance(order, VirtualOrder): + # Allow to place closer order only if current is virtual + + self.log.info('Placing virtual closer buy order') + new_order = self.place_virtual_buy_order(quote_amount, price) + else: + self.log.info('Placing closer buy order') + new_order = self.place_market_buy_order(quote_amount, price) elif place_order and asset == 'quote': - self.log.info('Placing closer sell order') - self.place_market_sell_order(quote_amount, price) + virtual_bound = self.market_center_price * math.sqrt(1 + self.target_spread) + orders_count = self.calc_sell_orders_count(virtual_bound, price) + print(orders_count) + if orders_count > self.operational_depth and isinstance(order, VirtualOrder): + self.log.info('Placing virtual closer sell order') + new_order = self.place_virtual_sell_order(quote_amount, price) + else: + self.log.info('Placing closer sell order') + new_order = self.place_market_sell_order(quote_amount, price) + else: + new_order = {"amount": quote_amount, "price": price} - return {"amount": quote_amount, "price": price} + return new_order - def place_further_order(self, asset, order, place_order=True, allow_partial=False): + def place_further_order(self, asset, order, place_order=True, allow_partial=False, virtual=False): """ Place order further from specified order :param asset: :param order: furthest buy or sell order :param bool | place_order: True = Places order to the market, False = returns amount and price :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance + :param bool | virtual: True = Force place a virtual order """ - - # Define asset-dependent variables balance = 0 order_type = '' symbol = '' + new_order = None + virtual_bound = self.market_center_price / math.sqrt(1 + self.target_spread) + # Define asset-dependent variables if asset == 'base': order_type = 'buy' balance = self.base_balance['amount'] @@ -1195,7 +1400,7 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals own_asset_amount = order['base']['amount'] opposite_asset_amount = own_asset_amount / price elif self.mode == 'neutral': - own_asset_amount = order['base']['amount'] / math.sqrt(1 + self.increment) + giown_asset_amount = order['base']['amount'] / math.sqrt(1 + self.increment) opposite_asset_amount = own_asset_amount / price limiter = 0 @@ -1213,7 +1418,7 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals # Check whether new order will exceed available balance if balance < limiter: if place_order and not allow_partial: - self.log.debug('Not enough balance to place furthest {} order; need/avail: {:.8f}/{:.8f}' + self.log.debug('Not enough balance to place further {} order; need/avail: {:.8f}/{:.8f}' .format(order_type, limiter, balance)) place_order = False elif allow_partial: @@ -1225,13 +1430,25 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals quote_amount = balance if place_order and asset == 'base': - self.log.info('Placing further buy order') - self.place_market_buy_order(quote_amount, price) + orders_count = self.calc_buy_orders_count(virtual_bound, price) + if orders_count > self.operational_depth or virtual: + self.log.info('Placing virtual further buy order') + new_order = self.place_virtual_buy_order(quote_amount, price) + else: + self.log.info('Placing further buy order') + new_order = self.place_market_buy_order(quote_amount, price) elif place_order and asset == 'quote': - self.log.info('Placing further sell order') - self.place_market_sell_order(quote_amount, price) + orders_count = self.calc_sell_orders_count(virtual_bound, price) + if orders_count > self.operational_depth or virtual: + self.log.info('Placing virtual further sell order') + new_order = self.place_virtual_sell_order(quote_amount, price) + else: + self.log.info('Placing further sell order') + new_order = self.place_market_sell_order(quote_amount, price) + else: + new_order = {"amount": quote_amount, "price": price} - return {"amount": quote_amount, "price": price} + return new_order def place_highest_sell_order(self, quote_balance, place_order=True, market_center_price=None): """ Places sell order furthest to the market center price @@ -1253,12 +1470,15 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente .format(market_center_price, price, self.upper_bound)) return + sell_orders_count = self.calc_sell_orders_count(price, self.upper_bound) + if self.fee_asset['id'] == self.market['quote']['id']: + buy_orders_count = self.calc_buy_orders_count(price, self.lower_bound) fee = self.get_order_creation_fee(self.fee_asset) - buy_orders_count = self.calc_buy_orders_count(price=price) - sell_orders_count = self.calc_sell_orders_count(price=price) + real_orders_count = min(buy_orders_count, self.operational_depth) + min(sell_orders_count, + self.operational_depth) # Exclude all further fees from avail balance - quote_balance = quote_balance - fee * (buy_orders_count + sell_orders_count) + quote_balance = quote_balance - fee * real_orders_count # Initialize local variables amount_quote = 0 @@ -1308,9 +1528,14 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) if place_order: - self.place_market_sell_order(amount_quote, price) + if sell_orders_count > self.operational_depth: + order = self.place_virtual_sell_order(amount_quote, price) + else: + order = self.place_market_sell_order(amount_quote, price) else: - return {"amount": amount_quote, "price": price} + order = {"amount": amount_quote, "price": price} + + return order def place_lowest_buy_order(self, base_balance, place_order=True, market_center_price=None): """ Places buy order furthest to the market center price @@ -1362,12 +1587,15 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p .format(market_center_price, price, self.lower_bound)) return + buy_orders_count = self.calc_buy_orders_count(price, self.lower_bound) + if self.fee_asset['id'] == self.market['base']['id']: fee = self.get_order_creation_fee(self.fee_asset) - buy_orders_count = self.calc_buy_orders_count(price=price) - sell_orders_count = self.calc_sell_orders_count(price=price) + sell_orders_count = self.calc_sell_orders_count(price, self.upper_bound) + real_orders_count = min(buy_orders_count, self.operational_depth) + min(sell_orders_count, + self.operational_depth) # Exclude all further fees from avail balance - base_balance = base_balance - fee * (buy_orders_count + sell_orders_count) + base_balance = base_balance - fee * real_orders_count # Initialize local variables amount_quote = 0 @@ -1420,34 +1648,117 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) if place_order: - self.place_market_buy_order(amount_quote, price) + if buy_orders_count > self.operational_depth: + order = self.place_virtual_buy_order(amount_quote, price) + else: + order = self.place_market_buy_order(amount_quote, price) else: - return {"amount": amount_quote, "price": price} + order = {"amount": amount_quote, "price": price} + + return order - def calc_buy_orders_count(self, price): - """ Calculate number of buy orders to place between lower_bound and specified price + def calc_buy_orders_count(self, price_high, price_low): + """ Calculate number of buy orders to place between high price and low price - :param float | price: Highest buy price bound + :param float | price_high: Highest buy price bound + :param float | price_low: Lowest buy price bound :return int | count: Returns number of orders """ orders_count = 0 - while price >= self.lower_bound: + while price_high >= price_low: orders_count += 1 - price = price / (1 + self.increment) + price_high = price_high / (1 + self.increment) return orders_count - def calc_sell_orders_count(self, price): - """ Calculate number of sell orders to place between upper_bound and specified price + def calc_sell_orders_count(self, price_low, price_high): + """ Calculate number of sell orders to place between low price and high price - :param float | price: Lowest sell price bound + :param float | price_low: Lowest sell price bound + :param float | price_high: Highest sell price bound :return int | count: Returns number of orders """ orders_count = 0 - while price <= self.upper_bound: + while price_low <= price_high: orders_count += 1 - price = price * (1 + self.increment) + price_low = price_low * (1 + self.increment) return orders_count + def place_virtual_buy_order(self, amount, price): + """ Place a virtual buy order + + :param float | amount: Order amount in QUOTE + :param float | price: Order price in BASE + :return dict | order: Returns virtual order instance + """ + symbol = self.market['base']['symbol'] + precision = self.market['base']['precision'] + + order = VirtualOrder() + order['price'] = price + + quote_asset = Amount(amount, self.market['quote']['symbol']) + order['quote'] = quote_asset + + base_asset = Amount(amount * price, self.market['base']['symbol']) + order['base'] = base_asset + order['for_sale'] = base_asset + + self.log.info('Placing a virtual buy order for {:.{prec}f} {} @ {:.8f}' + .format(order['base']['amount'], symbol, price, prec=precision)) + self.virtual_orders.append(order) + + # Immediately lower avail balance + self.base_balance['amount'] -= order['base']['amount'] + + return order + + def place_virtual_sell_order(self, amount, price): + """ Place a virtual sell order + + :param float | amount: Order amount in QUOTE + :param float | price: Order price in BASE + :return dict | order: Returns virtual order instance + """ + symbol = self.market['quote']['symbol'] + precision = self.market['quote']['precision'] + + order = VirtualOrder() + order['price'] = price ** -1 + + quote_asset = Amount(amount * price, self.market['base']['symbol']) + order['quote'] = quote_asset + + base_asset = Amount(amount, self.market['quote']['symbol']) + order['base'] = base_asset + order['for_sale'] = base_asset + + self.log.info('Placing a virtual sell order for {:.{prec}f} {} @ {:.8f}' + .format(amount, symbol, price, prec=precision)) + self.virtual_orders.append(order) + + # Immediately lower avail balance + self.quote_balance['amount'] -= order['base']['amount'] + + return order + + def cancel_orders_wrapper(self, orders, **kwargs): + """ Cancel specific order(s) + :param list orders: list of orders to cancel + """ + if not isinstance(orders, (list, set, tuple)): + orders = [orders] + + virtual_orders = [order['price'] for order in orders if isinstance(order, VirtualOrder)] + real_orders = [order for order in orders if 'id' in order] + + # Just rebuild virtual orders list to avoid calling Asset's __eq__ method + self.virtual_orders = [order for order in self.virtual_orders if order['price'] not in virtual_orders] + + if real_orders: + return self.cancel_orders(real_orders, **kwargs) + + return True + def error(self, *args, **kwargs): self.disabled = True @@ -1490,3 +1801,9 @@ def update_gui_slider(self): percentage = (total_balance['base'] / total) * 100 idle_add(self.view.set_worker_slider, self.worker_name, percentage) + +class VirtualOrder(dict): + """ Wrapper class to handle virtual orders comparison in list index() method + """ + def __float__(self): + return self['price'] From 7cc4fb67b935a447168a54f0f25cdf2d39466705 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 29 Nov 2018 13:47:27 +0200 Subject: [PATCH 1046/1846] Fix missing space PEP8 warning --- dexbot/strategies/staggered_orders.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 573e7b9e7..94b9595f3 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1802,6 +1802,7 @@ def update_gui_slider(self): idle_add(self.view.set_worker_slider, self.worker_name, percentage) + class VirtualOrder(dict): """ Wrapper class to handle virtual orders comparison in list index() method """ From 92b24bc921a8ed06e4841df8ec6bb44ce5666853 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 29 Nov 2018 13:47:47 +0200 Subject: [PATCH 1047/1846] Remove print statements --- dexbot/strategies/staggered_orders.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 94b9595f3..31f7a112f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1335,7 +1335,6 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False if place_order and asset == 'base': virtual_bound = self.market_center_price / math.sqrt(1 + self.target_spread) orders_count = self.calc_buy_orders_count(virtual_bound, price) - print(orders_count) if orders_count > self.operational_depth and isinstance(order, VirtualOrder): # Allow to place closer order only if current is virtual @@ -1347,7 +1346,6 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False elif place_order and asset == 'quote': virtual_bound = self.market_center_price * math.sqrt(1 + self.target_spread) orders_count = self.calc_sell_orders_count(virtual_bound, price) - print(orders_count) if orders_count > self.operational_depth and isinstance(order, VirtualOrder): self.log.info('Placing virtual closer sell order') new_order = self.place_virtual_sell_order(quote_amount, price) From 07190f944bafc889b5768a2ae3f46ceaf8c5ec8b Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 29 Nov 2018 13:48:27 +0200 Subject: [PATCH 1048/1846] Remove extra else from allocate_assset() --- dexbot/strategies/staggered_orders.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 31f7a112f..7dcc8e41c 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -693,8 +693,6 @@ def allocate_asset(self, asset, asset_balance): order_type, opposite_asset_limit, opposite_symbol)) self.place_closer_order(asset, closest_own_order, own_asset_limit=own_asset_limit, opposite_asset_limit=opposite_asset_limit, allow_partial=False) - else: - self.log.warning('Boostrap is off, but there is no opposite orders, not placing closer order') elif not opposite_orders: # Do not try to do anything than placing closer order whether there is no opposite orders return From e16dfacaa8e2d35baa51b154738f56a91ac61868 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 29 Nov 2018 13:48:53 +0200 Subject: [PATCH 1049/1846] Change dexbot version number to 0.7.27 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 9023e8369..281ad9cc2 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.26' +VERSION = '0.7.27' AUTHOR = 'Codaone Oy' __version__ = VERSION From de69acd47a2524d226f0bdd39a577b3f448614f9 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 29 Nov 2018 14:25:52 +0200 Subject: [PATCH 1050/1846] Refactor by moving tests to project root tests folder --- {dexbot/strategies/external_feeds/tests => tests}/__init__.py | 0 .../external_feeds/tests => tests}/async_feeds_test.py | 0 {dexbot/strategies/external_feeds/tests => tests}/ccxt_test.py | 0 {dexbot/strategies/external_feeds/tests => tests}/gecko_test.py | 2 +- .../external_feeds/tests => tests}/price_feed_test.py | 0 .../external_feeds/tests => tests}/process_pair_test.py | 0 .../strategies/external_feeds/tests => tests}/pywaves_test.py | 0 {dexbot/strategies/external_feeds/tests => tests}/styles.py | 0 {dexbot/strategies/external_feeds/tests => tests}/waves_test.py | 0 9 files changed, 1 insertion(+), 1 deletion(-) rename {dexbot/strategies/external_feeds/tests => tests}/__init__.py (100%) rename {dexbot/strategies/external_feeds/tests => tests}/async_feeds_test.py (100%) rename {dexbot/strategies/external_feeds/tests => tests}/ccxt_test.py (100%) rename {dexbot/strategies/external_feeds/tests => tests}/gecko_test.py (92%) rename {dexbot/strategies/external_feeds/tests => tests}/price_feed_test.py (100%) rename {dexbot/strategies/external_feeds/tests => tests}/process_pair_test.py (100%) rename {dexbot/strategies/external_feeds/tests => tests}/pywaves_test.py (100%) rename {dexbot/strategies/external_feeds/tests => tests}/styles.py (100%) rename {dexbot/strategies/external_feeds/tests => tests}/waves_test.py (100%) diff --git a/dexbot/strategies/external_feeds/tests/__init__.py b/tests/__init__.py similarity index 100% rename from dexbot/strategies/external_feeds/tests/__init__.py rename to tests/__init__.py diff --git a/dexbot/strategies/external_feeds/tests/async_feeds_test.py b/tests/async_feeds_test.py similarity index 100% rename from dexbot/strategies/external_feeds/tests/async_feeds_test.py rename to tests/async_feeds_test.py diff --git a/dexbot/strategies/external_feeds/tests/ccxt_test.py b/tests/ccxt_test.py similarity index 100% rename from dexbot/strategies/external_feeds/tests/ccxt_test.py rename to tests/ccxt_test.py diff --git a/dexbot/strategies/external_feeds/tests/gecko_test.py b/tests/gecko_test.py similarity index 92% rename from dexbot/strategies/external_feeds/tests/gecko_test.py rename to tests/gecko_test.py index dbbe43d16..7e9aad6c9 100644 --- a/dexbot/strategies/external_feeds/tests/gecko_test.py +++ b/tests/gecko_test.py @@ -1,5 +1,5 @@ import click -from dexbot.strategies.external_feeds.tests.styles import yellow +from tests import yellow from dexbot.strategies.external_feeds.gecko_feed import get_gecko_price from dexbot.strategies.external_feeds.process_pair import split_pair diff --git a/dexbot/strategies/external_feeds/tests/price_feed_test.py b/tests/price_feed_test.py similarity index 100% rename from dexbot/strategies/external_feeds/tests/price_feed_test.py rename to tests/price_feed_test.py diff --git a/dexbot/strategies/external_feeds/tests/process_pair_test.py b/tests/process_pair_test.py similarity index 100% rename from dexbot/strategies/external_feeds/tests/process_pair_test.py rename to tests/process_pair_test.py diff --git a/dexbot/strategies/external_feeds/tests/pywaves_test.py b/tests/pywaves_test.py similarity index 100% rename from dexbot/strategies/external_feeds/tests/pywaves_test.py rename to tests/pywaves_test.py diff --git a/dexbot/strategies/external_feeds/tests/styles.py b/tests/styles.py similarity index 100% rename from dexbot/strategies/external_feeds/tests/styles.py rename to tests/styles.py diff --git a/dexbot/strategies/external_feeds/tests/waves_test.py b/tests/waves_test.py similarity index 100% rename from dexbot/strategies/external_feeds/tests/waves_test.py rename to tests/waves_test.py From 164fda9eae6c6ce99a36f2399a526dd1b2500383 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 29 Nov 2018 14:32:19 +0200 Subject: [PATCH 1051/1846] Refactor styles tests to separate file --- dexbot/styles.py | 34 +++++++++++++++++++++++++++++++ tests/styles.py | 48 -------------------------------------------- tests/styles_test.py | 12 +++++++++++ 3 files changed, 46 insertions(+), 48 deletions(-) create mode 100644 dexbot/styles.py delete mode 100644 tests/styles.py create mode 100644 tests/styles_test.py diff --git a/dexbot/styles.py b/dexbot/styles.py new file mode 100644 index 000000000..36f1755a9 --- /dev/null +++ b/dexbot/styles.py @@ -0,0 +1,34 @@ +""" This is helper file to print out strings in different colours +""" + + +def style(value, styling): + return styling + value + '\033[0m' + + +def green(value): + return style(value, '\033[92m') + + +def blue(value): + return style(value, '\033[94m') + + +def yellow(value): + return style(value, '\033[93m') + + +def red(value): + return style(value, '\033[91m') + + +def pink(value): + return style(value, '\033[95m') + + +def bold(value): + return style(value, '\033[1m') + + +def underline(value): + return style(value, '\033[4m') diff --git a/tests/styles.py b/tests/styles.py deleted file mode 100644 index 686e8c88f..000000000 --- a/tests/styles.py +++ /dev/null @@ -1,48 +0,0 @@ -import os - -""" -This is the unit test for print styles -""" - -def style(s, style): - return style + s + '\033[0m' - - -def green(s): - return style(s, '\033[92m') - - -def blue(s): - return style(s, '\033[94m') - - -def yellow(s): - return style(s, '\033[93m') - - -def red(s): - return style(s, '\033[91m') - - -def pink(s): - return style(s, '\033[95m') - - -def bold(s): - return style(s, '\033[1m') - - -def underline(s): - return style(s, '\033[4m') - - -if __name__ == '__main__': - # Unit test - # Todo: Move tests to own files - print(green("green style test")) - print(blue("blue style test")) - print(yellow("yellow style test")) - print(red("red style test")) - print(pink("pink style test")) - print(bold("bold style test")) - print(underline("underline test")) diff --git a/tests/styles_test.py b/tests/styles_test.py new file mode 100644 index 000000000..e9dac1143 --- /dev/null +++ b/tests/styles_test.py @@ -0,0 +1,12 @@ +from dexbot.styles import green, blue, yellow, red, pink, bold, underline + + +if __name__ == '__main__': + # Test each text style + print(green("green style test")) + print(blue("blue style test")) + print(yellow("yellow style test")) + print(red("red style test")) + print(pink("pink style test")) + print(bold("bold style test")) + print(underline("underline test")) \ No newline at end of file From c1441026b4f601bd8e5edfd3625a4c822db71696 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 29 Nov 2018 15:05:33 +0200 Subject: [PATCH 1052/1846] Fix style import in gecko_test.py --- tests/gecko_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/gecko_test.py b/tests/gecko_test.py index 7e9aad6c9..45a3a0d27 100644 --- a/tests/gecko_test.py +++ b/tests/gecko_test.py @@ -1,5 +1,5 @@ import click -from tests import yellow +from dexbot.styles import yellow from dexbot.strategies.external_feeds.gecko_feed import get_gecko_price from dexbot.strategies.external_feeds.process_pair import split_pair From 297fb0f65abb0819867536dccc765b7f513fafdd Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 29 Nov 2018 21:28:54 +0200 Subject: [PATCH 1053/1846] Change imports --- dexbot/strategies/external_feeds/price_feed.py | 7 ++++--- tests/ccxt_test.py | 2 +- tests/price_feed_test.py | 2 +- tests/waves_test.py | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/external_feeds/price_feed.py b/dexbot/strategies/external_feeds/price_feed.py index a8081d1c7..f98df4adc 100755 --- a/dexbot/strategies/external_feeds/price_feed.py +++ b/dexbot/strategies/external_feeds/price_feed.py @@ -1,9 +1,10 @@ -import dexbot.strategies.external_feeds.ccxt_feed -from dexbot.strategies.external_feeds.waves_feed import get_waves_price +import re + +from dexbot.strategies.external_feeds.ccxt_feed import get_ccxt_price from dexbot.strategies.external_feeds.gecko_feed import get_gecko_price +from dexbot.strategies.external_feeds.waves_feed import get_waves_price from dexbot.strategies.external_feeds.process_pair import split_pair, join_pair, filter_prefix_symbol, \ filter_bit_symbol, get_consolidated_pair, debug -import re class PriceFeed: diff --git a/tests/ccxt_test.py b/tests/ccxt_test.py index adddc7261..cb1d34547 100644 --- a/tests/ccxt_test.py +++ b/tests/ccxt_test.py @@ -1,5 +1,5 @@ -import click import asyncio + from dexbot.strategies.external_feeds.ccxt_feed import get_ccxt_load_markets, get_ccxt_price """ diff --git a/tests/price_feed_test.py b/tests/price_feed_test.py index 2223e7a5b..304337ca8 100644 --- a/tests/price_feed_test.py +++ b/tests/price_feed_test.py @@ -1,5 +1,5 @@ from dexbot.strategies.external_feeds.price_feed import PriceFeed -from dexbot.strategies.external_feeds.process_pair import get_consolidated_pair + """ This is the unit test for testing price_feed module. diff --git a/tests/waves_test.py b/tests/waves_test.py index 857c9bf9b..1791306fd 100644 --- a/tests/waves_test.py +++ b/tests/waves_test.py @@ -1,5 +1,5 @@ from dexbot.strategies.external_feeds.process_pair import split_pair, filter_prefix_symbol, filter_bit_symbol -from dexbot.strategies.external_feeds.waves_feed import get_waves_price, get_waves_symbols +from dexbot.strategies.external_feeds.waves_feed import get_waves_price """ This is the unit test for getting external feed data from waves DEX. From b16735cc99569e6f2f8c96385118fd6d8f47559b Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 29 Nov 2018 21:29:47 +0200 Subject: [PATCH 1054/1846] Refactor code in general --- dexbot/strategies/base.py | 17 ++++++++--------- dexbot/strategies/external_feeds/ccxt_feed.py | 15 +++++++++------ dexbot/strategies/external_feeds/gecko_feed.py | 4 ++-- dexbot/strategies/external_feeds/price_feed.py | 10 +++++----- dexbot/strategies/external_feeds/waves_feed.py | 7 +++---- dexbot/strategies/relative_orders.py | 2 +- tests/waves_test.py | 10 ++-------- 7 files changed, 30 insertions(+), 35 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index c9711a6d5..3f81105ff 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -613,21 +613,20 @@ def get_lowest_own_sell_order(self, orders=None): def get_external_market_center_price(self): center_price = None self.log.debug('inside get_external_mcp, exchange: {} '.format(self.external_price_source)) - market = self.market.get_string('/') + market = self.market.get_string('/') self.log.debug('market: {} '.format(market)) - pf = PriceFeed(self.external_price_source, market) - pf.filter_symbols() - center_price = pf.get_center_price(None) + price_feed = PriceFeed(self.external_price_source, market) + price_feed.filter_symbols() + center_price = price_feed.get_center_price(None) self.log.debug('PriceFeed: {}'.format(center_price)) - if center_price is None: # try USDT - center_price = pf.get_center_price("USDT") + if center_price is None: # Try USDT + center_price = price_feed.get_center_price("USDT") self.log.debug('Substitute USD/USDT center price: {}'.format(center_price)) - if center_price is None: # try consolidated - center_price = pf.get_consolidated_price() + if center_price is None: # Try consolidated + center_price = price_feed.get_consolidated_price() self.log.debug('Consolidated center price: {}'.format(center_price)) return center_price - def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors=False): """ Returns the center price of market including own orders. diff --git a/dexbot/strategies/external_feeds/ccxt_feed.py b/dexbot/strategies/external_feeds/ccxt_feed.py index 08c437f15..9154119a6 100644 --- a/dexbot/strategies/external_feeds/ccxt_feed.py +++ b/dexbot/strategies/external_feeds/ccxt_feed.py @@ -21,12 +21,15 @@ async def fetch_ticker(exchange, symbol): ticker = None try: ticker = await exchange.fetch_ticker(symbol.upper()) - except Exception as e: - print(type(e).__name__, e.args, 'Exchange Error (ignoring)') - except ccxt.RequestTimeout as e: - print(type(e).__name__, e.args, 'Request Timeout (ignoring)') - except ccxt.ExchangeNotAvailable as e: - print(type(e).__name__, e.args, 'Exchange Not Available due to downtime or maintenance (ignoring)') + except Exception as exception: + print(type(exception).__name__, exception.args, + 'Exchange Error (ignoring)') + except accxt.RequestTimeout as exception: + print(type(exception).__name__, exception.args, + 'Request Timeout (ignoring)') + except accxt.ExchangeNotAvailable as exception: + print(type(exception).__name__, exception.args, + 'Exchange Not Available due to downtime or maintenance (ignoring)') await exchange.close() return ticker diff --git a/dexbot/strategies/external_feeds/gecko_feed.py b/dexbot/strategies/external_feeds/gecko_feed.py index e1f46196a..8e415cdd9 100644 --- a/dexbot/strategies/external_feeds/gecko_feed.py +++ b/dexbot/strategies/external_feeds/gecko_feed.py @@ -1,9 +1,9 @@ import requests import asyncio + from dexbot.strategies.external_feeds.process_pair import split_pair, debug -""" - To use Gecko API, note that gecko does not provide pairs by default. +""" To use Gecko API, note that gecko does not provide pairs by default. For base/quote one must be listed as ticker and the other as fullname, i.e. BTCUSD is vs_currency = usd , ids = bitcoin https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin diff --git a/dexbot/strategies/external_feeds/price_feed.py b/dexbot/strategies/external_feeds/price_feed.py index f98df4adc..b6241913d 100755 --- a/dexbot/strategies/external_feeds/price_feed.py +++ b/dexbot/strategies/external_feeds/price_feed.py @@ -58,11 +58,11 @@ def get_consolidated_price(self): try: pair1, pair2 = get_consolidated_pair(self.pair[0], self.pair[1]) self.pair = pair1 - p1_price = self.get_center_price(None) + pair1_price = self.get_center_price(None) self.pair = pair2 - p2_price = self.get_center_price(None) - if p1_price and p2_price: - center_price = p1_price * p2_price + pair2_price = self.get_center_price(None) + if pair1_price and pair2_price: + center_price = pair1_price * pair2_price print(original_pair, "price is ", center_price) self.pair = original_pair # put original pair back except Exception as e: @@ -87,7 +87,7 @@ def _get_center_price(self): symbol = self._symbol price = None if self._exchange not in self._alt_exchanges: - price = dexbot.strategies.external_feeds.ccxt_feed.get_ccxt_price(symbol, self._exchange) + price = get_ccxt_price(symbol, self._exchange) debug("use ccxt exchange ", self._exchange, ' symbol ', symbol, ' price:', price) elif self._exchange == 'gecko': price = get_gecko_price(symbol_=symbol) diff --git a/dexbot/strategies/external_feeds/waves_feed.py b/dexbot/strategies/external_feeds/waves_feed.py index 1924e5f93..072979e1d 100644 --- a/dexbot/strategies/external_feeds/waves_feed.py +++ b/dexbot/strategies/external_feeds/waves_feed.py @@ -8,9 +8,8 @@ async def get_json(url): - r = requests.get(url) - json_obj = r.json() - return json_obj + response = requests.get(url) + return response.json() def get_last_price(base, quote): @@ -19,7 +18,7 @@ def get_last_price(base, quote): market_bq = MARKET_URL + quote + '/' + base # external exchange format ticker = asyncio.get_event_loop().run_until_complete(get_json(WAVES_URL + market_bq)) current_price = ticker['24h_close'] - except Exception as e: + except Exception as exeption: pass # No pair found on waves dex for external price. return current_price diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index c736621bb..b225fd0ce 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -91,7 +91,7 @@ def __init__(self, *args, **kwargs): if external_source != 'none': self.center_price = self.get_external_market_center_price() if self.center_price is None: - self.center_price = self.worker["center_price"] # set as manual + self.center_price = self.worker["center_price"] # Set as manual else: self.center_price = self.worker["center_price"] diff --git a/tests/waves_test.py b/tests/waves_test.py index 1791306fd..8410d862d 100644 --- a/tests/waves_test.py +++ b/tests/waves_test.py @@ -1,8 +1,7 @@ from dexbot.strategies.external_feeds.process_pair import split_pair, filter_prefix_symbol, filter_bit_symbol from dexbot.strategies.external_feeds.waves_feed import get_waves_price -""" -This is the unit test for getting external feed data from waves DEX. +""" This is the unit test for getting external feed data from waves DEX. """ if __name__ == '__main__': @@ -12,7 +11,7 @@ raw_pair = split_pair(symbol) pair = [filter_bit_symbol(j) for j in [filter_prefix_symbol(i) for i in raw_pair]] - # test symbol and pair options for getting price + # Test symbol and pair options for getting price pair_price = get_waves_price(pair_=pair) if pair_price: print("pair price ", pair_price, sep=":") @@ -20,8 +19,3 @@ current_price = get_waves_price(symbol_=symbol) if current_price: print("symbol price ", current_price, sep=":") - - # get entire symbol list - print("\n") - # symbol_list = get_waves_symbols() - # print(symbol_list) From f276ea083df48c9bf5c7b41a5081b381ee729e12 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 29 Nov 2018 21:30:15 +0200 Subject: [PATCH 1055/1846] Change process_pair.py debug to false --- dexbot/strategies/external_feeds/process_pair.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/external_feeds/process_pair.py b/dexbot/strategies/external_feeds/process_pair.py index c4888c613..2c053f301 100644 --- a/dexbot/strategies/external_feeds/process_pair.py +++ b/dexbot/strategies/external_feeds/process_pair.py @@ -1,6 +1,6 @@ import re -isDebug = True +isDebug = False def debug(*args): From e80ac8968ebcbf59497367292bbfa49efa6c300f Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 30 Nov 2018 12:04:34 +0200 Subject: [PATCH 1056/1846] Add missing version numbers to requirements --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0b29432c1..459be625e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,8 +3,8 @@ pyqt-distutils==0.7.3 pyinstaller==3.3.1 click-datetime==0.2 cryptography==2.3 -aiohttp +aiohttp==2.3.10 requests>=2.20.0 yarl==1.1.0 ccxt==1.17.434 -pywaves +pywaves==0.8.20 From 7195b0815b0c539bf0d7ac668a6ea55682fd002a Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 30 Nov 2018 12:25:03 +0200 Subject: [PATCH 1057/1846] Remove broken tests --- tests/async_feeds_test.py | 24 ----------------- tests/ccxt_test.py | 25 ------------------ tests/price_feed_test.py | 55 --------------------------------------- 3 files changed, 104 deletions(-) delete mode 100644 tests/async_feeds_test.py delete mode 100644 tests/ccxt_test.py delete mode 100644 tests/price_feed_test.py diff --git a/tests/async_feeds_test.py b/tests/async_feeds_test.py deleted file mode 100644 index 3725c7457..000000000 --- a/tests/async_feeds_test.py +++ /dev/null @@ -1,24 +0,0 @@ -import asyncio -import aiohttp - -""" -This is the unit test for testing async with ccxt only with multiple urls. -""" - -gecko_coins_url = 'https://api.coingecko.com/api/v3/coins/' -waves_symbols = 'http://marketdata.wavesplatform.com/api/symbols' -cwatch_assets = 'https://api.cryptowat.ch/assets' -urls = [cwatch_assets, waves_symbols, gecko_coins_url] - - -@asyncio.coroutine -def call_url(url): - print('Starting {}'.format(url)) - response = yield from aiohttp.ClientSession().get(url) - data = yield from response.text() - print('url: {} bytes: {}'.format(url, len(data))) - return data - - -futures = [call_url(url) for url in urls] -asyncio.run(asyncio.wait(futures)) diff --git a/tests/ccxt_test.py b/tests/ccxt_test.py deleted file mode 100644 index cb1d34547..000000000 --- a/tests/ccxt_test.py +++ /dev/null @@ -1,25 +0,0 @@ -import asyncio - -from dexbot.strategies.external_feeds.ccxt_feed import get_ccxt_load_markets, get_ccxt_price - -""" -This is the unit test for getting external feed data from CCXT using ccxt_feed module. -""" - -if __name__ == '__main__': - - exchanges = ['bitfinex', 'kraken', 'binance', 'gdax'] - symbol = 'BTC/USDT' - - # testing get all pairs. - for exchange_id in exchanges: - print("\n\n\n") - print(exchange_id) - result = asyncio.get_event_loop().run_until_complete(get_ccxt_load_markets(exchange_id)) - all_symbols = [obj for obj in result] - print(all_symbols) - - # testing get center price - center_price = [get_ccxt_price(symbol, e) for e in exchanges] - print(' exchange: ', exchanges, ' symbol: ', symbol, sep=':') - print(' center_price: ', center_price) diff --git a/tests/price_feed_test.py b/tests/price_feed_test.py deleted file mode 100644 index 304337ca8..000000000 --- a/tests/price_feed_test.py +++ /dev/null @@ -1,55 +0,0 @@ -from dexbot.strategies.external_feeds.price_feed import PriceFeed - - -""" -This is the unit test for testing price_feed module. -Run this test first to cover everything in external feeds - -In DEXBot: unit of measure = BASE, asset of interest = QUOTE -""" - - -def test_exchanges(): - center_price = None - symbol = 'BTC/USD' - exchanges = ['gecko', 'bitfinex', 'kraken', 'gdax', 'binance', 'waves'] - - for exchange in exchanges: - pf = PriceFeed(exchange, symbol) - pf.filter_symbols() - center_price = pf.get_center_price(None) - print("center price: ", center_price) - if center_price is None: # try USDT - center_price = pf.get_center_price('USDT') - print("try again, s/USD/USDT, center price: ", center_price) - - -def test_consolidated_pair(): - symbol2 = 'STEEM/BTS' # STEEM/USD * USD/BTS = STEEM/BTS - pf = PriceFeed('gecko', symbol2) - center_price = pf.get_consolidated_price() - print(center_price) - - -def test_alternative_usd(): - # todo - refactor price_feed to handle alt USD options. - alternative_usd = ['USDT', 'USDC', 'TUSD', 'GUSD'] - exchanges = ['bittrex', 'poloniex', 'gemini', 'bitfinex', 'kraken', 'binance', 'okex'] - symbol = 'BTC/USD' # replace with alt usd - - for exchange in exchanges: - for alt in alternative_usd: - pf = PriceFeed(exchange, symbol) - center_price = pf.get_center_price(None) - if center_price: - print(symbol, ' using alt:', alt, center_price, "\n", sep=' ') - else: - center_price = pf.get_center_price(alt) - if center_price: - print(symbol, ' using alt:', alt, center_price, "\n", sep=' ') - - -if __name__ == '__main__': - test_exchanges() - test_consolidated_pair() - test_alternative_usd() From a23b2b26b3ca14fd5f6c55f3b3eec6e9329e36ea Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 30 Nov 2018 13:01:34 +0200 Subject: [PATCH 1058/1846] Fix crashing on older version worker --- dexbot/cli_conf.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 8ac8d210e..a5014eb98 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -217,8 +217,12 @@ def configure_worker(whiptail, worker_config): # If strategy has changed, create new config where base elements stay the same for config_item in StrategyBase.configure(): - key = config_item[0] - new_worker_config[key] = worker_config[key] + try: + key = config_item[0] + new_worker_config[key] = worker_config[key] + except KeyError as error: + # In case using old configuration file and there are new fields, this passes missing key + pass # Add module separately to the config new_worker_config['module'] = worker_config['module'] From 7fbabd332b119654f0a3ce5fb7dbd715b0bdc2a5 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 30 Nov 2018 13:20:06 +0200 Subject: [PATCH 1059/1846] Change dexbot version number to 0.8.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 281ad9cc2..badd3dd92 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.7.27' +VERSION = '0.8.0' AUTHOR = 'Codaone Oy' __version__ = VERSION From cdbf12cfc45534e49c577b0b4621446239221875 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 2 Dec 2018 14:53:17 +0500 Subject: [PATCH 1060/1846] Fix restoring of virtual orders There was a bug causing additional virtual orders placement after initial boostrap. --- dexbot/strategies/staggered_orders.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 7dcc8e41c..dc9a41226 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -508,7 +508,6 @@ def restore_virtual_orders(self): if not isinstance(furthest_order, VirtualOrder): # Failed to place order break - self.virtual_orders_restored = True if self.sell_orders: furthest_order = self.real_sell_orders[-1] @@ -517,7 +516,9 @@ def restore_virtual_orders(self): if not isinstance(furthest_order, VirtualOrder): # Failed to place order break - self.virtual_orders_restored = True + + # Set "restored" flag anyway to not break initial bootstrap + self.virtual_orders_restored = True def replace_real_order_with_virtual(self, order): """ Replace real limit order with virtual order From ed16b97ab5800ee923c16be202674ede8fcc9d3f Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 2 Dec 2018 14:57:02 +0500 Subject: [PATCH 1061/1846] Additional fix for 'NoneType' in allocate_asset() Previous fix in bd39424e097a98cc7812e05c7576cad1e159000c solved not all occurances of the issue. The problem condition may also occure when opposite side was reached a range bound, thus opposite_oeders will be None. --- dexbot/strategies/staggered_orders.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index dc9a41226..3d6663b76 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -666,7 +666,7 @@ def allocate_asset(self, asset, asset_balance): .format(order_type, self.actual_spread, self.target_spread + self.increment)) if self.bootstrapping: self.place_closer_order(asset, closest_own_order) - else: + elif opposite_orders: # Place order limited by size of the opposite-side order if self.mode == 'mountain': opposite_asset_limit = closest_opposite_order['base']['amount'] * (1 + self.increment) @@ -694,6 +694,9 @@ def allocate_asset(self, asset, asset_balance): order_type, opposite_asset_limit, opposite_symbol)) self.place_closer_order(asset, closest_own_order, own_asset_limit=own_asset_limit, opposite_asset_limit=opposite_asset_limit, allow_partial=False) + else: + # Opposite side probably reached range bound, allow to place partial order + self.place_closer_order(asset, closest_own_order, allow_partial=True) elif not opposite_orders: # Do not try to do anything than placing closer order whether there is no opposite orders return From cb96e3d0a8bf140042921297c121204da2aa7c1f Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 2 Dec 2018 15:02:05 +0500 Subject: [PATCH 1062/1846] Manual center price should be used only when there are no orders --- dexbot/strategies/staggered_orders.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 3d6663b76..41aa5801d 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -184,8 +184,9 @@ def maintain_strategy(self, *args, **kwargs): # Check if market center price is calculated self.market_center_price = self.get_market_center_price(suppress_errors=True) - # Set center price to manual value if needed - if self.center_price: + # Set center price to manual value if needed. Manual center price works only when there are no orders + if self.center_price and not (self.buy_orders or self.sell_orders): + self.log.debug('Using manual center price because of no sell or buy orders') self.market_center_price = self.center_price # Still not have market_center_price? Empty market, don't continue From 19038fa1e5e4ae37f291a6b6705f0585a6f3c9bd Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 2 Dec 2018 15:02:53 +0500 Subject: [PATCH 1063/1846] Add debug message about overriding bounds --- dexbot/strategies/staggered_orders.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 41aa5801d..40c017943 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -203,8 +203,12 @@ def maintain_strategy(self, *args, **kwargs): # Check market's price boundaries if self.market_center_price > self.upper_bound: + self.log.debug('Overriding upper bound by market center price: {} -> {:.8f}' + .format(self.upper_bound, self.market_center_price)) self.upper_bound = self.market_center_price elif self.market_center_price < self.lower_bound: + self.log.debug('Overriding lower bound by market center price: {} -> {:.8f}' + .format(self.lower_bound, self.market_center_price)) self.lower_bound = self.market_center_price # Remove orders that exceed boundaries From 14490133c295176aef20d9f166cd74775b36b545 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 2 Dec 2018 15:06:47 +0500 Subject: [PATCH 1064/1846] Adjust debug message in get_market_center_price() --- dexbot/strategies/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index f90421609..215e68e96 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -654,7 +654,7 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors # Calculate and return market center price. make sure buy_price has value if buy_price: center_price = buy_price * math.sqrt(sell_price / buy_price) - self.log.debug('Inside get_market_center_price: {} '.format(center_price)) + self.log.debug('Center price in get_market_center_price: {:.8f} '.format(center_price)) return center_price def get_market_buy_price(self, quote_amount=0, base_amount=0, exclude_own_orders=True): From 66388e15b9e386d5ed80c0281e911acf0de1a487 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 2 Dec 2018 16:11:26 +0500 Subject: [PATCH 1065/1846] Adjust center price by given percent in symmetrical way Reported by El Stone on telegram. The problem was that x% adjustement on FOO:BAR market did not produced the same result as -x% adjustement on BAR:FOO market. --- dexbot/strategies/relative_orders.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index b225fd0ce..49f1e8ebe 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -335,8 +335,14 @@ def calculate_manual_offset(center_price, manual_offset): :param float | center_price: :param float | manual_offset: :return: Center price with manual offset + + Adjust center price by given percent in symmetrical way. Thus, -1% adjustement on BTS:USD market will be + same as adjusting +1% on USD:BTS market. """ - return center_price + (center_price * manual_offset) + if manual_offset < 0: + return center_price / (1 + abs(manual_offset)) + else: + return center_price * (1 + manual_offset) def check_orders(self, *args, **kwargs): """ Tests if the orders need updating From 3c5c45403d8050b6297f855d1ad1ae798478ed00 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 2 Dec 2018 23:20:43 +0500 Subject: [PATCH 1066/1846] Simplify ticker usage --- dexbot/strategies/relative_orders.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 49f1e8ebe..241df575d 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -255,9 +255,9 @@ def update_orders(self): self.update_orders() def _calculate_center_price(self, suppress_errors=False): - ticker = self.market.ticker() - highest_bid = ticker.get("highestBid") - lowest_ask = ticker.get("lowestAsk") + highest_bid = float(self.ticker().get('highestBid')) + lowest_ask = float(self.ticker().get('lowestAsk')) + if highest_bid is None or highest_bid == 0.0: if not suppress_errors: self.log.critical( @@ -274,7 +274,7 @@ def _calculate_center_price(self, suppress_errors=False): return None # Calculate center price between two closest orders on the market - return highest_bid['price'] * math.sqrt(lowest_ask['price'] / highest_bid['price']) + return highest_bid * math.sqrt(lowest_ask / highest_bid) def calculate_center_price(self, center_price=None, asset_offset=False, spread=None, order_ids=None, manual_offset=0, suppress_errors=False): From 464f6d5ae9a6836b33dedfc602dc5521c45bc599 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 2 Dec 2018 23:53:13 +0500 Subject: [PATCH 1067/1846] Make calculate_asset_offset() more aggressive Updated implementation doing more aggressive offsetting of center price. This version of asset offset calculation was originally proposed by El Stone on telegram. This is a slightly refactored version. --- dexbot/strategies/relative_orders.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 241df575d..ee3852e45 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -314,19 +314,23 @@ def calculate_asset_offset(self, center_price, order_ids, spread): total = (total_balance['quote'] * center_price) + total_balance['base'] if not total: # Prevent division by zero - balance = 0 + base_percent = quote_percent = 0.5 else: - # Returns a value between -1 and 1 - balance = (total_balance['base'] / total) * 2 - 1 + base_percent = total_balance['base'] / total + quote_percent = 1 - base_percent - if balance < 0: - # With less of base asset center price should be offset downward - center_price = center_price / math.sqrt(1 + spread * (balance * -1)) - elif balance > 0: - # With more of base asset center price will be offset upwards - center_price = center_price * math.sqrt(1 + spread * balance) + highest_bid = float(self.ticker().get('highestBid')) + lowest_ask = float(self.ticker().get('lowestAsk')) + + lowest_price = center_price / (1 + spread) + highest_price = center_price * (1 + spread) + + # Use highest_bid price if spread-based price is lower. This limits offset aggression. + lowest_price = max(lowest_price, highest_bid) + # Use lowest_ask price if spread-based price is higher + highest_price = min(highest_price, lowest_ask) - return center_price + return math.pow(highest_price, base_percent) * math.pow(lowest_price, quote_percent) @staticmethod def calculate_manual_offset(center_price, manual_offset): From 442eb291f5d7b6551936efde162eb6dd78f33869 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 3 Dec 2018 09:19:07 +0200 Subject: [PATCH 1068/1846] Add initialization for some variables --- dexbot/strategies/staggered_orders.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 40c017943..6b49b161a 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -588,6 +588,7 @@ def allocate_asset(self, asset, asset_balance): """ self.log.debug('Need to allocate {}: {}'.format(asset, asset_balance)) closest_opposite_order = None + closest_opposite_price = 0 opposite_asset_limit = None opposite_orders = [] order_type = '' @@ -772,6 +773,7 @@ def allocate_asset(self, asset, asset_balance): else: # Place first buy order as close to the lower bound as possible self.bootstrapping = True + order = None self.log.debug('Placing first {} order'.format(order_type)) if asset == 'base': order = self.place_lowest_buy_order(asset_balance) @@ -821,6 +823,8 @@ def increase_order_sizes(self, asset, asset_balance, orders): order_type = '' symbol = '' precision = 0 + new_order_amount = 0 + furthest_order_bound = 0 if asset == 'quote': total_balance = self.quote_total_balance @@ -1076,6 +1080,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): orders_count = len(orders) orders = list(reversed(orders)) closest_order = orders[-1] + previous_amount = 0 for order in orders: order_index = orders.index(order) @@ -1144,6 +1149,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): if need_increase: price = 0 + quote_amount = 0 if asset == 'quote': price = (order['price'] ** -1) From 15967940917550ac4e92b516bf5e4b5e1db2aa9d Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 3 Dec 2018 09:19:28 +0200 Subject: [PATCH 1069/1846] Fix minor PEP8 warnings --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 6b49b161a..73fc0ba15 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -732,7 +732,7 @@ def allocate_asset(self, asset, asset_balance): funds_to_reserve = closer_own_order['amount'] * additional_reserve self.log.debug('Partially filled order on opposite side, reserving funds for next {} order: ' '{:.{prec}f} {}'.format(order_type, funds_to_reserve, own_symbol, - prec=own_precision)) + prec=own_precision)) asset_balance -= funds_to_reserve if asset_balance > own_threshold: @@ -1244,7 +1244,7 @@ def replace_partially_filled_order(self, order): needed = order['base']['amount'] - order['for_sale']['amount'] self.log.debug('Unable to replace partially filled {} order: avail/needed: {:.{prec}f}/{:.{prec}f} {}' .format(order_type, asset_balance['amount'], needed, order['base']['symbol'], - prec=precision)) + prec=precision)) def place_closer_order(self, asset, order, place_order=True, allow_partial=False, own_asset_limit=None, opposite_asset_limit=None): From 3f767f20d6745d6b64859044e86dcd2059d56514 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 3 Dec 2018 09:19:44 +0200 Subject: [PATCH 1070/1846] Remove unused variables --- dexbot/strategies/staggered_orders.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 73fc0ba15..07c1ccdec 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1267,7 +1267,6 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False order_type = '' quote_amount = 0 symbol = '' - new_order = None # Define asset-dependent variables if asset == 'base': @@ -1326,7 +1325,6 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False quote_amount = opposite_asset_amount limiter = base_amount elif asset == 'quote': - base_amount = opposite_asset_amount quote_amount = own_asset_amount limiter = quote_amount price = price ** -1 @@ -1382,7 +1380,6 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals balance = 0 order_type = '' symbol = '' - new_order = None virtual_bound = self.market_center_price / math.sqrt(1 + self.target_spread) # Define asset-dependent variables @@ -1411,7 +1408,6 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals own_asset_amount = order['base']['amount'] opposite_asset_amount = own_asset_amount / price elif self.mode == 'neutral': - giown_asset_amount = order['base']['amount'] / math.sqrt(1 + self.increment) opposite_asset_amount = own_asset_amount / price limiter = 0 @@ -1421,7 +1417,6 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals quote_amount = opposite_asset_amount limiter = base_amount elif asset == 'quote': - base_amount = opposite_asset_amount quote_amount = own_asset_amount limiter = quote_amount price = price ** -1 From 1180647e0bcfa31f209c59d689501221a8f05194 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 3 Dec 2018 09:24:37 +0200 Subject: [PATCH 1071/1846] Change dexbot version number to 0.8.1 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index badd3dd92..b194e6596 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.8.0' +VERSION = '0.8.1' AUTHOR = 'Codaone Oy' __version__ = VERSION From b587e780a1da98ee3ad48a1b7e4b3bc2e1c72cb7 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 7 Dec 2018 00:29:13 +0500 Subject: [PATCH 1072/1846] Set default mode to neutral Closes: #407 --- 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 07c1ccdec..e1ada598b 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -46,7 +46,7 @@ def configure(cls, return_base_config=True): return StrategyBase.configure(return_base_config) + [ ConfigElement( - 'mode', 'choice', 'mountain', 'Strategy mode', + 'mode', 'choice', 'neutral', 'Strategy mode', 'How to allocate funds and profits. Doesn\'t effect existing orders, only future ones', modes), ConfigElement( 'spread', 'float', 6, 'Spread', From 0298f3edd7002c087d31a0a08d27f7e3951df46d Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 7 Dec 2018 00:23:35 +0500 Subject: [PATCH 1073/1846] Introduce increase_single_order() function Reduce code doubling by moving common code of orders increase into single function. --- dexbot/strategies/staggered_orders.py | 159 +++++++++----------------- 1 file changed, 53 insertions(+), 106 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index e1ada598b..9ea18bce7 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -819,8 +819,55 @@ def increase_order_sizes(self, asset, asset_balance, orders): :return bool | True = all available funds was allocated False = not all funds was allocated, can increase more orders next time """ + + def increase_single_order(asset, order, new_order_amount): + """ To avoid code doubling, use this unified function to increase single order + + :param str | asset: 'base' or 'quote', depending if checking sell or buy + :param order | order: order needed to be increased + :param float | new_order_amount: BASE or QUOTE amount of a new order (depending on asset) + + """ + quote_amount = 0 + price = 0 + order_type = '' + order_amount = order['base']['amount'] + + if asset == 'quote': + order_type = 'sell' + price = (order['price'] ** -1) + quote_amount = new_order_amount + elif asset == 'base': + order_type = 'buy' + price = order['price'] + quote_amount = new_order_amount / price + + if asset_balance < new_order_amount - order['for_sale']['amount']: + # Balance should be enough to replace partially filled order + self.log.debug('Not enough balance to increase {} order at price {:.8f}' + .format(order_type, price)) + return True + + self.log.info('Increasing {} order at price {:.8f} from {:.{prec}f} to {:.{prec}f} {}' + .format(order_type, price, order_amount, new_order_amount, symbol, prec=precision)) + self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}, amount: {}, price: {:.8f}' + .format(order_type, self.mode, order_amount, price)) + self.cancel_orders_wrapper(order) + if asset == 'quote': + if isinstance(order, VirtualOrder): + self.place_virtual_sell_order(quote_amount, price) + else: + self.place_market_sell_order(quote_amount, price) + elif asset == 'base': + if isinstance(order, VirtualOrder): + self.place_virtual_buy_order(quote_amount, price) + else: + self.place_market_buy_order(quote_amount, price) + + # Only one increase at a time. This prevents running more than one increment round simultaneously + return False + total_balance = 0 - order_type = '' symbol = '' precision = 0 new_order_amount = 0 @@ -828,12 +875,10 @@ def increase_order_sizes(self, asset, asset_balance, orders): if asset == 'quote': total_balance = self.quote_total_balance - order_type = 'sell' symbol = self.market['quote']['symbol'] precision = self.market['quote']['precision'] elif asset == 'base': total_balance = self.base_total_balance - order_type = 'buy' symbol = self.market['base']['symbol'] precision = self.market['base']['precision'] @@ -907,40 +952,8 @@ def increase_order_sizes(self, asset, asset_balance, orders): """ new_order_amount = closer_bound / (1 + self.increment * 0.2) - quote_amount = 0 - price = 0 - - if asset == 'quote': - price = (order['price'] ** -1) - quote_amount = new_order_amount - elif asset == 'base': - price = order['price'] - quote_amount = new_order_amount / price - - if asset_balance < new_order_amount - order['for_sale']['amount']: - # Balance should be enough to replace partially filled order - self.log.debug('Not enough balance to increase {} order at price {:.8f}' - .format(order_type, price)) - return True - - self.log.info('Increasing {} order at price {:.8f} from {:.{prec}f} to {:.{prec}f} {}' - .format(order_type, price, order_amount, new_order_amount, symbol, prec=precision)) - self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}, amount: {}, price: {:.8f}' - .format(order_type, self.mode, order_amount, price)) - self.cancel_orders_wrapper(order) - if asset == 'quote': - if isinstance(order, VirtualOrder): - self.place_virtual_sell_order(quote_amount, price) - else: - self.place_market_sell_order(quote_amount, price) - elif asset == 'base': - if isinstance(order, VirtualOrder): - self.place_virtual_buy_order(quote_amount, price) - else: - self.place_market_buy_order(quote_amount, price) - - # Only one increase at a time. This prevents running more than one increment round simultaneously - return False + return increase_single_order(asset, order, new_order_amount) + elif (self.mode == 'valley' or (self.mode == 'buy_slope' and asset == 'base') or (self.mode == 'sell_slope' and asset == 'quote')): @@ -1029,40 +1042,8 @@ def increase_order_sizes(self, asset, asset_balance, orders): need_increase = True if need_increase: - price = 0 - quote_amount = 0 - - if asset == 'quote': - price = (order['price'] ** -1) - quote_amount = new_order_amount - elif asset == 'base': - price = order['price'] - quote_amount = new_order_amount / price - - if asset_balance < new_order_amount - order['for_sale']['amount']: - # Balance should be enough to replace partially filled order - self.log.debug('Not enough balance to increase {} order at price {:.8f}' - .format(order_type, price)) - return True - - self.log.info('Increasing {} order at price {:.8f} from {:.{prec}f} to {:.{prec}f} {}' - .format(order_type, price, order_amount, new_order_amount, symbol, prec=precision)) - self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}, amount: {}, price: {:.8f}' - .format(order_type, self.mode, order_amount, price)) - self.cancel_orders_wrapper(order) - if asset == 'quote': - if isinstance(order, VirtualOrder): - self.place_virtual_sell_order(quote_amount, price) - else: - self.place_market_sell_order(quote_amount, price) - elif asset == 'base': - if isinstance(order, VirtualOrder): - self.place_virtual_buy_order(quote_amount, price) - else: - self.place_market_buy_order(quote_amount, price) - - # One increase at a time. This prevents running more than one increment round simultaneously. - return False + return increase_single_order(asset, order, new_order_amount) + elif self.mode == 'neutral': """ Starting from the furthest order, for each order, see if it is approximately @@ -1148,41 +1129,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): need_increase = True if need_increase: - price = 0 - quote_amount = 0 - - if asset == 'quote': - price = (order['price'] ** -1) - quote_amount = new_order_amount - elif asset == 'base': - price = order['price'] - quote_amount = new_order_amount / price - - if asset_balance < new_order_amount - order['for_sale']['amount']: - # Balance should be enough to replace partially filled order - self.log.debug('Not enough balance to increase {} order at price {:.8f}' - .format(order_type, price)) - return True - - self.log.info('Increasing {} order at price {:.8f} from {:.{prec}f} to {:.{prec}f} {}' - .format(order_type, price, order_amount, new_order_amount, symbol, prec=precision)) - self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}' - ', amount: {:.8f}, price: {:.8f}' - .format(order_type, self.mode, order_amount, price)) - self.cancel_orders_wrapper(order) - if asset == 'quote': - if isinstance(order, VirtualOrder): - self.place_virtual_sell_order(quote_amount, price) - else: - self.place_market_sell_order(quote_amount, price) - elif asset == 'base': - if isinstance(order, VirtualOrder): - self.place_virtual_buy_order(quote_amount, price) - else: - self.place_market_buy_order(quote_amount, price) - - # One increase at a time. This prevents running more than one increment round simultaneously. - return False + return increase_single_order(asset, order, new_order_amount) return None From 816b4e26da04a364b7d6dd1c65d7749ab33053c7 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 7 Dec 2018 14:25:50 +0200 Subject: [PATCH 1074/1846] Fix typo in variable name --- dexbot/controllers/worker_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index 5c14def92..0d6913af4 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -85,8 +85,8 @@ def get_strategy_mode(worker_data): return worker_data['mode'] @staticmethod - def get_allow_instant_fill(worder_data): - return worder_data['allow_instant_fill'] + def get_allow_instant_fill(worker_data): + return worker_data['allow_instant_fill'] @staticmethod def get_assets(worker_data): From ce17bed17b32831228b368182027921efb368467 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 7 Dec 2018 00:32:17 +0500 Subject: [PATCH 1075/1846] Fix too small increase When we have small order size, small increment and small asset precision like 2, there is a situation possible when new order amount is smaller than `previous_amount + 1^-asset_precision`, for instance asset with precision=2 should give an order increase no less than 1.02 -> 1.03. You cannot increase 1.02 to 1.025. Min increase must be at least x2 precision. There is a rounding issue inside library which results in placing an order of wrong amount. ``` bitshares = BitShares(node=conf['node_bts'], no_broadcast=True) amount = Amount('2.01 ESCROW.RUBLE', blockchain_instance=bitshares) print(amount) print(float(amount)) print(float(amount) * 10 ** amount['asset']['precision']) int(float(amount) * 10 ** amount['asset']['precision']) ``` Output: ``` 2.01 ESCROW.RUBLE 2.01 200.99999999999997 200 ``` --- dexbot/strategies/staggered_orders.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 9ea18bce7..bd4e53e3f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -836,10 +836,16 @@ def increase_single_order(asset, order, new_order_amount): if asset == 'quote': order_type = 'sell' price = (order['price'] ** -1) + # New order amount must be at least x2 precision bigger + new_order_amount = max(new_order_amount, + order['base']['amount'] + 2 * 10 ** -self.market['quote']['precision']) quote_amount = new_order_amount elif asset == 'base': order_type = 'buy' price = order['price'] + # New order amount must be at least x2 precision bigger + new_order_amount = max(new_order_amount, + order['base']['amount'] + 2 * 10 ** -self.market['base']['precision']) quote_amount = new_order_amount / price if asset_balance < new_order_amount - order['for_sale']['amount']: From 796d3b6e346c0952b344a9ad95689ea0a3ff31fb Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 7 Dec 2018 20:44:53 +0500 Subject: [PATCH 1076/1846] Fix bootstrap on empty market Regression was introduced in e005c858a76671c9bb7f80cf4056d79b31932857 --- dexbot/strategies/staggered_orders.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index bd4e53e3f..fa038f398 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -189,10 +189,14 @@ def maintain_strategy(self, *args, **kwargs): self.log.debug('Using manual center price because of no sell or buy orders') self.market_center_price = self.center_price - # Still not have market_center_price? Empty market, don't continue + # On empty market we need manual center price anyway if not self.market_center_price: - self.log.warning('Cannot calculate center price on empty market, please set is manually') - return + if self.center_price: + self.market_center_price = self.center_price + else: + # Still not have market_center_price? Empty market, don't continue + self.log.warning('Cannot calculate center price on empty market, please set is manually') + return # Calculate balances, and use orders from previous call of self.refresh_orders() to reduce API calls self.refresh_balances(use_cached_orders=True) From 11e90930bb827ba663b79b67b3fa288f09c94c47 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 7 Dec 2018 20:46:02 +0500 Subject: [PATCH 1077/1846] Update instant fill check Avoid false positive when no bids or asks. --- dexbot/strategies/staggered_orders.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index fa038f398..0085651fa 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1238,12 +1238,14 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False # Check for instant fill if asset == 'base': price = order['price'] * (1 + self.increment) - if not self.is_instant_fill_enabled and price > float(self.ticker().get('lowestAsk')) and place_order: + lowest_ask = float(self.ticker().get('lowestAsk')) + if not self.is_instant_fill_enabled and price > lowest_ask and lowest_ask > 0 and place_order: self.log.info('Refusing to place an order which crosses lowest ask') return None elif asset == 'quote': price = (order['price'] ** -1) / (1 + self.increment) - if not self.is_instant_fill_enabled and price < float(self.ticker().get('highestBid')) and place_order: + highest_bid = float(self.ticker().get('highestBid')) + if not self.is_instant_fill_enabled and price < highest_bid and highest_bid > 0 and place_order: self.log.info('Refusing to place an order which crosses highest bid') return None From a83372f176f2c4ca6a625f94ae2e73bc8786386d Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 7 Dec 2018 21:13:33 +0500 Subject: [PATCH 1078/1846] Add return description --- 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 0085651fa..2d78bc8a1 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -830,7 +830,8 @@ def increase_single_order(asset, order, new_order_amount): :param str | asset: 'base' or 'quote', depending if checking sell or buy :param order | order: order needed to be increased :param float | new_order_amount: BASE or QUOTE amount of a new order (depending on asset) - + :return bool | True = available funds was allocated, cannot allocate remainder + False = not all funds was allocated, can increase more orders next time """ quote_amount = 0 price = 0 From 0c2ae119df5ff64d312ee3899c409de461d03a50 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 8 Dec 2018 00:19:35 +0500 Subject: [PATCH 1079/1846] Change asset thresholds Instead of keeping a 1/20000 of total balance, keep 10x of asset precision --- dexbot/strategies/staggered_orders.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 2d78bc8a1..bc4ab83d7 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -201,9 +201,9 @@ def maintain_strategy(self, *args, **kwargs): # Calculate balances, and use orders from previous call of self.refresh_orders() to reduce API calls self.refresh_balances(use_cached_orders=True) - # Calculate asset thresholds - self.quote_asset_threshold = self.quote_total_balance / 20000 - self.base_asset_threshold = self.base_total_balance / 20000 + # Calculate asset thresholds once + if not (self.quote_asset_threshold or self.base_asset_threshold): + self.calculate_asset_thresholds() # Check market's price boundaries if self.market_center_price > self.upper_bound: @@ -400,6 +400,22 @@ def log_maintenance_time(self): delta = datetime.now() - self.start self.log.debug('Maintenance execution took: {:.2f} seconds'.format(delta.total_seconds())) + def calculate_asset_thresholds(self): + """ Calculate minimal asset thresholds to allocate. + + The goal is to avoid trying to allocate too small amounts which may lead to "Trying to buy/sell 0" + situations. + """ + # Keep at least N of precision + reserve_ratio = 10 + + if self.market['quote']['precision'] <= self.market['base']['precision']: + self.quote_asset_threshold = reserve_ratio * 10 ** -self.market['quote']['precision'] + self.base_asset_threshold = self.quote_asset_threshold * self.market_center_price + else: + self.base_asset_threshold = reserve_ratio * 10 ** -self.market['base']['precision'] + self.quote_asset_threshold = self.base_asset_threshold / self.market_center_price + def refresh_balances(self, total_balances=True, use_cached_orders=False): """ This function is used to refresh account balances :param bool | total_balances: refresh total balance or skip it From ed730911ff02e4694c7b16f1e90afc2ac5527e9d Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 10 Dec 2018 00:26:46 +0500 Subject: [PATCH 1080/1846] Require orders to match minimal allowed size Imagine you are trading orange/apple pair and you can trade only full apple or full orange. But by your settings you require 1% increment. So, if you have an order of "sell 1 apple for 1 orange", minimal increment is 100%, e.g. next price point is x2, e.g. "sell 2 apples for 1 orange". You cannot sell 1.01 apples because this is not allowed. But if your first order was "sell 100 apples for 100 oranges", you can increment it exactly by 1% easily: "sell 101 apples for 100 oranges". This change introduces such check and do not allow to place too small orders. --- dexbot/strategies/staggered_orders.py | 80 +++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index bc4ab83d7..75422dbc7 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -158,6 +158,10 @@ def __init__(self, *args, **kwargs): # We do not waiting for order ids to be able to bundle operations self.returnOrderId = None + # Minimal order amounts depending on defined increment + self.order_min_base = 0 + self.order_min_quote = 0 + # Minimal check interval is needed to prevent event queue accumulation self.min_check_interval = 1 self.max_check_interval = 120 @@ -201,6 +205,9 @@ def maintain_strategy(self, *args, **kwargs): # Calculate balances, and use orders from previous call of self.refresh_orders() to reduce API calls self.refresh_balances(use_cached_orders=True) + if not (self.order_min_base or self.order_min_quote): + self.calculate_min_amounts() + # Calculate asset thresholds once if not (self.quote_asset_threshold or self.base_asset_threshold): self.calculate_asset_thresholds() @@ -400,6 +407,12 @@ def log_maintenance_time(self): delta = datetime.now() - self.start self.log.debug('Maintenance execution took: {:.2f} seconds'.format(delta.total_seconds())) + def calculate_min_amounts(self): + """ Calculate minimal order amounts depending on defined increment + """ + self.order_min_base = 2 * 10 ** -self.market['base']['precision'] / self.increment + self.order_min_quote = 2 * 10 ** -self.market['quote']['precision'] / self.increment + def calculate_asset_thresholds(self): """ Calculate minimal asset thresholds to allocate. @@ -1305,26 +1318,42 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False limiter = quote_amount price = price ** -1 + # Make sure new order is bigger than allowed minimum + hard_limit = 0 + if place_order: + corrected_quote_amount = self.check_min_order_size(quote_amount, price) + if corrected_quote_amount > quote_amount: + self.log.debug('Correcting closer order amount to minimal allowed') + quote_amount = corrected_quote_amount + base_amount = quote_amount * price + if asset == 'base': + hard_limit = base_amount + elif asset == 'quote': + hard_limit = quote_amount + limiter = hard_limit + # Check whether new order will exceed available balance if balance < limiter: if place_order and not allow_partial: self.log.debug('Not enough balance to place closer {} order; need/avail: {:.8f}/{:.8f}' .format(order_type, limiter, balance)) place_order = False - elif allow_partial: + elif allow_partial and balance > hard_limit: self.log.debug('Limiting {} order amount to available asset balance: {} {}' .format(order_type, balance, symbol)) if asset == 'base': quote_amount = balance / price elif asset == 'quote': quote_amount = balance + else: + self.log.debug('Not enough balance to place minimal allowed order') + place_order = False if place_order and asset == 'base': virtual_bound = self.market_center_price / math.sqrt(1 + self.target_spread) orders_count = self.calc_buy_orders_count(virtual_bound, price) if orders_count > self.operational_depth and isinstance(order, VirtualOrder): # Allow to place closer order only if current is virtual - self.log.info('Placing virtual closer buy order') new_order = self.place_virtual_buy_order(quote_amount, price) else: @@ -1397,19 +1426,36 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals limiter = quote_amount price = price ** -1 + # Make sure new order is bigger than allowed minimum + hard_limit = 0 + if place_order: + corrected_quote_amount = self.check_min_order_size(quote_amount, price) + if corrected_quote_amount > quote_amount: + self.log.debug('Correcting further order amount to minimal allowed') + quote_amount = corrected_quote_amount + base_amount = quote_amount * price + if asset == 'base': + hard_limit = base_amount + elif asset == 'quote': + hard_limit = quote_amount + limiter = hard_limit + # Check whether new order will exceed available balance if balance < limiter: if place_order and not allow_partial: - self.log.debug('Not enough balance to place further {} order; need/avail: {:.8f}/{:.8f}' + self.log.debug('Not enough balance to place closer {} order; need/avail: {:.8f}/{:.8f}' .format(order_type, limiter, balance)) place_order = False - elif allow_partial: + elif allow_partial and balance > hard_limit: self.log.debug('Limiting {} order amount to available asset balance: {} {}' .format(order_type, balance, symbol)) if asset == 'base': quote_amount = balance / price elif asset == 'quote': quote_amount = balance + else: + self.log.debug('Not enough balance to place minimal allowed order') + place_order = False if place_order and asset == 'base': orders_count = self.calc_buy_orders_count(virtual_bound, price) @@ -1510,6 +1556,12 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) if place_order: + # Make sure new order is bigger than allowed minimum + corrected_amount = self.check_min_order_size(amount_quote, price) + if corrected_amount > amount_quote: + self.log.warning('Placing increased order because calculated size is less than allowed minimum') + amount_quote = corrected_amount + if sell_orders_count > self.operational_depth: order = self.place_virtual_sell_order(amount_quote, price) else: @@ -1630,6 +1682,12 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p amount_quote = int(float(amount_quote) * 10 ** precision) / (10 ** precision) if place_order: + # Make sure new order is bigger than allowed minimum + corrected_amount = self.check_min_order_size(amount_quote, price) + if corrected_amount > amount_quote: + self.log.warning('Placing increased order because calculated size is less than allowed minimum') + amount_quote = corrected_amount + if buy_orders_count > self.operational_depth: order = self.place_virtual_buy_order(amount_quote, price) else: @@ -1665,6 +1723,20 @@ def calc_sell_orders_count(self, price_low, price_high): price_low = price_low * (1 + self.increment) return orders_count + def check_min_order_size(self, amount, price): + """ Check if order size is less than minimal allowed size + + :param float | amount: Order amount in QUOTE + :param float | price: Order price in BASE + :return float | new_amount: passed amount or minimal allowed amount + """ + if (amount < self.order_min_quote or + amount * price < self.order_min_base): + self.log.debug('Too small order, base: {:.8f}/{:.8f}, quote: {}/{}' + .format(amount * price, self.order_min_base, amount, self.order_min_quote)) + return max(self.order_min_quote, self.order_min_base / price) + return amount + def place_virtual_buy_order(self, amount, price): """ Place a virtual buy order From 2b16bb8a104f9c40dad75c2860248bf3d8ba1cae Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 11 Dec 2018 14:04:11 +0200 Subject: [PATCH 1081/1846] Add external feed input fields to strategy form --- .../views/ui/forms/relative_orders_widget.ui | 262 +++++++++++++++--- 1 file changed, 227 insertions(+), 35 deletions(-) diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index 23af59351..e5055b871 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 449 - 597 + 675 @@ -32,7 +32,7 @@ Worker Parameters - + @@ -127,7 +127,7 @@ - + @@ -207,7 +207,7 @@ - + @@ -287,14 +287,14 @@ - + Relative order size - + @@ -389,7 +389,7 @@ - + @@ -420,7 +420,7 @@ - + @@ -500,14 +500,14 @@ - + Dynamic spread - + @@ -605,7 +605,7 @@ - + @@ -685,7 +685,7 @@ - + @@ -783,7 +783,7 @@ - + @@ -817,7 +817,7 @@ - + @@ -912,7 +912,7 @@ - + @@ -1010,7 +1010,7 @@ - + @@ -1090,7 +1090,7 @@ - + @@ -1109,7 +1109,7 @@ - + @@ -1207,7 +1207,7 @@ - + @@ -1287,7 +1287,7 @@ - + @@ -1367,14 +1367,14 @@ - + Center price offset based on asset balances - + @@ -1469,7 +1469,7 @@ - + @@ -1592,7 +1592,7 @@ QSlider::handle:horizontal { - + @@ -1672,14 +1672,14 @@ QSlider::handle:horizontal { - + Reset orders on partial fill - + @@ -1774,7 +1774,7 @@ QSlider::handle:horizontal { - + @@ -1805,7 +1805,7 @@ QSlider::handle:horizontal { - + @@ -1885,14 +1885,14 @@ QSlider::handle:horizontal { - + Reset orders on center price change - + @@ -1987,7 +1987,7 @@ QSlider::handle:horizontal { - + @@ -2018,7 +2018,7 @@ QSlider::handle:horizontal { - + @@ -2098,14 +2098,14 @@ QSlider::handle:horizontal { - + Custom expiration - + @@ -2200,7 +2200,7 @@ QSlider::handle:horizontal { - + @@ -2234,6 +2234,198 @@ QSlider::handle:horizontal { + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + External feed + + + amount_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + The bot will try to get price information from this source + + + ? + + + 5 + + + + + + + + + + true + + + -1 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Use external reference price instead of center price acquired from the market + + + ? + + + 5 + + + + + + + + + + Use external source for center price calculation + + + From 4e24893943b324f0ac6209fc8f8781f9c56ad5df Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 11 Dec 2018 14:04:42 +0200 Subject: [PATCH 1082/1846] Add onchange events for new input fields --- dexbot/controllers/strategy_controller.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index a4b6a3de1..9d90ba485 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -106,6 +106,7 @@ def __init__(self, view, configure, worker_controller, worker_data): widget = self.view.strategy_widget # Event connecting + widget.external_feed_input.clicked.connect(self.onchange_external_feed_input) widget.relative_order_size_input.clicked.connect(self.onchange_relative_order_size_input) widget.dynamic_spread_input.clicked.connect(self.onchange_dynamic_spread_input) widget.center_price_dynamic_input.clicked.connect(self.onchange_center_price_dynamic_input) @@ -115,6 +116,7 @@ def __init__(self, view, configure, worker_controller, worker_data): widget.custom_expiration_input.clicked.connect(self.onchange_custom_expiration_input) # Trigger the onchange events once + self.onchange_external_feed_input(widget.external_feed_input.isChecked()) self.onchange_relative_order_size_input(widget.relative_order_size_input.isChecked()) self.onchange_center_price_dynamic_input(widget.center_price_dynamic_input.isChecked()) self.onchange_dynamic_spread_input(widget.dynamic_spread_input.isChecked()) @@ -130,6 +132,15 @@ def values(self): values['manual_offset'] = values['manual_offset'] / 10 return values + def onchange_external_feed_input(self, checked): + if checked: + self.view.strategy_widget.external_price_source_input.setDisabled(False) + + self.view.strategy_widget.reset_on_price_change_input.setChecked(False) + self.view.strategy_widget.price_change_threshold_input.setDisabled(True) + else: + self.view.strategy_widget.external_price_source_input.setDisabled(True) + def onchange_manual_offset_input(self): value = self.view.strategy_widget.manual_offset_input.value() / 10 text = "{}%".format(value) @@ -176,6 +187,10 @@ def onchange_reset_on_partial_fill_input(self, checked): def onchange_reset_on_price_change_input(self, checked): if checked and self.view.strategy_widget.center_price_dynamic_input.isChecked(): self.view.strategy_widget.price_change_threshold_input.setDisabled(False) + + # Disable external price feed + self.view.strategy_widget.external_feed_input.setChecked(False) + self.view.strategy_widget.external_price_source_input.setDisabled(True) else: self.view.strategy_widget.price_change_threshold_input.setDisabled(True) From a7015edf8e411ac8ab351d143aca11bc408915d4 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 11 Dec 2018 14:05:21 +0200 Subject: [PATCH 1083/1846] Refactor base.py by moving EXCHANGES --- dexbot/strategies/base.py | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 215e68e96..f8a159104 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -71,6 +71,17 @@ """ DetailElement = collections.namedtuple('DetailTab', 'type name title file') +# External exchanges used to calculate center price +EXCHANGES = [ + # ('none', 'None. Use Manual or Bitshares DEX Price (default)'), + ('gecko', 'Coingecko'), + ('waves', 'Waves DEX'), + ('kraken', 'Kraken'), + ('bitfinex', 'Bitfinex'), + ('gdax', 'Gdax'), + ('binance', 'Binance') +] + class StrategyBase(BaseStrategy, Storage, StateMachine, Events): """ A strategy based on this class is intended to work in one market. This class contains @@ -137,17 +148,6 @@ def configure(cls, return_base_config=True): :return: Returns a list of config elements """ - # External exchanges used to calculate center price - exchanges = [ - ('none', 'None. Use Manual or Bitshares DEX Price (default)'), - ('gecko', 'Coingecko'), - ('waves', 'Waves DEX'), - ('kraken', 'Kraken'), - ('bitfinex', 'Bitfinex'), - ('gdax', 'Gdax'), - ('binance', 'Binance') - ] - # Common configs base_config = [ ConfigElement('account', 'string', '', 'Account', @@ -156,8 +156,6 @@ def configure(cls, return_base_config=True): ConfigElement('market', 'string', 'USD:BTS', 'Market', 'BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"', r'[A-Z\.]+[:\/][A-Z\.]+'), - ConfigElement('external_center_price_source', 'choice', exchanges[0], 'External Source', - 'External Price Source, select one', exchanges), ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset', 'Asset to be used to pay transaction fees', r'[A-Z\.]+') @@ -249,10 +247,7 @@ def __init__(self, # Count of orders to be fetched from the API self.fetch_depth = 8 - - # Set external price source - self.external_price_source = self.worker.get('external_center_price_source') - + # Set fee asset fee_asset_symbol = self.worker.get('fee_asset') @@ -610,15 +605,16 @@ def get_lowest_own_sell_order(self, orders=None): except IndexError: return None - def get_external_market_center_price(self): + def get_external_market_center_price(self, external_price_source): center_price = None - self.log.debug('inside get_external_mcp, exchange: {} '.format(self.external_price_source)) + self.log.debug('inside get_external_mcp, exchange: {} '.format(external_price_source)) market = self.market.get_string('/') self.log.debug('market: {} '.format(market)) - price_feed = PriceFeed(self.external_price_source, market) + price_feed = PriceFeed(external_price_source, market) price_feed.filter_symbols() center_price = price_feed.get_center_price(None) self.log.debug('PriceFeed: {}'.format(center_price)) + if center_price is None: # Try USDT center_price = price_feed.get_center_price("USDT") self.log.debug('Substitute USD/USDT center price: {}'.format(center_price)) From 18f235a4a7d974819342eb95b1ea95f80f6d17c1 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 11 Dec 2018 14:06:11 +0200 Subject: [PATCH 1084/1846] Refactor Relative Orders slightly --- dexbot/strategies/relative_orders.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index ee3852e45..4c6c9f39f 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -1,7 +1,7 @@ import math from datetime import datetime, timedelta -from dexbot.strategies.base import StrategyBase, ConfigElement, DetailElement +from dexbot.strategies.base import StrategyBase, ConfigElement, DetailElement, EXCHANGES from dexbot.qt_queue.idle_queue import idle_add @@ -12,6 +12,10 @@ class Strategy(StrategyBase): @classmethod def configure(cls, return_base_config=True): return StrategyBase.configure(return_base_config) + [ + ConfigElement('external_price_source', 'choice', EXCHANGES[0], 'External price source', + 'The bot will try to get price information from this source', EXCHANGES), + ConfigElement('external_feed', 'bool', False, 'External price feed', + 'Use external reference price instead of center price acquired from the market', None), ConfigElement('amount', 'float', 1, 'Amount', 'Fixed order size, expressed in quote asset, unless "relative order size" selected', (0, None, 8, '')), @@ -44,7 +48,8 @@ def configure(cls, return_base_config=True): ConfigElement('partial_fill_threshold', 'float', 30, 'Fill threshold', 'Order fill threshold to reset orders', (0, 100, 2, '%')), ConfigElement('reset_on_price_change', 'bool', False, 'Reset orders on center price change', - 'Reset orders when center price is changed more than threshold (set False for external feeds)', None), + 'Reset orders when center price is changed more than threshold ' + '(set False for external feeds)', None), ConfigElement('price_change_threshold', 'float', 2, 'Price change threshold', 'Define center price threshold to react on', (0, 100, 2, '%')), ConfigElement('custom_expiration', 'bool', False, 'Custom expiration', @@ -80,19 +85,27 @@ def __init__(self, *args, **kwargs): if not self.market_center_price: self.empty_market = True - + + # Set external price source, defaults to False if not found + self.external_feed = self.worker.get('external_feed', False) + self.external_price_source = self.worker.get('external_price_source', None) + # Worker parameters self.is_center_price_dynamic = self.worker['center_price_dynamic'] + if self.is_center_price_dynamic: self.center_price = None self.center_price_depth = self.worker.get('center_price_depth', 0) else: - external_source = self.external_price_source - if external_source != 'none': - self.center_price = self.get_external_market_center_price() + if self.external_feed: + # Try getting center price from external source + self.center_price = self.get_external_market_center_price(self.external_price_source) + if self.center_price is None: - self.center_price = self.worker["center_price"] # Set as manual + # Use manual center price as fallback + self.center_price = self.worker["center_price"] else: + # Use manually set center price self.center_price = self.worker["center_price"] self.is_relative_order_size = self.worker.get('relative_order_size', False) From be75807d5dcc373f6ec8e581fbcbf2a19b630c6c Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 11 Dec 2018 14:59:39 +0200 Subject: [PATCH 1085/1846] Change external feed fields behavior --- dexbot/controllers/strategy_controller.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 9d90ba485..186a1dd0d 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -169,6 +169,10 @@ def onchange_center_price_dynamic_input(self, checked): self.view.strategy_widget.center_price_input.setDisabled(True) self.view.strategy_widget.center_price_depth_input.setDisabled(False) self.view.strategy_widget.reset_on_price_change_input.setDisabled(False) + self.view.strategy_widget.external_feed_input.setEnabled(True) + + if self.view.strategy_widget.external_feed_input.isChecked(): + self.view.strategy_widget.external_price_source_input.setEnabled(True) if self.view.strategy_widget.reset_on_price_change_input.isChecked(): self.view.strategy_widget.price_change_threshold_input.setDisabled(False) @@ -177,6 +181,9 @@ def onchange_center_price_dynamic_input(self, checked): self.view.strategy_widget.center_price_depth_input.setDisabled(True) self.view.strategy_widget.reset_on_price_change_input.setDisabled(True) self.view.strategy_widget.price_change_threshold_input.setDisabled(True) + self.view.strategy_widget.external_feed_input.setChecked(False) + self.view.strategy_widget.external_feed_input.setDisabled(True) + self.view.strategy_widget.external_price_source_input.setDisabled(True) def onchange_reset_on_partial_fill_input(self, checked): if checked: From 4cb887c35924c28295a472963dd628db4fec755f Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 6 Dec 2018 20:15:48 +0500 Subject: [PATCH 1086/1846] Remove old BaseStrategy Relative Orders and Staggered Orders was migrated to StrategyBase, so old BaseStrategy class may be removed. --- dexbot/basestrategy.py | 909 -------------------------- dexbot/strategies/base.py | 13 +- dexbot/strategies/staggered_orders.py | 2 +- 3 files changed, 12 insertions(+), 912 deletions(-) delete mode 100644 dexbot/basestrategy.py diff --git a/dexbot/basestrategy.py b/dexbot/basestrategy.py deleted file mode 100644 index 548457bd3..000000000 --- a/dexbot/basestrategy.py +++ /dev/null @@ -1,909 +0,0 @@ -import datetime -import logging -import collections -import time -import math -import copy - -from .storage import Storage -from .statemachine import StateMachine -from .config import Config -from .helper import truncate - -from events import Events -import bitsharesapi -import bitsharesapi.exceptions -import bitshares.exceptions -from bitshares.amount import Amount -from bitshares.amount import Asset -from bitshares.market import Market -from bitshares.account import Account -from bitshares.price import FilledOrder, Order, UpdateCallOrder -from bitshares.instance import shared_bitshares_instance - -MAX_TRIES = 3 - -ConfigElement = collections.namedtuple('ConfigElement', 'key type default title description extra') -# Strategies need to specify their own configuration values, so each strategy can have -# a class method 'configure' which returns a list of ConfigElement named tuples. -# Tuple fields as follows: -# - Key: the key in the bot config dictionary that gets saved back to config.yml -# - Type: one of "int", "float", "bool", "string", "choice" -# - Default: the default value. must be right type. -# - Title: name shown to the user, preferably not too long -# - Description: comments to user, full sentences encouraged -# - Extra: -# For int: a (min, max, suffix) tuple -# For float: a (min, max, precision, suffix) tuple -# For string: a regular expression, entries must match it, can be None which equivalent to .* -# For bool, ignored -# For choice: a list of choices, choices are in turn (tag, label) tuples. -# labels get presented to user, and tag is used as the value saved back to the config dict - - -class BaseStrategy(Storage, StateMachine, Events): - """ Base Strategy and methods available in all Sub Classes that - inherit this BaseStrategy. - - BaseStrategy inherits: - - * :class:`dexbot.storage.Storage` - * :class:`dexbot.statemachine.StateMachine` - * ``Events`` - - Available attributes: - - * ``basestrategy.bitshares``: instance of ´`bitshares.BitShares()`` - * ``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 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, BaseStrategy inherits :class:`dexbot.storage.Storage` - which allows to permanently store data in a sqlite database - using: - - ``basestrategy["key"] = "value"`` - - .. note:: This applies a ``json.loads(json.dumps(value))``! - - 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. - """ - - __events__ = [ - 'ontick', - 'onMarketUpdate', - 'onAccount', - 'error_ontick', - 'error_onMarketUpdate', - 'error_onAccount', - 'onOrderMatched', - 'onOrderPlaced', - 'onUpdateCallOrder', - ] - - @classmethod - def configure(cls, return_base_config=True): - """ - Return a list of ConfigElement objects defining the configuration values for - this class - User interfaces should then generate widgets based on this values, gather - data and save back to the config dictionary for the worker. - - NOTE: when overriding you almost certainly will want to call the ancestor - and then add your config values to the list. - """ - - # External exchanges used to calculate center price - exchanges = [ - ('gecko', 'coingecko'), - ('ccxt-kraken', 'kraken'), - ('ccxt-bitfinex', 'bitfinex'), - ('ccxt-gdax', 'gdax'), - ('ccxt-binance', 'binance') - ] - - # These configs are common to all bots - base_config = [ - ConfigElement('account', 'string', '', 'Account', - 'BitShares account name for the bot to operate with', - ''), - ConfigElement('market', 'string', 'USD:BTS', 'Market', - 'BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"', - r'[A-Z\.]+[:\/][A-Z\.]+'), - ConfigElement('external_center_price', 'bool', True, - 'Use External center price (if not available, defaults to manual center price)', - 'External center price expressed in base asset: BASE/QUOTE', None), - ConfigElement('external_center_price_source', 'choice', 'gecko', 'External Source', - 'External Price Source, select one', exchanges), - ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset', - 'Asset to be used to pay transaction fees', - r'[A-Z\.]+') - ] - if return_base_config: - return base_config - return [] - - def __init__( - self, - name, - config=None, - onAccount=None, - onOrderMatched=None, - onOrderPlaced=None, - onMarketUpdate=None, - onUpdateCallOrder=None, - ontick=None, - bitshares_instance=None, - *args, - **kwargs - ): - # BitShares instance - self.bitshares = bitshares_instance or shared_bitshares_instance() - - # Storage - Storage.__init__(self, name) - - # Statemachine - StateMachine.__init__(self, name) - - # Events - Events.__init__(self) - - if ontick: - self.ontick += ontick - if onMarketUpdate: - self.onMarketUpdate += onMarketUpdate - if onAccount: - self.onAccount += onAccount - if onOrderMatched: - self.onOrderMatched += onOrderMatched - if onOrderPlaced: - self.onOrderPlaced += onOrderPlaced - if onUpdateCallOrder: - self.onUpdateCallOrder += onUpdateCallOrder - - # Redirect this event to also call order placed and order matched - self.onMarketUpdate += self._callbackPlaceFillOrders - - if config: - self.config = config - else: - self.config = config = Config.get_worker_config_file(name) - - self.worker = config["workers"][name] - self._account = Account( - self.worker["account"], - full=True, - bitshares_instance=self.bitshares - ) - self._market = Market( - config["workers"][name]["market"], - bitshares_instance=self.bitshares - ) - - # Recheck flag - Tell the strategy to check for updated orders - self.recheck_orders = False - - # Set external price source - if self.worker.get('external_center_price', False): - self.external_price_source = self.worker.get('external_center_price_source') - - # Set fee asset - fee_asset_symbol = self.worker.get('fee_asset') - if fee_asset_symbol: - try: - self.fee_asset = Asset(fee_asset_symbol) - except bitshares.exceptions.AssetDoesNotExistsException: - self.fee_asset = Asset('1.3.0') - else: - self.fee_asset = Asset('1.3.0') - - # 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 - # will be reset to False after reset only - self.disabled = False - - # Order expiration time in seconds - self.expiration = 60 * 60 * 24 * 365 * 5 - - # buy/sell actions will return order id by default - self.returnOrderId = 'head' - - # CER cache - self.core_exchange_rate = None - - # 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} - ) - - self.orders_log = logging.LoggerAdapter( - logging.getLogger('dexbot.orders_log'), {} - ) - - 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: - 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: - if not suppress_errors: - self.log.critical( - "Cannot estimate center price, there is no lowest ask." - ) - self.disabled = True - return None - - center_price = highest_bid['price'] * math.sqrt(lowest_ask['price'] / highest_bid['price']) - return center_price - - def calculate_center_price(self, center_price=None, asset_offset=False, spread=None, - order_ids=None, manual_offset=0, suppress_errors=False): - """ Calculate center price which shifts based on available funds - """ - if center_price is None: - # No center price was given so we simply calculate the center price - calculated_center_price = self._calculate_center_price(suppress_errors) - else: - # 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 center_price: - calculated_center_price = center_price - - if asset_offset: - total_balance = self.total_balance(order_ids) - total = (total_balance['quote'] * calculated_center_price) + total_balance['base'] - - if not total: # Prevent division by zero - balance = 0 - else: - # Returns a value between -1 and 1 - balance = (total_balance['base'] / total) * 2 - 1 - - if balance < 0: - # With less of base asset center price should be offset downward - calculated_center_price = calculated_center_price / math.sqrt(1 + spread * (balance * -1)) - elif balance > 0: - # With more of base asset center price will be offset upwards - calculated_center_price = calculated_center_price * math.sqrt(1 + spread * balance) - else: - calculated_center_price = calculated_center_price - - # Calculate final_offset_price if manual center price offset is given - if manual_offset: - calculated_center_price = calculated_center_price + (calculated_center_price * manual_offset) - - return calculated_center_price - - @property - def orders(self): - """ Return the account's open orders in the current market - """ - self.account.refresh() - return [o for o in self.account.openorders if self.worker["market"] == o.market and self.account.openorders] - - @property - def all_orders(self): - """ Return the accounts's open orders in all markets - """ - self.account.refresh() - return [o for o in self.account.openorders] - - def get_buy_orders(self, sort=None, orders=None): - """ Return buy orders - :param str sort: DESC or ASC will sort the orders accordingly, default None. - :param list orders: List of orders. If None given get all orders from Blockchain. - :return list buy_orders: List of buy orders only. - """ - buy_orders = [] - - if not orders: - orders = self.orders - - # Find buy orders - for order in orders: - if self.is_buy_order(order): - buy_orders.append(order) - if sort: - buy_orders = self.sort_orders(buy_orders, sort) - - return buy_orders - - def get_sell_orders(self, sort=None, orders=None): - """ Return sell orders - :param str sort: DESC or ASC will sort the orders accordingly, default None. - :param list orders: List of orders. If None given get all orders from Blockchain. - :return list sell_orders: List of sell orders only. - """ - sell_orders = [] - - if not orders: - orders = self.orders - - # Find sell orders - for order in orders: - if self.is_sell_order(order): - sell_orders.append(order) - - if sort: - sell_orders = self.sort_orders(sell_orders, sort) - - return sell_orders - - def is_buy_order(self, order): - """ Checks if the order is Buy order - :param order: Buy / Sell order - :return: bool: True = Buy order - """ - if order['base']['symbol'] == self.market['base']['symbol']: - return True - return False - - def is_sell_order(self, order): - """ Checks if the order is Sell order - :param order: Buy / Sell order - :return: bool: True = Sell order - """ - if order['base']['symbol'] != self.market['base']['symbol']: - return True - return False - - @staticmethod - def sort_orders(orders, sort='DESC'): - """ Return list of orders sorted ascending or descending - :param list orders: list of orders to be sorted - :param str sort: ASC or DESC. Default DESC - :return list: Sorted list of orders. - """ - if sort == 'ASC': - reverse = False - elif sort == 'DESC': - reverse = True - else: - return None - - # Sort orders by price - return sorted(orders, key=lambda order: order['price'], reverse=reverse) - - def get_order_cancellation_fee(self, fee_asset): - """ Returns the order cancellation fee in the specified asset. - - :param string | fee_asset: Asset in which the fee is wanted - :return: Cancellation fee as fee asset - """ - # Get fee - fees = self.dex.returnFees() - limit_order_cancel = fees['limit_order_cancel'] - return self.convert_fee(limit_order_cancel['fee'], fee_asset) - - def get_order_creation_fee(self, fee_asset): - """ Returns the cost of creating an order in the asset specified - - :param fee_asset: QUOTE, BASE, BTS, or any other - :return: - """ - # Get fee - fees = self.dex.returnFees() - limit_order_create = fees['limit_order_create'] - return self.convert_fee(limit_order_create['fee'], fee_asset) - - @staticmethod - def get_order(order_id, return_none=True): - """ Returns the Order object for the order_id - - :param str|dict order_id: blockchain object id of the order - can be an order 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 not order_id: - return None - if 'id' in order_id: - order_id = order_id['id'] - order = Order(order_id) - if return_none and order['deleted']: - return None - return order - - def get_updated_order(self, order_id): - """ Tries to get the updated order from the API - returns None if the order doesn't exist - - :param str|dict order_id: blockchain object id of the order - can be an order dict with the id key in it - """ - if isinstance(order_id, dict): - order_id = order_id['id'] - - # Get the limited order by id - order = None - for limit_order in self.account['limit_orders']: - if order_id == limit_order['id']: - order = limit_order - break - else: - return order - - # Do not try to continue whether there is no order in the blockchain - if not order: - return None - - order = self.get_updated_limit_order(order) - return Order(order, bitshares_instance=self.bitshares) - - @property - def updated_orders(self): - """ Returns all open orders as updated orders - """ - self.account.refresh() - - limited_orders = [] - for order in self.account['limit_orders']: - base_asset_id = order['sell_price']['base']['asset_id'] - quote_asset_id = order['sell_price']['quote']['asset_id'] - # Check if the order is in the current market - if not self.is_current_market(base_asset_id, quote_asset_id): - continue - - limited_orders.append(self.get_updated_limit_order(order)) - - return [ - Order(o, bitshares_instance=self.bitshares) - for o in limited_orders - ] - - @staticmethod - def get_updated_limit_order(limit_order): - """ Returns a modified limit_order so that when passed to Order class, - will return an Order object with updated amount values - :param limit_order: an item of Account['limit_orders'] - :return: dict - """ - o = copy.deepcopy(limit_order) - price = float(o['sell_price']['base']['amount']) / float(o['sell_price']['quote']['amount']) - base_amount = float(o['for_sale']) - quote_amount = base_amount / price - o['sell_price']['base']['amount'] = base_amount - o['sell_price']['quote']['amount'] = quote_amount - return o - - @property - def market(self): - """ Return the market object as :class:`bitshares.market.Market` - """ - return self._market - - @property - def account(self): - """ Return the full account as :class:`bitshares.account.Account` object! - - Can be refreshed by using ``x.refresh()`` - """ - return self._account - - def balance(self, asset): - """ Return the balance of your worker's account for a specific asset - """ - return self._account.balance(asset) - - @property - def balances(self): - """ Return the balances of your worker's account - """ - return self._account.balances - - def _callbackPlaceFillOrders(self, d): - """ This method distinguishes notifications caused by Matched orders - from those caused by placed orders - """ - if isinstance(d, FilledOrder): - self.onOrderMatched(d) - elif isinstance(d, Order): - self.onOrderPlaced(d) - elif isinstance(d, UpdateCallOrder): - self.onUpdateCallOrder(d) - else: - pass - - def execute(self): - """ Execute a bundle of operations - """ - self.bitshares.blocking = "head" - r = self.bitshares.txbuffer.broadcast() - self.bitshares.blocking = False - return r - - def _cancel(self, orders): - try: - self.retry_action( - self.bitshares.cancel, - orders, account=self.account, fee_asset=self.fee_asset['id'] - ) - except bitsharesapi.exceptions.UnhandledRPCError as e: - if str(e).startswith('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: - self.log.exception("Unable to cancel order") - except bitshares.exceptions.MissingKeyError: - self.log.exception('Unable to cancel order(s), private key missing.') - - return True - - def cancel(self, orders, batch_only=False): - """ Cancel specific order(s) - :param list orders: list of orders to cancel - :param bool batch_only: try cancel orders only in batch mode without one-by-one fallback - """ - if not isinstance(orders, (list, set, tuple)): - orders = [orders] - - orders = [order['id'] for order in orders if 'id' in order] - - success = self._cancel(orders) - if not success and batch_only: - return False - if not success and len(orders) > 1 and not batch_only: - # One of the order cancels failed, cancel the orders one by one - for order in orders: - self._cancel(order) - return True - - def cancel_all(self): - """ Cancel all orders of the worker's account - """ - self.log.info('Canceling all orders') - if self.orders: - self.cancel(self.orders) - self.log.info("Orders canceled") - - def pause(self): - """ Pause the worker - """ - # By default, just call cancel_all(); strategies may override this method - self.cancel_all() - self.clear_orders() - - def market_buy(self, quote_amount, price, return_none=False, *args, **kwargs): - symbol = self.market['base']['symbol'] - precision = self.market['base']['precision'] - base_amount = truncate(price * quote_amount, precision) - - # Don't try to place an order of size 0 - if not base_amount: - self.log.critical('Trying to buy 0') - self.disabled = True - return None - - # Make sure we have enough balance for the order - if self.returnOrderId and self.balance(self.market['base']) < base_amount: - self.log.critical( - "Insufficient buy balance, needed {} {}".format( - base_amount, symbol) - ) - self.disabled = True - return None - - self.log.info( - 'Placing a buy order for {:.{prec}} {} @ {:.8f}'.format( - base_amount, symbol, price, prec=precision) - ) - - # Place the order - buy_transaction = self.retry_action( - self.market.buy, - price, - Amount(amount=quote_amount, asset=self.market["quote"]), - account=self.account.name, - expiration=self.expiration, - returnOrderId=self.returnOrderId, - fee_asset=self.fee_asset['id'], - *args, - **kwargs - ) - - self.log.debug('Placed buy order {}'.format(buy_transaction)) - - if self.returnOrderId: - buy_order = self.get_order(buy_transaction['orderid'], return_none=return_none) - if buy_order and 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, quote_amount, price) - self.recheck_orders = True - return buy_order - else: - return True - - def market_sell(self, quote_amount, price, return_none=False, *args, **kwargs): - symbol = self.market['quote']['symbol'] - precision = self.market['quote']['precision'] - quote_amount = truncate(quote_amount, precision) - - # Don't try to place an order of size 0 - if not quote_amount: - self.log.critical('Trying to sell 0') - self.disabled = True - return None - - # Make sure we have enough balance for the order - if self.returnOrderId and self.balance(self.market['quote']) < quote_amount: - self.log.critical( - "Insufficient sell balance, needed {} {}".format( - quote_amount, symbol) - ) - self.disabled = True - return None - - self.log.info( - 'Placing a sell order for {:.{prec}f} {} @ {:.8f}'.format( - quote_amount, symbol, price, prec=precision) - ) - - # Place the order - sell_transaction = self.retry_action( - self.market.sell, - price, - Amount(amount=quote_amount, asset=self.market["quote"]), - account=self.account.name, - expiration=self.expiration, - returnOrderId=self.returnOrderId, - fee_asset=self.fee_asset['id'], - *args, - **kwargs - ) - - self.log.debug('Placed sell order {}'.format(sell_transaction)) - if self.returnOrderId: - sell_order = self.get_order(sell_transaction['orderid'], return_none=return_none) - if sell_order and 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, quote_amount, price) - sell_order.invert() - self.recheck_orders = True - return sell_order - else: - return True - - 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 is_current_market(self, base_asset_id, quote_asset_id): - """ Returns True if given asset id's are of the current market - """ - if quote_asset_id == self.market['quote']['id']: - if base_asset_id == self.market['base']['id']: - return True - return False - if quote_asset_id == self.market['base']['id']: - if base_asset_id == self.market['quote']['id']: - return True - return False - return False - - def purge(self): - """ Clear all the worker data from the database and cancel all orders - """ - self.clear_orders() - self.cancel_all() - self.clear() - - @staticmethod - def purge_worker_data(worker_name): - """ Remove worker data from database only """ - Storage.clear_worker_data(worker_name) - - 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'] - - # Total balance calculation - for balance in self.balances: - if balance.asset['id'] == quote_asset: - quote += balance['amount'] - elif balance.asset['id'] == base_asset: - base += balance['amount'] - - if order_ids is None: - # Get all orders from Blockchain - order_ids = [order['id'] for order in self.orders] - if order_ids: - 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 account_total_value(self, return_asset): - """ Returns the total value of the account in given asset - :param str return_asset: Asset which is wanted as return - :return: float: Value of the account in one asset - """ - total_value = 0 - - # Total balance calculation - for balance in self.balances: - if balance['symbol'] != return_asset: - # Convert to asset if different - total_value += self.convert_asset(balance['amount'], balance['symbol'], return_asset) - else: - total_value += balance['amount'] - - # Orders balance calculation - for order in self.all_orders: - updated_order = self.get_updated_order(order['id']) - - if not order: - continue - if updated_order['base']['symbol'] == return_asset: - total_value += updated_order['base']['amount'] - else: - total_value += self.convert_asset( - updated_order['base']['amount'], - updated_order['base']['symbol'], - return_asset - ) - - return total_value - - @staticmethod - def convert_asset(from_value, from_asset, to_asset): - """Converts asset to another based on the latest market value - :param from_value: Amount of the input asset - :param from_asset: Symbol of the input asset - :param to_asset: Symbol of the output asset - :return: Asset converted to another asset as float value - """ - market = Market('{}/{}'.format(from_asset, to_asset)) - ticker = market.ticker() - latest_price = ticker.get('latest', {}).get('price', None) - return from_value * latest_price - - def convert_fee(self, fee_amount, fee_asset): - """ Convert fee amount in BTS to fee in fee_asset - - :param float | fee_amount: fee amount paid in BTS - :param Asset | fee_asset: fee asset to pay fee in - :return: float | amount of fee_asset to pay fee - """ - if isinstance(fee_asset, str): - fee_asset = Asset(fee_asset) - - if fee_asset['id'] == '1.3.0': - # Fee asset is BTS, so no further calculations are needed - return fee_amount - else: - if not self.core_exchange_rate: - # Determine how many fee_asset is needed for core-exchange - temp_market = Market(base=fee_asset, quote=Asset('1.3.0')) - self.core_exchange_rate = temp_market.ticker()['core_exchange_rate'] - return fee_amount * self.core_exchange_rate['base']['amount'] - - 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} - - 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.warning("Ignoring: '{}'".format(str(e))) - 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 - - def write_order_log(self, worker_name, order): - operation_type = 'TRADE' - - if order['base']['symbol'] == self.market['base']['symbol']: - base_symbol = order['base']['symbol'] - base_amount = -order['base']['amount'] - quote_symbol = order['quote']['symbol'] - quote_amount = order['quote']['amount'] - else: - base_symbol = order['quote']['symbol'] - base_amount = order['quote']['amount'] - quote_symbol = order['base']['symbol'] - quote_amount = -order['base']['amount'] - - message = '{};{};{};{};{};{};{};{}'.format( - worker_name, - order['id'], - operation_type, - base_symbol, - base_amount, - quote_symbol, - quote_amount, - datetime.datetime.now().isoformat() - ) - - self.orders_log.info(message) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 215e68e96..ec3083f6f 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -5,7 +5,6 @@ import math import time -from dexbot.basestrategy import BaseStrategy # Todo: Once the old BaseStrategy deprecates, remove it. from dexbot.config import Config from dexbot.storage import Storage from dexbot.statemachine import StateMachine @@ -72,7 +71,7 @@ DetailElement = collections.namedtuple('DetailTab', 'type name title file') -class StrategyBase(BaseStrategy, Storage, StateMachine, Events): +class StrategyBase(Storage, StateMachine, Events): """ A strategy based on this class is intended to work in one market. This class contains most common methods needed by the strategy. @@ -981,6 +980,16 @@ def get_updated_order(self, order_id): updated_order = self.get_updated_limit_order(order) return Order(updated_order, bitshares_instance=self.bitshares) + def execute(self): + """ Execute a bundle of operations + + :return: dict: transaction + """ + self.bitshares.blocking = "head" + r = self.bitshares.txbuffer.broadcast() + self.bitshares.blocking = False + return r + def is_buy_order(self, order): """ Check whether an order is buy order diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 07c1ccdec..ec5a5d0d5 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1769,7 +1769,7 @@ def error(self, *args, **kwargs): self.disabled = True def pause(self): - """ Override pause() in BaseStrategy """ + """ Override pause() """ pass def purge(self): From 2166ebf2c9e18be1dfb7887fd15c923368041f21 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 11 Dec 2018 13:03:15 +0500 Subject: [PATCH 1087/1846] Fix error message Closes: #410 --- 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 75422dbc7..7cd389aef 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -115,7 +115,7 @@ def __init__(self, *args, **kwargs): self.center_price = self.worker['center_price'] if self.target_spread < self.increment: - self.log.error('Spread is more than increment, refusing to work because worker will make losses') + self.log.error('Spread must be more than increment, refusing to work because worker will make losses') self.disabled = True if self.operational_depth < 2: From af85d1f03b984e214b99e9c784c5ac82f6d6c1b0 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 12 Dec 2018 09:36:20 +0200 Subject: [PATCH 1088/1846] Change width of external feed source field --- dexbot/views/ui/forms/relative_orders_widget.ui | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index e5055b871..cb9566eb1 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 449 - 675 + 687 @@ -2334,6 +2334,12 @@ QSlider::handle:horizontal { true + + + 170 + 16777215 + + -1 From 5beeb75855485df29f9d3e44c7e2c6f538694730 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 12 Dec 2018 13:34:23 +0200 Subject: [PATCH 1089/1846] Remove extra space PEP8 warning --- dexbot/strategies/staggered_orders.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 7cd389aef..38a023871 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1084,7 +1084,6 @@ def increase_single_order(asset, order, new_order_amount): if need_increase: return increase_single_order(asset, order, new_order_amount) - elif self.mode == 'neutral': """ Starting from the furthest order, for each order, see if it is approximately maximum size. From 3c3432a16a2ceb25d5e2401f251080542b15bf96 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 12 Dec 2018 13:35:44 +0200 Subject: [PATCH 1090/1846] Change dexbot version number to 0.8.2 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index b194e6596..f896f1ed7 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.8.1' +VERSION = '0.8.2' AUTHOR = 'Codaone Oy' __version__ = VERSION From 53e0e7d79ada316b4e3b887496b7b3291ff32215 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 12 Dec 2018 14:05:58 +0200 Subject: [PATCH 1091/1846] Fix crash on Staggered Orders --- 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 06f587d22..33dd4ce62 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1844,7 +1844,7 @@ def update_gui_slider(self): else: order_ids = None - total_balance = self.total_balance(order_ids) + total_balance = self.count_asset(order_ids) total = (total_balance['quote'] * latest_price) + total_balance['base'] # Prevent division by zero From e56ef3a3eff9abffe5e1491cd80624a06fc141ec Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 12 Dec 2018 14:11:38 +0200 Subject: [PATCH 1092/1846] Remove walls.py strategy --- dexbot/strategies/walls.py | 137 ------------------------------------- 1 file changed, 137 deletions(-) delete mode 100644 dexbot/strategies/walls.py diff --git a/dexbot/strategies/walls.py b/dexbot/strategies/walls.py deleted file mode 100644 index 2b6e4d91f..000000000 --- a/dexbot/strategies/walls.py +++ /dev/null @@ -1,137 +0,0 @@ -from math import fabs -from collections import Counter -from bitshares.amount import Amount -from dexbot.basestrategy import BaseStrategy, ConfigElement -from dexbot.errors import InsufficientFundsError - - -class Strategy(BaseStrategy): - """ Walls strategy - """ - - @classmethod - def configure(cls, return_base_config=True): - return BaseStrategy.configure(return_base_config) + [ - ConfigElement("spread", "float", 5, "Spread", - "The spread between sell and buy as percentage", (0, 100, 2, '%')), - ConfigElement("threshold", "float", 5, "Threshold", - "Percentage the feed has to move before we change orders", (0, 100, 2, '%')), - ConfigElement("buy", "float", 0, "Buy", - "The default amount to buy", (0, None, 8, '')), - ConfigElement("sell", "float", 0, "Sell", - "The default amount to sell", (0, None, 8, '')), - ConfigElement("blocks", "int", 20, "Block num", - "Number of blocks to wait before re-calculating", (0, 10000, '')) - ] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Define Callbacks - self.onMarketUpdate += self.test - self.ontick += self.tick - self.onAccount += self.test - - self.error_ontick = self.error - self.error_onMarketUpdate = self.error - self.error_onAccount = self.error - - # Counter for blocks - self.counter = Counter() - - # Tests for actions - self.test_blocks = self.worker.get("test", {}).get("blocks", 0) - - def error(self, *args, **kwargs): - self.disabled = True - self.cancelall() - self.log.info(self.execute()) - - def updateorders(self): - """ Update the orders - """ - self.log.info("Replacing orders") - - # Canceling orders - self.cancel_all() - - # Target - target = self.worker.get("target", {}) - price = self.getprice() - - # prices - buy_price = price * (1 - target["offsets"]["buy"] / 100) - sell_price = price * (1 + target["offsets"]["sell"] / 100) - - # Store price in storage for later use - self["feed_price"] = float(price) - - # Buy Side - if float(self.balance(self.market["base"])) < buy_price * target["amount"]["buy"]: - InsufficientFundsError(Amount(target["amount"]["buy"] * float(buy_price), self.market["base"])) - self["insufficient_buy"] = True - else: - self["insufficient_buy"] = False - self.market.buy( - buy_price, - Amount(target["amount"]["buy"], self.market["quote"]), - account=self.account - ) - - # Sell Side - if float(self.balance(self.market["quote"])) < target["amount"]["sell"]: - InsufficientFundsError(Amount(target["amount"]["sell"], self.market["quote"])) - self["insufficient_sell"] = True - else: - self["insufficient_sell"] = False - self.market.sell( - sell_price, - Amount(target["amount"]["sell"], self.market["quote"]), - account=self.account - ) - - self.log.info(self.execute()) - - def getprice(self): - """ Here we obtain the price for the quote and make sure it has - a feed price - """ - 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() - price = ticker.get("quoteSettlement_price") - assert abs(price["price"]) != float("inf"), "Check price feed of asset! (%s)" % str(price) - return price - - def tick(self, d): - """ ticks come in on every block - """ - if self.test_blocks: - if not (self.counter["blocks"] or 0) % self.test_blocks: - self.test() - self.counter["blocks"] += 1 - - def test(self, *args, **kwargs): - """ Tests if the orders need updating - """ - orders = self.orders - - # Test if still 2 orders in the market (the walls) - if 0 < len(orders) < 2: - if ( - not self["insufficient_buy"] and - not self["insufficient_sell"] - ): - self.log.info("No 2 orders available. Updating orders!") - self.updateorders() - elif len(orders) == 0: - self.updateorders() - - # Test if price feed has moved more than the threshold - if ( - self["feed_price"] and - 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() From 88eaf9f551af6fba36ce1f41d9bb080983030fba Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 12 Dec 2018 14:30:39 +0200 Subject: [PATCH 1093/1846] Change dexbot version number to 0.8.3 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index f896f1ed7..b01bb939d 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.8.2' +VERSION = '0.8.3' AUTHOR = 'Codaone Oy' __version__ = VERSION From 047bad6aa377a8b3b7b54f0ec1842cc8bffe1ef0 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Dec 2018 14:25:16 +0200 Subject: [PATCH 1094/1846] Add tests/ccxt_test.py --- tests/ccxt_test.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/ccxt_test.py diff --git a/tests/ccxt_test.py b/tests/ccxt_test.py new file mode 100644 index 000000000..cdde11a57 --- /dev/null +++ b/tests/ccxt_test.py @@ -0,0 +1,58 @@ +from dexbot.strategies.external_feeds.price_feed import PriceFeed + + +""" This is the unit test for testing price_feed module. + Run this test first to cover everything in external feeds + + In DEXBot: + unit of measure = BASE + asset of interest = QUOTE +""" + + +def test_exchanges(): + symbol = 'BTC/USD' + exchanges = ['gecko', 'bitfinex', 'kraken', 'gdax', 'binance', 'waves'] + + for exchange in exchanges: + price_feed = PriceFeed(exchange, symbol) + price_feed.filter_symbols() + center_price = price_feed.get_center_price(None) + print("Center price: {}".format(center_price)) + + if center_price is None: + # Try USDT + center_price = price_feed.get_center_price('USDT') + print("Try again, USD/USDT center price: {}".format(center_price)) + + +def test_consolidated_pair(): + symbol = 'STEEM/BTS' # STEEM/USD * USD/BTS = STEEM/BTS + price_feed = PriceFeed('gecko', symbol) + center_price = price_feed.get_consolidated_price() + print(center_price) + + +def test_alternative_usd(): + # Todo - Refactor price_feed to handle alt USD options. + alternative_usd = ['USDT', 'USDC', 'TUSD', 'GUSD'] + exchanges = ['bittrex', 'poloniex', 'gemini', 'bitfinex', 'kraken', 'binance', 'okex'] + symbol = 'BTC/USD' # Replace with alt usd + + for exchange in exchanges: + for alternative in alternative_usd: + price_feed = PriceFeed(exchange, symbol) + center_price = price_feed.get_center_price(None) + + if center_price: + print('{} using alt: {} {}'.format(symbol, alternative, center_price)) + else: + center_price = price_feed.get_center_price(alternative) + if center_price: + print('{} using alt: {} {}'.format(symbol, alternative, center_price)) + + +if __name__ == '__main__': + test_exchanges() + test_consolidated_pair() + test_alternative_usd() From 5b9599df7b1cf01f07aca12d8996804b1bbc40ca Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Dec 2018 14:25:45 +0200 Subject: [PATCH 1095/1846] Add doc string to get_external_market_center_price --- dexbot/strategies/base.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index f8a159104..dfa9ffd5a 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -606,7 +606,11 @@ def get_lowest_own_sell_order(self, orders=None): return None def get_external_market_center_price(self, external_price_source): - center_price = None + """ Get center price from an external market for current market pair + + :param external_price_source: External market name + :return: Center price as float + """ self.log.debug('inside get_external_mcp, exchange: {} '.format(external_price_source)) market = self.market.get_string('/') self.log.debug('market: {} '.format(market)) From f38f5197d66e4922035873f18aab22164b95df88 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Dec 2018 14:26:32 +0200 Subject: [PATCH 1096/1846] Fix crash on GUI when using external feed --- dexbot/strategies/external_feeds/ccxt_feed.py | 3 +++ dexbot/strategies/external_feeds/gecko_feed.py | 4 ++++ dexbot/strategies/external_feeds/waves_feed.py | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/dexbot/strategies/external_feeds/ccxt_feed.py b/dexbot/strategies/external_feeds/ccxt_feed.py index 9154119a6..b3bcfe2c6 100644 --- a/dexbot/strategies/external_feeds/ccxt_feed.py +++ b/dexbot/strategies/external_feeds/ccxt_feed.py @@ -37,6 +37,9 @@ async def fetch_ticker(exchange, symbol): def get_ccxt_price(symbol, exchange_name): """ Get all tickers from multiple exchanges using async """ center_price = None + + async_loop = asyncio.new_event_loop() + asyncio.set_event_loop(async_loop) exchange = getattr(accxt, exchange_name)({'verbose': False}) ticker = asyncio.get_event_loop().run_until_complete(fetch_ticker(exchange, symbol)) if ticker: diff --git a/dexbot/strategies/external_feeds/gecko_feed.py b/dexbot/strategies/external_feeds/gecko_feed.py index 8e415cdd9..ce4efc701 100644 --- a/dexbot/strategies/external_feeds/gecko_feed.py +++ b/dexbot/strategies/external_feeds/gecko_feed.py @@ -19,7 +19,11 @@ async def get_json(url): def _get_market_price(base, quote): try: + async_loop = asyncio.new_event_loop() + asyncio.set_event_loop(async_loop) + coin_list = asyncio.get_event_loop().run_until_complete(get_json(GECKO_COINS_URL + 'list')) + quote_name = check_gecko_symbol_exists(coin_list, quote.lower()) lookup_pair = "?vs_currency=" + base.lower() + "&ids=" + quote_name market_url = GECKO_COINS_URL + 'markets' + lookup_pair diff --git a/dexbot/strategies/external_feeds/waves_feed.py b/dexbot/strategies/external_feeds/waves_feed.py index 072979e1d..72fb4b2a8 100644 --- a/dexbot/strategies/external_feeds/waves_feed.py +++ b/dexbot/strategies/external_feeds/waves_feed.py @@ -16,6 +16,8 @@ def get_last_price(base, quote): current_price = None try: market_bq = MARKET_URL + quote + '/' + base # external exchange format + async_loop = asyncio.new_event_loop() + asyncio.set_event_loop(async_loop) ticker = asyncio.get_event_loop().run_until_complete(get_json(WAVES_URL + market_bq)) current_price = ticker['24h_close'] except Exception as exeption: @@ -24,6 +26,8 @@ def get_last_price(base, quote): def get_waves_symbols(): + async_loop = asyncio.new_event_loop() + asyncio.set_event_loop(async_loop) symbol_list = asyncio.get_event_loop().run_until_complete(get_json(WAVES_URL + SYMBOLS_URL)) return symbol_list From a2d9e37d6fd32b8d8ea66464764b5004dc0b9a97 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Dec 2018 14:37:25 +0200 Subject: [PATCH 1097/1846] Change debug log to use string format --- dexbot/strategies/external_feeds/price_feed.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/external_feeds/price_feed.py b/dexbot/strategies/external_feeds/price_feed.py index b6241913d..06fa4b49b 100755 --- a/dexbot/strategies/external_feeds/price_feed.py +++ b/dexbot/strategies/external_feeds/price_feed.py @@ -88,13 +88,13 @@ def _get_center_price(self): price = None if self._exchange not in self._alt_exchanges: price = get_ccxt_price(symbol, self._exchange) - debug("use ccxt exchange ", self._exchange, ' symbol ', symbol, ' price:', price) + debug('Use ccxt exchange {} symbol {} price: {}'.format(self.exchange, symbol, price)) elif self._exchange == 'gecko': price = get_gecko_price(symbol_=symbol) - debug("gecko exchange - ", self._exchange, ' symbol ', symbol, ' price:', price) + debug('Use ccxt exchange {} symbol {} price: {}'.format(self.exchange, symbol, price)) elif self._exchange == 'waves': price = get_waves_price(symbol_=symbol) - debug("use waves -", self._exchange, ' symbol ', symbol, ' price:', price) + debug('Use waves exchange {} symbol {} price: {}'.format(self.exchange, symbol, price)) return price def get_center_price(self, type): From e8898b5a99292655c10c23c0cb22461618c8b63e Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Dec 2018 15:11:03 +0200 Subject: [PATCH 1098/1846] Add external price feed logic to Relative Orders --- dexbot/strategies/relative_orders.py | 36 ++++++++++++++++------------ 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 4c6c9f39f..96862d814 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -80,16 +80,23 @@ def __init__(self, *args, **kwargs): self.error_onAccount = self.error # Market status - self.market_center_price = self.get_market_center_price(suppress_errors=True) self.empty_market = False - if not self.market_center_price: - self.empty_market = True + # Get market center price from Bitshares + self.market_center_price = self.get_market_center_price(suppress_errors=True) # Set external price source, defaults to False if not found self.external_feed = self.worker.get('external_feed', False) self.external_price_source = self.worker.get('external_price_source', None) + if self.external_feed: + # Get external center price from given source + self.external_market_center_price = self.get_external_market_center_price(self.external_price_source) + + if not self.market_center_price: + # Bitshares has no center price making it an empty market or one that has only one sided orders + self.empty_market = True + # Worker parameters self.is_center_price_dynamic = self.worker['center_price_dynamic'] @@ -97,16 +104,8 @@ def __init__(self, *args, **kwargs): self.center_price = None self.center_price_depth = self.worker.get('center_price_depth', 0) else: - if self.external_feed: - # Try getting center price from external source - self.center_price = self.get_external_market_center_price(self.external_price_source) - - if self.center_price is None: - # Use manual center price as fallback - self.center_price = self.worker["center_price"] - else: - # Use manually set center price - self.center_price = self.worker["center_price"] + # Use manually set center price + self.center_price = self.worker["center_price"] self.is_relative_order_size = self.worker.get('relative_order_size', False) self.is_asset_offset = self.worker.get('center_price_offset', False) @@ -146,7 +145,7 @@ def __init__(self, *args, **kwargs): return # Check if market has center price when using dynamic center price - if self.empty_market and (self.is_center_price_dynamic or self.dynamic_spread): + if not self.external_feed and self.empty_market and (self.is_center_price_dynamic or self.dynamic_spread): self.log.info('Market is empty and using dynamic market parameters. Waiting for market change...') return @@ -203,7 +202,12 @@ def calculate_order_prices(self): if self.is_center_price_dynamic: # Calculate center price from the market orders - if self.center_price_depth > 0: + + if self.external_feed: + # Try getting center price from external source + center_price = self.get_external_market_center_price(self.external_price_source) + + if self.center_price_depth > 0 and not self.external_feed: # Calculate with quote amount if given center_price = self.get_market_center_price(quote_amount=self.center_price_depth) @@ -404,6 +408,8 @@ def check_orders(self, *args, **kwargs): # Check center price change when using market center price with reset option on change if self.is_reset_on_price_change and self.is_center_price_dynamic: + # This doesn't use external price feed because it is not allowed to be active + # same time as reset_on_price_change spread = self.spread # Calculate spread if dynamic spread option in use, this calculation includes own orders on the market From 5ec3bb2cd75051f52e78655eddc83087630e1ab5 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Dec 2018 15:18:39 +0200 Subject: [PATCH 1099/1846] Update aiohttp to 3.0.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 459be625e..220e05969 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pyqt-distutils==0.7.3 pyinstaller==3.3.1 click-datetime==0.2 cryptography==2.3 -aiohttp==2.3.10 +aiohttp==3.0.1 requests>=2.20.0 yarl==1.1.0 ccxt==1.17.434 From 2a8662576508c2beeda31e42210fd50ba32c00c0 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 17 Dec 2018 08:34:50 +0200 Subject: [PATCH 1100/1846] Change Relative Orders form behavior --- dexbot/controllers/strategy_controller.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 186a1dd0d..f2aef1be9 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -116,9 +116,9 @@ def __init__(self, view, configure, worker_controller, worker_data): widget.custom_expiration_input.clicked.connect(self.onchange_custom_expiration_input) # Trigger the onchange events once - self.onchange_external_feed_input(widget.external_feed_input.isChecked()) self.onchange_relative_order_size_input(widget.relative_order_size_input.isChecked()) self.onchange_center_price_dynamic_input(widget.center_price_dynamic_input.isChecked()) + self.onchange_external_feed_input(widget.external_feed_input.isChecked()) self.onchange_dynamic_spread_input(widget.dynamic_spread_input.isChecked()) self.onchange_reset_on_partial_fill_input(widget.reset_on_partial_fill_input.isChecked()) self.onchange_reset_on_price_change_input(widget.reset_on_price_change_input.isChecked()) @@ -138,7 +138,9 @@ def onchange_external_feed_input(self, checked): self.view.strategy_widget.reset_on_price_change_input.setChecked(False) self.view.strategy_widget.price_change_threshold_input.setDisabled(True) + self.view.strategy_widget.center_price_depth_input.setDisabled(True) else: + self.view.strategy_widget.center_price_depth_input.setEnabled(True) self.view.strategy_widget.external_price_source_input.setDisabled(True) def onchange_manual_offset_input(self): @@ -198,6 +200,7 @@ def onchange_reset_on_price_change_input(self, checked): # Disable external price feed self.view.strategy_widget.external_feed_input.setChecked(False) self.view.strategy_widget.external_price_source_input.setDisabled(True) + self.view.strategy_widget.center_price_depth_input.setEnabled(True) else: self.view.strategy_widget.price_change_threshold_input.setDisabled(True) From db0ccb4670c5bcf32cca64179c28524d6dfa807f Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 17 Dec 2018 14:54:33 +0200 Subject: [PATCH 1101/1846] Add balances table to the database --- dexbot/storage.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/dexbot/storage.py b/dexbot/storage.py index 32305f267..694510af1 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -8,7 +8,7 @@ from . import helper from dexbot import APP_NAME, AUTHOR -from sqlalchemy import create_engine, Column, String, Integer +from sqlalchemy import create_engine, Column, String, Integer, Float from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker @@ -46,6 +46,30 @@ def __init__(self, worker, order_id, order): self.order = order +class Balances(Base): + __tablename__ = 'balances' + + id = Column(Integer, primary_key=True) + account = Column(String) + worker = Column(String) + base_total = Column(Float) + base_symbol = Column(String) + quote_total = Column(Float) + quote_symbol = Column(String) + center_price = Column(Float) + timestamp = Column(Integer) + + def __init__(self, account, worker, base_total, base_symbol, quote_total, quote_symbol, center_price, timestamp): + self.account = account + self.worker = worker + self.base_total = base_total + self.base_symbol = base_symbol + self.quote_total = quote_total + self.quote_symbol = quote_symbol + self.center_price = center_price + self.timestamp = timestamp + + class Storage(dict): """ Storage class From 2ced3a45084399adc12c62c971fcb00e2830df49 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 17 Dec 2018 14:57:26 +0200 Subject: [PATCH 1102/1846] Add saving of balance on every onAccount tick --- dexbot/storage.py | 18 ++++++++++++++++++ dexbot/strategies/base.py | 19 +++++++++++++++++++ dexbot/strategies/relative_orders.py | 3 +++ 3 files changed, 40 insertions(+) diff --git a/dexbot/storage.py b/dexbot/storage.py index 694510af1..c2b1241c5 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -127,6 +127,12 @@ def clear_worker_data(worker): db_worker.clear_orders(worker) db_worker.clear(worker) + @staticmethod + def store_balance_entry(account, worker, base_total, base_symbol, quote_total, quote_symbol, center_price, + timestamp): + db_worker.save_balance(account, worker, base_total, base_symbol, quote_total, quote_symbol, center_price, + timestamp) + class DatabaseWorker(threading.Thread): """ Thread safe database worker @@ -304,6 +310,18 @@ def _fetch_orders(self, worker, token): result[row.order_id] = json.loads(row.order) self._set_result(token, result) + def save_balance(self, account, worker, base_total, base_symbol, + quote_total, quote_symbol, center_price, timestamp): + self.execute_noreturn(self._save_balance, account, worker, base_total, base_symbol, + quote_total, quote_symbol, center_price, timestamp) + + def _save_balance(self, account, worker, base_total, base_symbol, + quote_total, quote_symbol, center_price, timestamp): + balance = Balances(account, worker, base_total, base_symbol, + quote_total, quote_symbol, center_price, timestamp) + self.session.add(balance) + self.session.commit() + # Derive sqlite file directory data_dir = user_data_dir(APP_NAME, AUTHOR) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index ec3083f6f..f8546937a 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1198,6 +1198,25 @@ def retry_action(self, action, *args, **kwargs): else: raise + def store_profit_estimation_data(self): + """ Save total quote, total base, center_price, and datetime in to the database + """ + self.store_balance_entry(self.config.get('account'), + self.worker_name, + self.balance(self.base_asset).get('amount'), + self.market['base'].get('symbol'), + self.balance(self.quote_asset).get('amount'), + self.market['quote'].get('symbol'), + self.get_market_center_price(), + time.time()) + + def get_profit_estimation_data(self, seconds): + """ Get balance history closest to the given time + + :returns The data as dict from the first timestamp going backwards from seconds argument + """ + pass + def write_order_log(self, worker_name, order): """ Write order log to csv file diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index ee3852e45..71a5a2020 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -353,6 +353,9 @@ def check_orders(self, *args, **kwargs): """ delta = datetime.now() - self.last_check + # Store current available balance and balance in orders to the database for profit calculation purpose + self.store_profit_estimation_data() + # Only allow to check orders whether minimal time passed if delta < timedelta(seconds=self.min_check_interval) and not self.initializing: self.log.debug('Ignoring market_update event as min_check_interval is not passed') From 55ac8d9ee7ab447019893cd6af4e129c54aa4cf2 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 18 Dec 2018 14:50:22 +0200 Subject: [PATCH 1103/1846] Add fetching for balance history entries --- dexbot/storage.py | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/dexbot/storage.py b/dexbot/storage.py index c2b1241c5..ed3130967 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -128,10 +128,16 @@ def clear_worker_data(worker): db_worker.clear(worker) @staticmethod - def store_balance_entry(account, worker, base_total, base_symbol, quote_total, quote_symbol, center_price, - timestamp): - db_worker.save_balance(account, worker, base_total, base_symbol, quote_total, quote_symbol, center_price, - timestamp) + def store_balance_entry(account, worker, base_total, base_symbol, quote_total, quote_symbol, + center_price, timestamp): + balance = Balances(account, worker, base_total, base_symbol, + quote_total, quote_symbol, center_price, timestamp) + # Save balance to db + db_worker.save_balance(balance) + + @staticmethod + def get_balance_history(account, worker, timestamp): + return db_worker.get_balance(account, worker, timestamp) class DatabaseWorker(threading.Thread): @@ -310,18 +316,27 @@ def _fetch_orders(self, worker, token): result[row.order_id] = json.loads(row.order) self._set_result(token, result) - def save_balance(self, account, worker, base_total, base_symbol, - quote_total, quote_symbol, center_price, timestamp): - self.execute_noreturn(self._save_balance, account, worker, base_total, base_symbol, - quote_total, quote_symbol, center_price, timestamp) + def save_balance(self, balance): + self.execute_noreturn(self._save_balance, balance) - def _save_balance(self, account, worker, base_total, base_symbol, - quote_total, quote_symbol, center_price, timestamp): - balance = Balances(account, worker, base_total, base_symbol, - quote_total, quote_symbol, center_price, timestamp) + def _save_balance(self, balance): self.session.add(balance) self.session.commit() + def get_balance(self, account, worker, timestamp): + return self.execute(self._get_balance, account, worker, timestamp) + + def _get_balance(self, account, worker, timestamp, token): + """ Get first item that has smaller or same time as given timestamp and matches account and worker name + """ + result = self.session.query(Balances).filter( + Balances.account == account, + Balances.worker == worker, + Balances.timestamp < timestamp + ).first() + + self._set_result(token, result) + # Derive sqlite file directory data_dir = user_data_dir(APP_NAME, AUTHOR) From 97ee39374563f098516e08d1e60a55f7ff268750 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 18 Dec 2018 14:50:50 +0200 Subject: [PATCH 1104/1846] Refactor functions out of strategies to base.py --- dexbot/strategies/base.py | 72 +++++++++++++++++++++++---- dexbot/strategies/relative_orders.py | 33 +----------- dexbot/strategies/staggered_orders.py | 28 ++--------- 3 files changed, 68 insertions(+), 65 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index f8546937a..67dcc4762 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -10,6 +10,7 @@ from dexbot.statemachine import StateMachine from dexbot.helper import truncate from dexbot.strategies.external_feeds.price_feed import PriceFeed +from dexbot.qt_queue.idle_queue import idle_add from events import Events import bitshares.exceptions @@ -1201,21 +1202,24 @@ def retry_action(self, action, *args, **kwargs): def store_profit_estimation_data(self): """ Save total quote, total base, center_price, and datetime in to the database """ - self.store_balance_entry(self.config.get('account'), - self.worker_name, - self.balance(self.base_asset).get('amount'), - self.market['base'].get('symbol'), - self.balance(self.quote_asset).get('amount'), - self.market['quote'].get('symbol'), - self.get_market_center_price(), - time.time()) + account = self.config['workers'][self.worker_name].get('account') + base_amount = self.balance(self.base_asset).get('amount') + base_symbol = self.market['base'].get('symbol') + quote_amount = self.balance(self.quote_asset).get('amount') + quote_symbol = self.market['quote'].get('symbol') + center_price = self.get_market_center_price() + timestamp = time.time() + + self.store_balance_entry(account, self.worker_name, base_amount, base_symbol, + quote_amount, quote_symbol, center_price, timestamp) def get_profit_estimation_data(self, seconds): """ Get balance history closest to the given time :returns The data as dict from the first timestamp going backwards from seconds argument """ - pass + return self.get_balance_history(self.config['workers'][self.worker_name].get('account'), + self.worker_name, seconds) def write_order_log(self, worker_name, order): """ Write order log to csv file @@ -1408,3 +1412,53 @@ def sort_orders_by_price(orders, sort='DESC'): # Sort orders by price return sorted(orders, key=lambda order: order['price'], reverse=reverse) + + # GUI updaters + def update_gui_slider(self): + ticker = self.market.ticker() + latest_price = ticker.get('latest', {}).get('price', None) + if not latest_price: + return + + order_ids = None + orders = self.fetch_orders() + + if orders: + order_ids = orders.keys() + + total_balance = self.count_asset(order_ids) + 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 + + def update_gui_profit(self): + profit = 0 + time_range = 60 * 60 * 24 * 7 # 7 days + current_time = time.time() + timestamp = current_time - time_range + + # Fetch the balance from history + old_data = self.get_balance_history(self.config['workers'][self.worker_name].get('account'), self.worker_name, + timestamp) + if old_data: + earlier_base = old_data.base_total + earlier_quote = old_data.quote_total + earlier_price = old_data.center_price + + balances = self.count_asset(return_asset='base') + base = balances['base'].get('amount') + quote = balances['quote'].get('amount') + + # Calculate profit + base_roi = base / earlier_base + quote_roi = quote / earlier_quote + profit = round(math.sqrt(base_roi * quote_roi), 3) + + # Add to idle que + idle_add(self.view.set_worker_profit, self.worker_name, float(profit)) + self['profit'] = profit diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 71a5a2020..0f7e4b344 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -421,37 +421,6 @@ def check_orders(self, *args, **kwargs): if self.view: self.update_gui_slider() + self.update_gui_profit() self.last_check = datetime.now() - - # 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(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): - ticker = self.market.ticker() - latest_price = ticker.get('latest', {}).get('price', None) - if not latest_price: - return - - order_ids = None - orders = self.fetch_orders() - - if orders: - order_ids = orders.keys() - - total_balance = self.count_asset(order_ids) - 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/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 33dd4ce62..4e427a039 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -168,6 +168,7 @@ def __init__(self, *args, **kwargs): self.current_check_interval = self.min_check_interval if self.view: + self.update_gui_profit() self.update_gui_slider() def maintain_strategy(self, *args, **kwargs): @@ -401,6 +402,9 @@ def maintain_strategy(self, *args, **kwargs): self.last_check = datetime.now() self.log_maintenance_time() + # Update profit estimate + self.update_gui_profit() + def log_maintenance_time(self): """ Measure time from self.start and print a log message """ @@ -1831,30 +1835,6 @@ def tick(self, d): self.maintain_strategy() self.counter += 1 - def update_gui_slider(self): - ticker = self.market.ticker() - latest_price = ticker.get('latest', {}).get('price', None) - - if not latest_price: - return - - orders = self.fetch_orders() - if orders: - order_ids = orders.keys() - else: - order_ids = None - - total_balance = self.count_asset(order_ids) - total = (total_balance['quote'] * latest_price) + total_balance['base'] - - # Prevent division by zero - if not total: - percentage = 50 - else: - percentage = (total_balance['base'] / total) * 100 - - idle_add(self.view.set_worker_slider, self.worker_name, percentage) - class VirtualOrder(dict): """ Wrapper class to handle virtual orders comparison in list index() method From b6dcc6f0c086e7b443f3a7d18cb560b193a05b05 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 18 Dec 2018 14:51:19 +0200 Subject: [PATCH 1105/1846] Modify worker item widget --- dexbot/views/ui/worker_item_widget.ui | 34 ++++++++++++++++++++++++++- dexbot/views/worker_item.py | 8 +++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/dexbot/views/ui/worker_item_widget.ui b/dexbot/views/ui/worker_item_widget.ui index 310076e76..0e2df9ff1 100644 --- a/dexbot/views/ui/worker_item_widget.ui +++ b/dexbot/views/ui/worker_item_widget.ui @@ -198,6 +198,29 @@ border-radius: 20px; + + + + + 30 + 16777215 + + + + + Source Sans Pro + 75 + true + + + + (7d) + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + @@ -218,7 +241,7 @@ border-radius: 20px; - + @@ -230,6 +253,9 @@ border-radius: 20px; + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse @@ -369,6 +395,12 @@ border-radius: 10px; padding-left: 5px; 60 + + + 170 + 16777215 + + Source Sans Pro diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index 217aa0218..e17810154 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -105,7 +105,15 @@ def set_worker_market(self, value): self.quote_asset_label.setText(values[0]) def set_worker_profit(self, value): + green = '#99F75C' + red = '#f75b5b' + value = float(value) + if value < 0: + self.profit_label.setStyleSheet('color: {}'.format(red)) + else: + self.profit_label.setStyleSheet('color: {}'.format(green)) + if value >= 0: value = '+' + str(value) From 090ba699c50927299b794cbe9e3ff656207cc4aa Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 18 Dec 2018 14:57:18 +0200 Subject: [PATCH 1106/1846] Remove unused variable --- dexbot/strategies/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 67dcc4762..d1a98f1cf 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1448,7 +1448,6 @@ def update_gui_profit(self): if old_data: earlier_base = old_data.base_total earlier_quote = old_data.quote_total - earlier_price = old_data.center_price balances = self.count_asset(return_asset='base') base = balances['base'].get('amount') From 00e27706c11d38394a3006115c07f60d741fdb12 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 19 Dec 2018 15:43:12 +0200 Subject: [PATCH 1107/1846] Modify fetching balance history to more accurate --- dexbot/storage.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/dexbot/storage.py b/dexbot/storage.py index ed3130967..39d10868c 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -136,8 +136,8 @@ def store_balance_entry(account, worker, base_total, base_symbol, quote_total, q db_worker.save_balance(balance) @staticmethod - def get_balance_history(account, worker, timestamp): - return db_worker.get_balance(account, worker, timestamp) + def get_balance_history(account, worker, timestamp, base_asset, quote_asset): + return db_worker.get_balance(account, worker, timestamp, base_asset, quote_asset) class DatabaseWorker(threading.Thread): @@ -323,16 +323,18 @@ def _save_balance(self, balance): self.session.add(balance) self.session.commit() - def get_balance(self, account, worker, timestamp): - return self.execute(self._get_balance, account, worker, timestamp) + def get_balance(self, account, worker, timestamp, base_asset, quote_asset): + return self.execute(self._get_balance, account, worker, timestamp, base_asset, quote_asset) - def _get_balance(self, account, worker, timestamp, token): + def _get_balance(self, account, worker, timestamp, base_asset, quote_asset, token): """ Get first item that has smaller or same time as given timestamp and matches account and worker name """ result = self.session.query(Balances).filter( Balances.account == account, Balances.worker == worker, - Balances.timestamp < timestamp + Balances.base_symbol == base_asset, + Balances.quote_symbol == quote_asset, + Balances.timestamp > timestamp ).first() self._set_result(token, result) From 36ff01e47159acad1fb28fc25245d9ff9ce2bf20 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 19 Dec 2018 15:44:54 +0200 Subject: [PATCH 1108/1846] Change profit calculation --- dexbot/strategies/base.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index d1a98f1cf..722dbf7d3 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1444,19 +1444,21 @@ def update_gui_profit(self): # Fetch the balance from history old_data = self.get_balance_history(self.config['workers'][self.worker_name].get('account'), self.worker_name, - timestamp) + timestamp, self.base_asset, self.quote_asset) if old_data: earlier_base = old_data.base_total earlier_quote = old_data.quote_total - balances = self.count_asset(return_asset='base') - base = balances['base'].get('amount') - quote = balances['quote'].get('amount') + # Check that the earlier base and quote are not 0, to avoid ZeroDivision error + if earlier_base != 0 or earlier_quote != 0: + base_balance = self.count_asset(return_asset='base') + quote_balance = self.count_asset(return_asset='quote') + base = truncate(base_balance['base'].get('amount'), self.market['base']['precision']) + quote = truncate(quote_balance['quote'].get('amount'), self.market['quote']['precision']) - # Calculate profit - base_roi = base / earlier_base - quote_roi = quote / earlier_quote - profit = round(math.sqrt(base_roi * quote_roi), 3) + base_roi = base / earlier_base + quote_roi = quote / earlier_quote + profit = round((100 / math.sqrt(base_roi * quote_roi)) - 100) # Add to idle que idle_add(self.view.set_worker_profit, self.worker_name, float(profit)) From 3dc7192bd86ac857cb1b451ff51bee41e5b33631 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 20 Dec 2018 01:38:04 +0500 Subject: [PATCH 1109/1846] Fix comment --- dexbot/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/storage.py b/dexbot/storage.py index 39d10868c..d68ba3ee1 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -327,7 +327,7 @@ def get_balance(self, account, worker, timestamp, base_asset, quote_asset): return self.execute(self._get_balance, account, worker, timestamp, base_asset, quote_asset) def _get_balance(self, account, worker, timestamp, base_asset, quote_asset, token): - """ Get first item that has smaller or same time as given timestamp and matches account and worker name + """ Get first item that has bigger time as given timestamp and matches account and worker name """ result = self.session.query(Balances).filter( Balances.account == account, From 4797522898066f967df1acca14d05f8c3f9c7cde Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 20 Dec 2018 01:39:10 +0500 Subject: [PATCH 1110/1846] Change profit estimation formula Use correct profit formula by litepresence --- dexbot/strategies/base.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 722dbf7d3..40508e56f 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1448,17 +1448,25 @@ def update_gui_profit(self): if old_data: earlier_base = old_data.base_total earlier_quote = old_data.quote_total + old_center_price = old_data.center_price - # Check that the earlier base and quote are not 0, to avoid ZeroDivision error - if earlier_base != 0 or earlier_quote != 0: - base_balance = self.count_asset(return_asset='base') - quote_balance = self.count_asset(return_asset='quote') - base = truncate(base_balance['base'].get('amount'), self.market['base']['precision']) - quote = truncate(quote_balance['quote'].get('amount'), self.market['quote']['precision']) + # Calculate max theoretical balances based on starting price + old_maxquantity_base = earlier_base + earlier_quote * old_center_price + old_maxquantity_quote = earlier_quote + earlier_base / old_center_price - base_roi = base / earlier_base - quote_roi = quote / earlier_quote - profit = round((100 / math.sqrt(base_roi * quote_roi)) - 100) + # Current balances + balance = self.count_asset() + base_balance = balance['base'] + quote_balance = balance['quote'] + + # Calculate max theoretical current balances + center_price = self.get_market_center_price() + maxquantity_base = base_balance + quote_balance * center_price + maxquantity_quote = quote_balance + base_balance / center_price + + base_roi = maxquantity_base / old_maxquantity_base + quote_roi = maxquantity_quote / old_maxquantity_quote + profit = round(math.sqrt(base_roi * quote_roi) - 1, 4) # Add to idle que idle_add(self.view.set_worker_profit, self.worker_name, float(profit)) From 151b283ed792c5070289f020109ce64a818b918e Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 20 Dec 2018 10:27:31 +0200 Subject: [PATCH 1111/1846] Fix ROI calculation --- dexbot/strategies/base.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 722dbf7d3..588ef7e0f 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1202,10 +1202,11 @@ def retry_action(self, action, *args, **kwargs): def store_profit_estimation_data(self): """ Save total quote, total base, center_price, and datetime in to the database """ + assets = self.count_asset() account = self.config['workers'][self.worker_name].get('account') - base_amount = self.balance(self.base_asset).get('amount') + base_amount = assets['base'] base_symbol = self.market['base'].get('symbol') - quote_amount = self.balance(self.quote_asset).get('amount') + quote_amount = assets['quote'] quote_symbol = self.market['quote'].get('symbol') center_price = self.get_market_center_price() timestamp = time.time() @@ -1453,12 +1454,12 @@ def update_gui_profit(self): if earlier_base != 0 or earlier_quote != 0: base_balance = self.count_asset(return_asset='base') quote_balance = self.count_asset(return_asset='quote') - base = truncate(base_balance['base'].get('amount'), self.market['base']['precision']) - quote = truncate(quote_balance['quote'].get('amount'), self.market['quote']['precision']) + base = base_balance['base'].get('amount') + quote = quote_balance['quote'].get('amount') base_roi = base / earlier_base quote_roi = quote / earlier_quote - profit = round((100 / math.sqrt(base_roi * quote_roi)) - 100) + profit = round((math.sqrt(base_roi * quote_roi) - 1) * 100, 3) # Add to idle que idle_add(self.view.set_worker_profit, self.worker_name, float(profit)) From 52184ec26697320b085592a3021593fd62726ff5 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 20 Dec 2018 13:21:39 +0200 Subject: [PATCH 1112/1846] Change couple variable names --- dexbot/strategies/base.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index ac1449dc8..337006f0e 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1452,8 +1452,8 @@ def update_gui_profit(self): old_center_price = old_data.center_price # Calculate max theoretical balances based on starting price - old_maxquantity_base = earlier_base + earlier_quote * old_center_price - old_maxquantity_quote = earlier_quote + earlier_base / old_center_price + old_max_quantity_base = earlier_base + earlier_quote * old_center_price + old_max_quantity_quote = earlier_quote + earlier_base / old_center_price # Current balances balance = self.count_asset() @@ -1462,11 +1462,11 @@ def update_gui_profit(self): # Calculate max theoretical current balances center_price = self.get_market_center_price() - maxquantity_base = base_balance + quote_balance * center_price - maxquantity_quote = quote_balance + base_balance / center_price + max_quantity_base = base_balance + quote_balance * center_price + max_quantity_quote = quote_balance + base_balance / center_price - base_roi = maxquantity_base / old_maxquantity_base - quote_roi = maxquantity_quote / old_maxquantity_quote + base_roi = max_quantity_base / old_max_quantity_base + quote_roi = max_quantity_quote / old_max_quantity_quote profit = round(math.sqrt(base_roi * quote_roi) - 1, 4) # Add to idle que From 338c51ff1ae536158250a74653f99c7c1a777c98 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 20 Dec 2018 14:22:47 +0200 Subject: [PATCH 1113/1846] Change dexbot version number to 0.8.4 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index b01bb939d..a97ac6650 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.8.3' +VERSION = '0.8.4' AUTHOR = 'Codaone Oy' __version__ = VERSION From 8ab7a78e47aad60260f917c3f69cd395638c60e5 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 20 Dec 2018 17:47:59 +0500 Subject: [PATCH 1114/1846] Remove unused import --- dexbot/strategies/staggered_orders.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 4e427a039..5bc77da9b 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -8,7 +8,6 @@ from bitshares.amount import Amount from dexbot.strategies.base import StrategyBase, ConfigElement, DetailElement -from dexbot.qt_queue.idle_queue import idle_add class Strategy(StrategyBase): From 4d0530b1736baa8142b41bb28c3e7b3bf6fac305 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 20 Dec 2018 18:25:46 +0500 Subject: [PATCH 1115/1846] Add comment --- dexbot/strategies/staggered_orders.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 5bc77da9b..af911ed7d 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -205,6 +205,7 @@ def maintain_strategy(self, *args, **kwargs): # Calculate balances, and use orders from previous call of self.refresh_orders() to reduce API calls self.refresh_balances(use_cached_orders=True) + # Calculate minimal orders amounts based on asset precision if not (self.order_min_base or self.order_min_quote): self.calculate_min_amounts() From 8d8c25c3ea2726940289cc84bc4fcefb9ca9418e Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 20 Dec 2018 19:36:18 +0500 Subject: [PATCH 1116/1846] Try to update gui profit only in GUI mode Traceback (most recent call last): File "/home/vvk/devel/DEXBot/dexbot/worker.py", line 122, in on_block self.workers[worker_name].ontick(data) File "/home/vvk/.local/share/virtualenvs/DEXBot-XTl2tJdV/lib/python3.6/site-packages/Events-0.3-py3.6.egg/events/events.py", line 95, in __call__ f(*a, **kw) File "/home/vvk/devel/DEXBot/dexbot/strategies/staggered_orders.py", line 1870, in tick self.maintain_strategy() File "/home/vvk/devel/DEXBot/dexbot/strategies/staggered_orders.py", line 411, in maintain_strategy self.update_gui_profit() File "/home/vvk/devel/DEXBot/dexbot/strategies/base.py", line 1473, in update_gui_profit idle_add(self.view.set_worker_profit, self.worker_name, float(profit)) AttributeError: 'NoneType' object has no attribute 'set_worker_profit' --- 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 af911ed7d..ff07fe4e7 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -403,7 +403,8 @@ def maintain_strategy(self, *args, **kwargs): self.log_maintenance_time() # Update profit estimate - self.update_gui_profit() + if self.view: + self.update_gui_profit() def log_maintenance_time(self): """ Measure time from self.start and print a log message From e812aebd2b4b87d00189b45c8216c0aed9702f35 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 21 Dec 2018 01:10:54 +0500 Subject: [PATCH 1117/1846] Fix log message --- 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 ff07fe4e7..39b54d5da 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1447,7 +1447,7 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals # Check whether new order will exceed available balance if balance < limiter: if place_order and not allow_partial: - self.log.debug('Not enough balance to place closer {} order; need/avail: {:.8f}/{:.8f}' + self.log.debug('Not enough balance to place further {} order; need/avail: {:.8f}/{:.8f}' .format(order_type, limiter, balance)) place_order = False elif allow_partial and balance > hard_limit: From 060f43f72e728b9355cc13273e970d45d8b55527 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 21 Dec 2018 01:26:59 +0500 Subject: [PATCH 1118/1846] More nice precision in log messages --- dexbot/strategies/staggered_orders.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 39b54d5da..6c879e5ea 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1258,16 +1258,19 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False order_type = '' quote_amount = 0 symbol = '' + precision = 0 # Define asset-dependent variables if asset == 'base': order_type = 'buy' balance = self.base_balance['amount'] symbol = self.base_balance['symbol'] + precision = self.market['base']['precision'] elif asset == 'quote': order_type = 'sell' balance = self.quote_balance['amount'] symbol = self.quote_balance['symbol'] + precision = self.market['quote']['precision'] # Check for instant fill if asset == 'base': @@ -1339,12 +1342,12 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False # Check whether new order will exceed available balance if balance < limiter: if place_order and not allow_partial: - self.log.debug('Not enough balance to place closer {} order; need/avail: {:.8f}/{:.8f}' - .format(order_type, limiter, balance)) + self.log.debug('Not enough balance to place closer {} order; need/avail: {:.{prec}f}/{:.{prec}f}' + .format(order_type, limiter, balance, prec=precision)) place_order = False elif allow_partial and balance > hard_limit: - self.log.debug('Limiting {} order amount to available asset balance: {} {}' - .format(order_type, balance, symbol)) + self.log.debug('Limiting {} order amount to available asset balance: {:.{prec}f} {}' + .format(order_type, balance, symbol, prec=precision)) if asset == 'base': quote_amount = balance / price elif asset == 'quote': @@ -1389,6 +1392,7 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals balance = 0 order_type = '' symbol = '' + precision = 0 virtual_bound = self.market_center_price / math.sqrt(1 + self.target_spread) # Define asset-dependent variables @@ -1396,10 +1400,12 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals order_type = 'buy' balance = self.base_balance['amount'] symbol = self.base_balance['symbol'] + precision = self.market['base']['precision'] elif asset == 'quote': order_type = 'sell' balance = self.quote_balance['amount'] symbol = self.quote_balance['symbol'] + precision = self.market['quote']['precision'] price = order['price'] / (1 + self.increment) @@ -1447,12 +1453,12 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals # Check whether new order will exceed available balance if balance < limiter: if place_order and not allow_partial: - self.log.debug('Not enough balance to place further {} order; need/avail: {:.8f}/{:.8f}' - .format(order_type, limiter, balance)) + self.log.debug('Not enough balance to place further {} order; need/avail: {:.{prec}f}/{:.{prec}f}' + .format(order_type, limiter, balance, prec=precision)) place_order = False elif allow_partial and balance > hard_limit: - self.log.debug('Limiting {} order amount to available asset balance: {} {}' - .format(order_type, balance, symbol)) + self.log.debug('Limiting {} order amount to available asset balance: {:.{prec}f} {}' + .format(order_type, balance, symbol, prec=precision)) if asset == 'base': quote_amount = balance / price elif asset == 'quote': From 9cf59e2d4eb7f4c646f5f2f0625bc98748fa81b5 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 21 Dec 2018 01:30:19 +0500 Subject: [PATCH 1119/1846] Update grammar in log messages (tnx ElStone) --- dexbot/strategies/base.py | 4 ++-- dexbot/strategies/staggered_orders.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 337006f0e..33f91760d 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1083,7 +1083,7 @@ def place_market_buy_order(self, amount, price, return_none=False, *args, **kwar self.disabled = True return None - self.log.info('Placing a buy order for {:.{prec}f} {} @ {:.8f}' + self.log.info('Placing a buy order with {:.{prec}f} {} @ {:.8f}' .format(base_amount, symbol, price, prec=precision)) # Place the order @@ -1138,7 +1138,7 @@ def place_market_sell_order(self, amount, price, return_none=False, *args, **kwa self.disabled = True return None - self.log.info('Placing a sell order for {:.{prec}f} {} @ {:.8f}' + self.log.info('Placing a sell order with {:.{prec}f} {} @ {:.8f}' .format(quote_amount, symbol, price, prec=precision)) # Place the order diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 6c879e5ea..20e744051 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1767,7 +1767,7 @@ def place_virtual_buy_order(self, amount, price): order['base'] = base_asset order['for_sale'] = base_asset - self.log.info('Placing a virtual buy order for {:.{prec}f} {} @ {:.8f}' + self.log.info('Placing a virtual buy order with {:.{prec}f} {} @ {:.8f}' .format(order['base']['amount'], symbol, price, prec=precision)) self.virtual_orders.append(order) @@ -1796,7 +1796,7 @@ def place_virtual_sell_order(self, amount, price): order['base'] = base_asset order['for_sale'] = base_asset - self.log.info('Placing a virtual sell order for {:.{prec}f} {} @ {:.8f}' + self.log.info('Placing a virtual sell order with {:.{prec}f} {} @ {:.8f}' .format(amount, symbol, price, prec=precision)) self.virtual_orders.append(order) From a45122ffa083139ba2b4a84a5f725d326c6fff79 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 21 Dec 2018 01:33:50 +0500 Subject: [PATCH 1120/1846] Add get_recent_balance_entry() method Method to fetch most recent balance history from the db. --- dexbot/storage.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/dexbot/storage.py b/dexbot/storage.py index d68ba3ee1..96203c9a8 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -139,6 +139,10 @@ def store_balance_entry(account, worker, base_total, base_symbol, quote_total, q def get_balance_history(account, worker, timestamp, base_asset, quote_asset): return db_worker.get_balance(account, worker, timestamp, base_asset, quote_asset) + @staticmethod + def get_recent_balance_entry(account, worker, base_asset, quote_asset): + return db_worker.get_recent_balance_entry(account, worker, base_asset, quote_asset) + class DatabaseWorker(threading.Thread): """ Thread safe database worker @@ -339,6 +343,20 @@ def _get_balance(self, account, worker, timestamp, base_asset, quote_asset, toke self._set_result(token, result) + def get_recent_balance_entry(self, account, worker, base_asset, quote_asset): + return self.execute(self._get_recent_balance_entry, account, worker, base_asset, quote_asset) + + def _get_recent_balance_entry(self, account, worker, base_asset, quote_asset, token): + """ Get most recent balance history item that matches account and worker name + """ + result = self.session.query(Balances).filter( + Balances.account == account, + Balances.worker == worker, + Balances.base_symbol == base_asset, + Balances.quote_symbol == quote_asset, + ).order_by(Balances.id.desc()).first() + + self._set_result(token, result) # Derive sqlite file directory data_dir = user_data_dir(APP_NAME, AUTHOR) From fac81e772a4c04859c585ec2b3cbfbfd7babb87a Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 21 Dec 2018 01:42:46 +0500 Subject: [PATCH 1121/1846] Store profit estimation data in Staggered Orders --- dexbot/strategies/staggered_orders.py | 44 +++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 20e744051..d9f5dc9ec 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1,4 +1,5 @@ import sys +import time import math import traceback import bitsharesapi.exceptions @@ -125,6 +126,7 @@ def __init__(self, *args, **kwargs): # Assume we are in bootstrap mode by default. This prevents weird things when bootstrap was interrupted self.bootstrapping = True self.market_center_price = None + self.old_center_price = None self.buy_orders = [] self.sell_orders = [] self.real_buy_orders = [] @@ -205,6 +207,9 @@ def maintain_strategy(self, *args, **kwargs): # Calculate balances, and use orders from previous call of self.refresh_orders() to reduce API calls self.refresh_balances(use_cached_orders=True) + # Store balance entry for profit estimation if needed + self.store_profit_estimation_data() + # Calculate minimal orders amounts based on asset precision if not (self.order_min_base or self.order_min_quote): self.calculate_min_amounts() @@ -618,6 +623,40 @@ def replace_virtual_order_with_real(self, order): return True return False + def store_profit_estimation_data(self, force=False): + """ Stores balance history entry if center price moved enough + + :param bool | force: True = force store data, False = store data only on center price change + """ + need_store = False + account = self.config['workers'][self.worker_name].get('account') + + if force: + need_store = True + + # If old center price is not set, try fetch from the db + if not self.old_center_price and not force: + old_data = self.get_recent_balance_entry(account, self.worker_name, self.base_asset, self.quote_asset) + if old_data: + self.old_center_price = old_data.center_price + else: + need_store = True + + if self.old_center_price and self.market_center_price and not force: + # Check if center price changed more than increment + diff = abs(self.old_center_price - self.market_center_price) / self.old_center_price + if diff > self.increment: + self.log.debug('Center price change is {:.2%}, need to store balance data'.format(diff)) + need_store = True + + if need_store and self.market_center_price: + timestamp = time.time() + self.log.debug('Storing balance data at center price {:.8f}'.format(self.market_center_price)) + self.store_balance_entry(account, self.worker_name, self.base_total_balance, self.base_asset, + self.quote_total_balance, self.quote_asset, self.market_center_price, timestamp) + # Cache center price for later comparisons + self.old_center_price = self.market_center_price + def allocate_asset(self, asset, asset_balance): """ Allocates available asset balance as buy or sell orders. @@ -741,6 +780,11 @@ def allocate_asset(self, asset, asset_balance): else: # Opposite side probably reached range bound, allow to place partial order self.place_closer_order(asset, closest_own_order, allow_partial=True) + + # Store balance data whether new actual spread will match target spread + if self.actual_spread + self.increment >= self.target_spread and not self.bitshares.txbuffer.is_empty(): + # Tranasctions are not yet sent, so balance refresh is not needed + self.store_profit_estimation_data(force=True) elif not opposite_orders: # Do not try to do anything than placing closer order whether there is no opposite orders return From 722db4c85faf7f4ddcbe908829284ccd7f3498da Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 21 Dec 2018 08:48:42 +0200 Subject: [PATCH 1122/1846] Fix small typo --- 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 d9f5dc9ec..22beff570 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -783,7 +783,7 @@ def allocate_asset(self, asset, asset_balance): # Store balance data whether new actual spread will match target spread if self.actual_spread + self.increment >= self.target_spread and not self.bitshares.txbuffer.is_empty(): - # Tranasctions are not yet sent, so balance refresh is not needed + # Transactions are not yet sent, so balance refresh is not needed self.store_profit_estimation_data(force=True) elif not opposite_orders: # Do not try to do anything than placing closer order whether there is no opposite orders From dd190ae05b33259106dab3508d4cc32eea10a71e Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 21 Dec 2018 08:50:08 +0200 Subject: [PATCH 1123/1846] Change dexbot version number to 0.8.5 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index a97ac6650..625751044 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.8.4' +VERSION = '0.8.5' AUTHOR = 'Codaone Oy' __version__ = VERSION From 073c097aeb45e2c06c94cd3923a910c395556214 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 21 Dec 2018 13:13:01 +0200 Subject: [PATCH 1124/1846] Update pyqt5 to 5.11.3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 220e05969..6be2bc5bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pyqt5==5.10 +pyqt5==5.11.3 pyqt-distutils==0.7.3 pyinstaller==3.3.1 click-datetime==0.2 From 81b6726e7c73aa425038165ff9e341ba5ce96e98 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 21 Dec 2018 13:13:16 +0200 Subject: [PATCH 1125/1846] Update pyinstaller to 3.4 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6be2bc5bd..f7811eb18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ pyqt5==5.11.3 pyqt-distutils==0.7.3 -pyinstaller==3.3.1 +pyinstaller==3.4 click-datetime==0.2 cryptography==2.3 aiohttp==3.0.1 From 1526dddbf84bdb7017cce60d5321948a2c35910f Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 21 Dec 2018 13:13:28 +0200 Subject: [PATCH 1126/1846] Update websocket-client to 0.54.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f8bff3898..b5bb25fd2 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ 'sdnotify', 'appdirs>=1.4.3', 'pycryptodomex==3.6.4', - 'websocket-client==0.53.0' + 'websocket-client==0.54.0' ] From fbc32519249b20aeb0bf9256b2554bb42b2a2650 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 21 Dec 2018 23:38:53 +0500 Subject: [PATCH 1127/1846] More nice precision in log messages --- dexbot/strategies/staggered_orders.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 22beff570..41783510f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -674,6 +674,7 @@ def allocate_asset(self, asset, asset_balance): own_threshold = 0 own_symbol = '' own_precision = 0 + opposite_precision = 0 opposite_symbol = '' increase_finished = False @@ -685,6 +686,7 @@ def allocate_asset(self, asset, asset_balance): opposite_orders = self.sell_orders own_threshold = self.base_asset_threshold own_precision = self.market['base']['precision'] + opposite_precision = self.market['quote']['precision'] elif asset == 'quote': order_type = 'sell' own_symbol = self.quote_balance['symbol'] @@ -693,6 +695,7 @@ def allocate_asset(self, asset, asset_balance): opposite_orders = self.buy_orders own_threshold = self.quote_asset_threshold own_precision = self.market['quote']['precision'] + opposite_precision = self.market['quote']['precision'] if own_orders: # Get currently the furthest and closest orders @@ -754,27 +757,27 @@ def allocate_asset(self, asset, asset_balance): if self.mode == 'mountain': opposite_asset_limit = closest_opposite_order['base']['amount'] * (1 + self.increment) own_asset_limit = None - self.log.debug('Limiting {} order by opposite order: {} {}'.format( - order_type, opposite_asset_limit, opposite_symbol)) + self.log.debug('Limiting {} order by opposite order: {:.{prec}f} {}'.format( + order_type, opposite_asset_limit, opposite_symbol, prec=opposite_precision)) elif ((self.mode == 'buy_slope' and asset == 'base') or (self.mode == 'sell_slope' and asset == 'quote')): opposite_asset_limit = None own_asset_limit = closest_opposite_order['quote']['amount'] - self.log.debug('Limiting {} order by opposite order: {} {}' - .format(order_type, own_asset_limit, own_symbol)) + self.log.debug('Limiting {} order by opposite order: {:.{prec}f} {}' + .format(order_type, own_asset_limit, own_symbol, prec=own_precision)) elif self.mode == 'neutral': opposite_asset_limit = closest_opposite_order['base']['amount'] * \ math.sqrt(1 + self.increment) own_asset_limit = None - self.log.debug('Limiting {} order by opposite order: {} {}'.format( - order_type, opposite_asset_limit, opposite_symbol)) + self.log.debug('Limiting {} order by opposite order: {:.{prec}f} {}'.format( + order_type, opposite_asset_limit, opposite_symbol, prec=opposite_precision)) elif (self.mode == 'valley' or (self.mode == 'buy_slope' and asset == 'quote') or (self.mode == 'sell_slope' and asset == 'base')): opposite_asset_limit = closest_opposite_order['base']['amount'] own_asset_limit = None - self.log.debug('Limiting {} order by opposite order: {} {}'.format( - order_type, opposite_asset_limit, opposite_symbol)) + self.log.debug('Limiting {} order by opposite order: {:.{prec}f} {}'.format( + order_type, opposite_asset_limit, opposite_symbol, prec=opposite_precision)) self.place_closer_order(asset, closest_own_order, own_asset_limit=own_asset_limit, opposite_asset_limit=opposite_asset_limit, allow_partial=False) else: From 34f66f92556624efa98db50322364de2d7cc5778 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 21 Dec 2018 23:40:48 +0500 Subject: [PATCH 1128/1846] Improve orders increase in mountain mode --- dexbot/strategies/staggered_orders.py | 35 +++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 41783510f..896f919c6 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -143,6 +143,7 @@ def __init__(self, *args, **kwargs): self.quote_asset_threshold = 0 self.base_asset_threshold = 0 self.min_increase_factor = 1.15 + self.mountain_max_increase_mode = False # Initial balance history elements should not be equal to avoid immediate bootstrap turn off self.quote_balance_history = [1, 2, 3] self.base_balance_history = [1, 2, 3] @@ -997,6 +998,7 @@ def increase_single_order(asset, order, new_order_amount): for order in orders: order_index = orders.index(order) order_amount = order['base']['amount'] + is_closest_order = False # This check prevents choosing order with index lower than the list length if order_index == 0: @@ -1004,6 +1006,7 @@ def increase_single_order(asset, order, new_order_amount): # This allows our closest order amount exceed highest opposite-side order amount closer_order = order closer_bound = closer_order['base']['amount'] * (1 + self.increment) + is_closest_order = True else: closer_order = orders[order_index - 1] closer_bound = closer_order['base']['amount'] @@ -1023,8 +1026,29 @@ def increase_single_order(asset, order, new_order_amount): if (further_bound > order_amount * (1 + self.increment / 10) < closer_bound and further_bound - order_amount >= order_amount * self.increment / 2): # Calculate new order size and place the order to the market + """ To prevent moving liquidity away from center, let new order be no more than `order_amount * + increase_factor`. This is for situations when we increasing order on side which was previously + bigger. Example: buy side, amounts in QUOTE: + [1000 1000 1000 100 100 100

] + + Without increase_factor: + [1000 1000 1000 1000 100 100
] + + With increase_factor: + [1000 1000 1000 200 100 100
] + [1000 1000 1000 200 200 100
] + [1000 1000 1000 200 200 200
] + + At the same time, we want MAX orders size increase for ALL orders in case of external transfer + of new funds. To achieve this we are setting self.mountain_max_increase_mode flag when + examining furthest order. + """ new_order_amount = further_bound + if not self.mountain_max_increase_mode: + increase_factor = max(1 + self.increment, self.min_increase_factor) + new_order_amount = min(further_bound, order_amount * increase_factor) + if is_least_order: new_orders_sum = 0 amount = order_amount @@ -1032,8 +1056,7 @@ def increase_single_order(asset, order, new_order_amount): amount = amount * (1 + self.increment) new_orders_sum += amount # To reduce allocation rounds, increase furthest order more - new_order_amount = order_amount * (total_balance / new_orders_sum) \ - * (1 + self.increment * 0.75) + new_order_amount = order_amount * (total_balance / new_orders_sum) * (1 + self.increment) if new_order_amount < closer_bound: """ This is for situations when calculated new_order_amount is not big enough to @@ -1043,6 +1066,14 @@ def increase_single_order(asset, order, new_order_amount): increased. """ new_order_amount = closer_bound / (1 + self.increment * 0.2) + else: + # Set bypass flag to not limit next orders + self.mountain_max_increase_mode = True + self.log.debug('Activating max increase mode for mountain mode') + elif is_closest_order and self.mountain_max_increase_mode: + # Turn off bypass flag when reaching closest order + self.log.debug('Deactivating max increase mode for mountain mode') + self.mountain_max_increase_mode = False return increase_single_order(asset, order, new_order_amount) From 706512b7e85ea69012b39acb65e15d3b64a38086 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 22 Dec 2018 00:20:13 +0500 Subject: [PATCH 1129/1846] Increase upper limit for operational depth (GUI) This would allow to set Operational Depth to >99 in the GUI. --- 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 896f919c6..8945e07b9 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -74,7 +74,7 @@ def configure(cls, return_base_config=True): 'Allow to execute orders by market', None), ConfigElement( 'operational_depth', 'int', 10, 'Operational depth', - 'Order depth to maintain on books', (2, None, None)) + 'Order depth to maintain on books', (2, 9999999, None)) ] @classmethod From 65452b7cb50d42ef37e26f4ef98e77f1a7a4a283 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 24 Dec 2018 20:50:32 +0500 Subject: [PATCH 1130/1846] Fix dexbot-cli configure with empty config When there is no configfile, just auto-generate it. Closes: #274 --- dexbot/ui.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/dexbot/ui.py b/dexbot/ui.py index 2ce5ff70b..9c0018500 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -9,6 +9,8 @@ from bitshares import BitShares from bitshares.instance import set_shared_bitshares_instance +from dexbot.config import Config + log = logging.getLogger(__name__) @@ -120,12 +122,9 @@ def new_func(ctx, *args, **kwargs): def configfile(f): @click.pass_context def new_func(ctx, *args, **kwargs): - try: - ctx.config = yaml.safe_load(open(ctx.obj["configfile"])) - except FileNotFoundError: - alert("Looking for the config file in %s\nNot found!\n" - "Try running 'dexbot configure' to generate\n" % ctx.obj['configfile']) - sys.exit(78) # 'configuration error' in sysexits.h + if not os.path.isfile(ctx.obj["configfile"]): + Config(path=ctx.obj['configfile']) + ctx.config = yaml.safe_load(open(ctx.obj["configfile"])) return ctx.invoke(f, *args, **kwargs) return update_wrapper(new_func, f) From 659aaa16a5e0f94e94dc0cef14b3589747738b43 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 24 Dec 2018 21:26:35 +0500 Subject: [PATCH 1131/1846] Emit a log message when fee asset balance is too small Closes: #408 --- dexbot/strategies/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 33f91760d..540c357ce 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1196,6 +1196,9 @@ def retry_action(self, action, *args, **kwargs): self.log.warning("retrying on '{}'".format(str(exception))) self.bitshares.txbuffer.clear() time.sleep(6) # Wait at least a BitShares block + elif "Assert Exception: delta.amount > 0: Insufficient Balance" in str(exception): + self.log.critical('Insufficient balance of fee asset') + raise else: raise From eb1a4a97152115c385b9f79fee94cc9a41e33093 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 27 Dec 2018 22:38:18 +0500 Subject: [PATCH 1132/1846] Update debug message --- 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 8945e07b9..6bec2a04d 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1519,7 +1519,8 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals if place_order: corrected_quote_amount = self.check_min_order_size(quote_amount, price) if corrected_quote_amount > quote_amount: - self.log.debug('Correcting further order amount to minimal allowed') + self.log.debug('Correcting further order amount to minimal allowed: {} -> {}' + .format(quote_amount, corrected_quote_amount)) quote_amount = corrected_quote_amount base_amount = quote_amount * price if asset == 'base': From 822f15d28f6b0a8a968cad6b31ea64bd8095ddf9 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 27 Dec 2018 22:38:37 +0500 Subject: [PATCH 1133/1846] Fix further order for neutral mode --- dexbot/strategies/staggered_orders.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 6bec2a04d..cabacfaf9 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1501,6 +1501,7 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals own_asset_amount = order['base']['amount'] opposite_asset_amount = own_asset_amount / price elif self.mode == 'neutral': + own_asset_amount = order['base']['amount'] / math.sqrt(1 + self.increment) opposite_asset_amount = own_asset_amount / price limiter = 0 From 4446c12c844a046b0e109af6048588453f5b221b Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 27 Dec 2018 22:45:36 +0500 Subject: [PATCH 1134/1846] Change condition in place_xxx_order() Adjust condition to not emit an unneeded debug message. --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index cabacfaf9..78a9bfe30 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1430,7 +1430,7 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False quote_amount = balance / price elif asset == 'quote': quote_amount = balance - else: + elif place_order: self.log.debug('Not enough balance to place minimal allowed order') place_order = False @@ -1543,7 +1543,7 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals quote_amount = balance / price elif asset == 'quote': quote_amount = balance - else: + elif place_order: self.log.debug('Not enough balance to place minimal allowed order') place_order = False From 8cc9a3adef4858bd4f8f6e0583e9909d4746b0b8 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 27 Dec 2018 22:46:47 +0500 Subject: [PATCH 1135/1846] Adjust replacing of real orders with virtual Replacements were work only for valley mode, now all modes are supported. --- dexbot/strategies/staggered_orders.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 78a9bfe30..abc34c6ed 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -246,15 +246,19 @@ def maintain_strategy(self, *args, **kwargs): return # Replace excessive real orders with virtual ones, buy side - if (self.real_buy_orders and len(self.real_buy_orders) > self.operational_depth + 5 and - self.real_buy_orders[-1]['base']['amount'] == self.real_buy_orders[-2]['base']['amount']): + if self.real_buy_orders and len(self.real_buy_orders) > self.operational_depth + 5: # Note: replace should happen only if next order is same-sized. Otherwise it will break proper allocation - self.replace_real_order_with_virtual(self.real_buy_orders[-1]) + test_order = self.place_further_order('base', self.real_buy_orders[-2], place_order=False) + diff = abs(test_order['amount'] - self.real_buy_orders[-1]['quote']['amount']) + if diff <= self.order_min_quote: + self.replace_real_order_with_virtual(self.real_buy_orders[-1]) # Replace excessive real orders with virtual ones, sell side - if (self.real_sell_orders and len(self.real_sell_orders) > self.operational_depth + 5 and - self.real_sell_orders[-1]['base']['amount'] == self.real_sell_orders[-2]['base']['amount']): - self.replace_real_order_with_virtual(self.real_sell_orders[-1]) + if self.real_sell_orders and len(self.real_sell_orders) > self.operational_depth + 5: + test_order = self.place_further_order('quote', self.real_sell_orders[-2], place_order=False) + diff = abs(test_order['amount'] - self.real_sell_orders[-1]['base']['amount']) + if diff <= self.order_min_quote: + self.replace_real_order_with_virtual(self.real_sell_orders[-1]) # Check for operational depth, buy side if (self.virtual_buy_orders and From 60b1fdc64080b4da033e010ba1bd016a46252577 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 27 Dec 2018 23:28:40 +0500 Subject: [PATCH 1136/1846] Prevent IndexError in fallback logic --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index abc34c6ed..9b76bcf1e 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -398,11 +398,11 @@ def maintain_strategy(self, *args, **kwargs): else: side_to_cancel = 'sell' - if side_to_cancel == 'buy': + if side_to_cancel == 'buy' and self.buy_orders: self.log.info('Free balances are not changing, bootstrap is off and target spread is not reached. ' 'Cancelling lowest buy order as a fallback') self.cancel_orders_wrapper(self.buy_orders[-1]) - elif side_to_cancel == 'sell': + elif side_to_cancel == 'sell' and self.sell_orders: self.log.info('Free balances are not changing, bootstrap is off and target spread is not reached. ' 'Cancelling highest sell order as a fallback') self.cancel_orders_wrapper(self.sell_orders[-1]) From cbf968cbe5bb65f7f5b05767233944ecd28ffee1 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 27 Dec 2018 23:50:44 +0500 Subject: [PATCH 1137/1846] Don't allow to place orders which crosses lower/upper bounds --- dexbot/strategies/staggered_orders.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 9b76bcf1e..5f35244c5 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -219,16 +219,6 @@ def maintain_strategy(self, *args, **kwargs): if not (self.quote_asset_threshold or self.base_asset_threshold): self.calculate_asset_thresholds() - # Check market's price boundaries - if self.market_center_price > self.upper_bound: - self.log.debug('Overriding upper bound by market center price: {} -> {:.8f}' - .format(self.upper_bound, self.market_center_price)) - self.upper_bound = self.market_center_price - elif self.market_center_price < self.lower_bound: - self.log.debug('Overriding lower bound by market center price: {} -> {:.8f}' - .format(self.lower_bound, self.market_center_price)) - self.lower_bound = self.market_center_price - # Remove orders that exceed boundaries success = self.remove_outside_orders(self.sell_orders, self.buy_orders) if not success: @@ -1361,12 +1351,18 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False if not self.is_instant_fill_enabled and price > lowest_ask and lowest_ask > 0 and place_order: self.log.info('Refusing to place an order which crosses lowest ask') return None + if price > self.upper_bound: + self.log.warning('Refusing to place buy order which crosses upper bound') + return None elif asset == 'quote': price = (order['price'] ** -1) / (1 + self.increment) highest_bid = float(self.ticker().get('highestBid')) if not self.is_instant_fill_enabled and price < highest_bid and highest_bid > 0 and place_order: self.log.info('Refusing to place an order which crosses highest bid') return None + if price < self.lower_bound: + self.log.warning('Refusing to place sell order which crosses lower bound') + return None # For next steps we do not need inverted price for sell orders price = order['price'] * (1 + self.increment) From ded63fce7f984926ae75e8476cda09cf7f87cb61 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 31 Dec 2018 07:52:32 +0200 Subject: [PATCH 1138/1846] Update Travis and AppVeyor to use Python 3.6 --- .travis.yml | 3 +++ appveyor.yml | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index ca5347673..f075117c5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,9 +6,12 @@ matrix: python: '3.6' - os: osx language: generic + python: '3.6' before_install: - brew update install: + - pip install pyinstaller + - pip install --upgrade setuptools - make install script: - echo "@TODO - Running tests..." diff --git a/appveyor.yml b/appveyor.yml index e99c2d046..4db634823 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,8 +4,8 @@ image: Visual Studio 2015 environment: matrix: - # Python 3.5.3 - 64-bit - - PYTHON: "C:\\Python35-x64" + # Python 3.6.6 - 64-bit + - PYTHON: "C:\\Python36-x64" #---------------------------------# # Build # @@ -18,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:\Python35-x64\python.exe c:\Python35-x64\python3.exe + - copy c:\Python36-x64\python.exe c:\Python36-x64\python3.exe - python --version - make install From e629fa8278c800545c1b8a6c3b65c36184d65872 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 31 Dec 2018 08:50:06 +0200 Subject: [PATCH 1139/1846] Change dexbot version number to 0.9.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 625751044..81e19a7d4 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.8.5' +VERSION = '0.9.0' AUTHOR = 'Codaone Oy' __version__ = VERSION From f692500426210f26192c6b42eaea68f3882708bd Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 31 Dec 2018 09:21:31 +0200 Subject: [PATCH 1140/1846] Change dexbot version number to 0.9.1 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 81e19a7d4..c4f7e1082 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.0' +VERSION = '0.9.1' AUTHOR = 'Codaone Oy' __version__ = VERSION From d6443346c9df03a4745ad70207e9ea831558886d Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 31 Dec 2018 10:38:21 +0200 Subject: [PATCH 1141/1846] Change dexbot version number to 0.9.2 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index c4f7e1082..c13a62c8f 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.1' +VERSION = '0.9.2' AUTHOR = 'Codaone Oy' __version__ = VERSION From cb384cfca8ae2faf2be72f129e8fe9b837670c71 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 31 Dec 2018 11:47:20 +0200 Subject: [PATCH 1142/1846] Change dexbot version number to 0.9.3 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index c13a62c8f..424b74442 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.2' +VERSION = '0.9.3' AUTHOR = 'Codaone Oy' __version__ = VERSION From 9acd5e97aa0d48f80b38f1edbdf08909f2e87a21 Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 31 Dec 2018 12:53:47 +0200 Subject: [PATCH 1143/1846] Fix error when trying to delete worker from GUI --- dexbot/worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/worker.py b/dexbot/worker.py index 47c5e3ea5..ccdaf126c 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -236,7 +236,7 @@ def remove_market(self, worker_name): def remove_offline_worker(config, worker_name, bitshares_instance): # Initialize the base strategy to get control over the data strategy = StrategyBase(worker_name, config, bitshares_instance=bitshares_instance) - strategy.purge() + strategy.clear_all_worker_data() @staticmethod def remove_offline_worker_data(worker_name): From 4c1014be0c5be1dba0c99557a7d24ca34605f04f Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 31 Dec 2018 13:07:15 +0200 Subject: [PATCH 1144/1846] Change dexbot version number to 0.9.4 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 424b74442..dce3ee06f 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.3' +VERSION = '0.9.4' AUTHOR = 'Codaone Oy' __version__ = VERSION From a2b1462d78d7154cb10871a7cec9a44c8d6664de Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 31 Dec 2018 13:18:33 +0200 Subject: [PATCH 1145/1846] Change dexbot version number to 0.9.5 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index dce3ee06f..6fc775a48 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.4' +VERSION = '0.9.5' AUTHOR = 'Codaone Oy' __version__ = VERSION From b7149f3fd58031815efd6c3b983a0ba706b862d7 Mon Sep 17 00:00:00 2001 From: Juhani Haapala Date: Mon, 7 Jan 2019 19:09:05 +0200 Subject: [PATCH 1146/1846] Fix ui latency when getting node latency --- dexbot/views/worker_list.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index beba741ac..28edb9382 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -137,14 +137,15 @@ def _update_statusbar_message(self): self.statusbar_updater_first_run = False time.sleep(1) - idle_add(self.set_statusbar_message) + msg = self.get_statusbar_message() + idle_add(self.set_statusbar_message, msg) 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): + def get_statusbar_message(self): node = self.config['node'] try: start = time.time() @@ -154,9 +155,12 @@ def set_statusbar_message(self): latency = -1 if latency != -1: - self.status_bar.showMessage("ver {} - Node delay: {:.2f}ms".format(__version__, latency)) + return "ver {} - Node delay: {:.2f}ms".format(__version__, latency) else: - self.status_bar.showMessage("ver {} - Node disconnected".format(__version__)) + return "ver {} - Node disconnected".format(__version__) + + def set_statusbar_message(self, msg): + self.status_bar.showMessage(msg) def set_worker_status(self, worker_name, level, status): if worker_name != 'NONE': From 3273d6408c6e9f97e3e0e04a4a851d7efce1292c Mon Sep 17 00:00:00 2001 From: gabev Date: Sat, 19 Jan 2019 11:15:29 +0200 Subject: [PATCH 1147/1846] Add Dockerfile --- Dockerfile | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..e7fc3f8a8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +# Download base image Ubuntu 16.04 +FROM ubuntu:16.04 + +# Update Ubuntu Software repository +RUN apt-get update + +RUN apt-get install -y software-properties-common + +RUN add-apt-repository universe + +# Install dependencies and then DEXBot +RUN apt-get install -y --install-recommends gcc libssl-dev python3-pip python3-dev python3-async whiptail inetutils-ping wget + +# Install app dependencies + +RUN pip3 install pyyaml +RUN pip3 install uptick +RUN pip3 install tabulate + +# Download and Install DEXBot + +RUN wget https://github.com/Codaone/DEXBot/archive/0.9.5.tar.gz +RUN tar zxvpf 0.9.5.tar.gz && rm -rf 0.9.5.tar.gz +RUN cd DEXBot-0.9.5 +WORKDIR DEXBot-0.9.5 +RUN make +RUN make install-user + + + + + From 44e8816f4caad8e86cea7f8f99522388d8a525d3 Mon Sep 17 00:00:00 2001 From: gabev Date: Thu, 24 Jan 2019 20:05:29 -0800 Subject: [PATCH 1148/1846] Update Dockerfile --- Dockerfile | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index e7fc3f8a8..5d3775d0b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,32 +1,37 @@ -# Download base image Ubuntu 16.04 -FROM ubuntu:16.04 +# Download base image Ubuntu 18.04 +FROM ubuntu:18.04 # Update Ubuntu Software repository -RUN apt-get update - -RUN apt-get install -y software-properties-common - -RUN add-apt-repository universe +RUN apt-get update +RUN apt-get install -y software-properties-common +RUN add-apt-repository universe # Install dependencies and then DEXBot -RUN apt-get install -y --install-recommends gcc libssl-dev python3-pip python3-dev python3-async whiptail inetutils-ping wget +RUN apt-get install -y --install-recommends gcc libssl-dev python3-pip python3-dev python3-async whiptail inetutils-ping wget sudo git # Install app dependencies -RUN pip3 install pyyaml +RUN pip3 install pyyaml RUN pip3 install uptick RUN pip3 install tabulate +RUN pip3 install ruamel.yaml +RUN pip3 install sqlalchemy +RUN pip3 install ccxt # Download and Install DEXBot -RUN wget https://github.com/Codaone/DEXBot/archive/0.9.5.tar.gz +RUN wget https://github.com/Codaone/DEXBot/archive/0.9.5.tar.gz RUN tar zxvpf 0.9.5.tar.gz && rm -rf 0.9.5.tar.gz RUN cd DEXBot-0.9.5 WORKDIR DEXBot-0.9.5 RUN make RUN make install-user +ENV LC_ALL=C.UTF-8 +ENV LANG=C.UTF-8 +#Add dexbot user - - +RUN useradd -m dexbot && echo "dexbot:dexbot" | chpasswd && adduser dexbot sudo +RUN usermod -aG sudo dexbot +USER dexbot From 5ba892d86b296086c7317e71707ee22c18f2c936 Mon Sep 17 00:00:00 2001 From: gabev Date: Thu, 24 Jan 2019 20:07:32 -0800 Subject: [PATCH 1149/1846] Update Dockerfile --- Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5d3775d0b..09beceb52 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,16 +2,16 @@ FROM ubuntu:18.04 # Update Ubuntu Software repository -RUN apt-get update -RUN apt-get install -y software-properties-common -RUN add-apt-repository universe +RUN apt-get update +RUN apt-get install -y software-properties-common +RUN add-apt-repository universe # Install dependencies and then DEXBot RUN apt-get install -y --install-recommends gcc libssl-dev python3-pip python3-dev python3-async whiptail inetutils-ping wget sudo git # Install app dependencies -RUN pip3 install pyyaml +RUN pip3 install pyyaml RUN pip3 install uptick RUN pip3 install tabulate RUN pip3 install ruamel.yaml From 1b2e0cc12440dfb7450ef2e5b5db4c1f869e7158 Mon Sep 17 00:00:00 2001 From: gabev Date: Sat, 26 Jan 2019 21:04:54 -0800 Subject: [PATCH 1150/1846] Update Dockerfile --- Dockerfile | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 09beceb52..551e1f9c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ RUN apt-get update RUN apt-get install -y software-properties-common RUN add-apt-repository universe -# Install dependencies and then DEXBot +# Install dependencies RUN apt-get install -y --install-recommends gcc libssl-dev python3-pip python3-dev python3-async whiptail inetutils-ping wget sudo git # Install app dependencies @@ -20,10 +20,9 @@ RUN pip3 install ccxt # Download and Install DEXBot -RUN wget https://github.com/Codaone/DEXBot/archive/0.9.5.tar.gz -RUN tar zxvpf 0.9.5.tar.gz && rm -rf 0.9.5.tar.gz -RUN cd DEXBot-0.9.5 -WORKDIR DEXBot-0.9.5 +RUN git clone https://github.com/Codaone/DEXBot.git /home/Dexbot/ +RUN cd /home/Dexbot/ +WORKDIR /home/Dexbot/ RUN make RUN make install-user From 2756934fc32bd8d01cf0a191826da75540c7f60c Mon Sep 17 00:00:00 2001 From: Bohdan V Date: Sun, 27 Jan 2019 15:42:34 +0200 Subject: [PATCH 1151/1846] Add VERSION support into Dockerfile ARG --- Dockerfile | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Dockerfile b/Dockerfile index 551e1f9c7..97b53d6cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,36 +1,36 @@ # Download base image Ubuntu 18.04 FROM ubuntu:18.04 +# Variable arguments to populate labels +ARG VERSION=0.9.5 +ARG USER=dexbot + +# Set ENV variables +ENV LC_ALL C.UTF-8 +ENV LANG C.UTF-8 +ENV DEXBOT_HOME_PATH /home/$USER +ENV DEXBOT_REPO_PATH $DEXBOT_HOME_PATH/repo +ENV PATH $DEXBOT_HOME_PATH/.local/bin:$PATH + # Update Ubuntu Software repository RUN apt-get update RUN apt-get install -y software-properties-common -RUN add-apt-repository universe +RUN add-apt-repository universe # Install dependencies RUN apt-get install -y --install-recommends gcc libssl-dev python3-pip python3-dev python3-async whiptail inetutils-ping wget sudo git -# Install app dependencies +# Create user and change workdir +RUN groupadd -r $USER && useradd -r -g $USER $USER +WORKDIR $DEXBOT_HOME_PATH +RUN chown -R $USER:$USER $DEXBOT_HOME_PATH +USER dexbot -RUN pip3 install pyyaml -RUN pip3 install uptick -RUN pip3 install tabulate -RUN pip3 install ruamel.yaml -RUN pip3 install sqlalchemy -RUN pip3 install ccxt +RUN pip3 install --user pyyaml uptick tabulate ruamel.yaml sqlalchemy ccxt # Download and Install DEXBot -RUN git clone https://github.com/Codaone/DEXBot.git /home/Dexbot/ -RUN cd /home/Dexbot/ -WORKDIR /home/Dexbot/ -RUN make -RUN make install-user +RUN git clone https://github.com/Codaone/DEXBot.git -b $VERSION $DEXBOT_REPO_PATH +RUN cd $DEXBOT_REPO_PATH && make install-user +RUN rm -rf $DEXBOT_REPO_PATH -ENV LC_ALL=C.UTF-8 -ENV LANG=C.UTF-8 - -#Add dexbot user - -RUN useradd -m dexbot && echo "dexbot:dexbot" | chpasswd && adduser dexbot sudo -RUN usermod -aG sudo dexbot -USER dexbot From 771756407f61ba2ff664d8a1bc6a2798b5aaafb4 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 24 Jan 2019 23:24:44 +0500 Subject: [PATCH 1152/1846] Fix division by zero in get_market_xxx_price Fixes #438 --- dexbot/strategies/base.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index bf6faa6e7..a62a018bb 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -728,6 +728,10 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, exclude_own_orders quote_amount += missing_amount break + # Prevent division by zero + if not quote_amount: + return 0.0 + return base_amount / quote_amount def get_market_orders(self, depth=1, updated=True): @@ -831,6 +835,10 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, exclude_own_order quote_amount += missing_amount / order['price'] break + # Prevent division by zero + if not quote_amount: + return 0.0 + return base_amount / quote_amount def get_market_spread(self, quote_amount=0, base_amount=0): From b7256428d426d2c0b9de8b41a20d1c100bd83671 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 7 Feb 2019 01:09:37 +0500 Subject: [PATCH 1153/1846] Handle trx broadcasting errors in replace_virtual_order_with_real() Closes: #446 --- dexbot/strategies/staggered_orders.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 5f35244c5..1a2858af5 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -605,12 +605,20 @@ def replace_virtual_order_with_real(self, order): quote_amount = order['quote']['amount'] price = order['price'] self.log.info('Replacing virtual buy order with real order') - new_order = self.place_market_buy_order(quote_amount, price, returnOrderId=True) + try: + new_order = self.place_market_buy_order(quote_amount, price, returnOrderId=True) + except bitsharesapi.exceptions.RPCError as e: + self.log.exception('Error broadcasting trx:') + return False else: quote_amount = order['base']['amount'] price = order['price'] ** -1 self.log.info('Replacing virtual sell order with real order') - new_order = self.place_market_sell_order(quote_amount, price, returnOrderId=True) + try: + new_order = self.place_market_sell_order(quote_amount, price, returnOrderId=True) + except bitsharesapi.exceptions.RPCError as e: + self.log.exception('Error broadcasting trx:') + return False if new_order: # Cancel virtual order From 72c3bc9f528f20dcbdc9de2364dd8892ba3f24ac Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 8 Feb 2019 00:20:37 +0500 Subject: [PATCH 1154/1846] Log order id in case of Exception in get_order() Because issue #367 is not reproducible, add order_id logging for the possible future occurencies of this bug. Closes: #367 --- dexbot/strategies/base.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index bf6faa6e7..a22e69c11 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1370,7 +1370,11 @@ def get_order(order_id, return_none=True): return None if 'id' in order_id: order_id = order_id['id'] - order = Order(order_id) + try: + order = Order(order_id) + except Exception: + log.error('Got an exception getting order id {}'.format(order_id)) + raise if return_none and order['deleted']: return None return order From 997bdd735a15698ce3aa7dbb3fd82f4ddf3d11c4 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 8 Feb 2019 21:54:11 +0500 Subject: [PATCH 1155/1846] Rename variables to be less confusing --- dexbot/strategies/relative_orders.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 59a5757eb..79bcf3200 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -171,7 +171,7 @@ def tick(self, d): self.counter += 1 @property - def amount_quote(self): + def amount_to_sell(self): """ Get quote amount, calculate if order size is relative """ if self.is_relative_order_size: @@ -181,7 +181,7 @@ def amount_quote(self): return self.order_size @property - def amount_base(self): + def amount_to_buy(self): """ Get base amount, calculate if order size is relative """ if self.is_relative_order_size: @@ -244,20 +244,20 @@ def update_orders(self): order_ids = [] expected_num_orders = 0 - amount_base = self.amount_base - amount_quote = self.amount_quote + amount_to_buy = self.amount_to_buy + amount_to_sell = self.amount_to_sell # Buy Side - if amount_base: - buy_order = self.place_market_buy_order(amount_base, self.buy_price, True) + if amount_to_buy: + buy_order = self.place_market_buy_order(amount_to_buy, self.buy_price, True) if buy_order: self.save_order(buy_order) order_ids.append(buy_order['id']) expected_num_orders += 1 # Sell Side - if amount_quote: - sell_order = self.place_market_sell_order(amount_quote, self.sell_price, True) + if amount_to_sell: + sell_order = self.place_market_sell_order(amount_to_sell, self.sell_price, True) if sell_order: self.save_order(sell_order) order_ids.append(sell_order['id']) From 43b301428ae33fe3239ba5d300cd4715bcac9bf2 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 10 Feb 2019 17:47:37 +0500 Subject: [PATCH 1156/1846] Add check for minimal order amount When using relative order amount, we need to make sure either BASE or QUOTE amount will not be less than minimal possible amount of both assets. Minimal possible amount is based on asset precision. E.g., for Bitcoin minimal amount is 0.00000001 (precision = 8). Closes: #276 --- dexbot/strategies/relative_orders.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 79bcf3200..15628d565 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -174,22 +174,32 @@ def tick(self, d): def amount_to_sell(self): """ Get quote amount, calculate if order size is relative """ + amount = self.order_size if self.is_relative_order_size: quote_balance = float(self.balance(self.market["quote"])) - return quote_balance * (self.order_size / 100) - else: - return self.order_size + amount = quote_balance * (self.order_size / 100) + + # Sell / receive amount should match x2 of minimal possible fraction of asset + if (amount < 2 * 10 ** -self.market['quote']['precision'] or + amount * self.sell_price < 2 * 10 ** -self.market['base']['precision']): + amount = 0 + return amount @property def amount_to_buy(self): """ Get base amount, calculate if order size is relative """ + amount = self.order_size 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 - return base_balance * (self.order_size / 100) / self.buy_price - else: - return self.order_size + amount = base_balance * (self.order_size / 100) / self.buy_price + + # Sell / receive amount should match x2 of minimal possible fraction of asset + if (amount < 2 * 10 ** -self.market['quote']['precision'] or + amount * self.buy_price < 2 * 10 ** -self.market['base']['precision']): + amount = 0 + return amount def calculate_order_prices(self): # Set center price as None, in case dynamic has not amount given, center price is calculated from market orders From 36cb8e4bbd021aceb3eb8ea7c6f8f95c7adbb6b4 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 8 Feb 2019 19:59:34 +0500 Subject: [PATCH 1157/1846] Fix market fee in get_market_xxx_price() Market fee calculation was broken because get_market_fee() returned a market fee for self.fee_asset. Because get_market_fee() didn't used anywhere except get_market_xxx_price(), just remove it. Closes: #453 --- dexbot/strategies/base.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index bf6faa6e7..d9450b7d1 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -516,13 +516,6 @@ def get_allocated_assets(self, order_ids=None, return_asset=False): return {'quote': quote, 'base': base} - def get_market_fee(self): - """ Returns the fee percentage for buying specified asset - - :return: Fee percentage in decimal form (0.025) - """ - return self.fee_asset.market_fee_percent - def get_market_buy_orders(self, depth=10): """ Fetches most recent data and returns list of buy orders. @@ -698,7 +691,7 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, exclude_own_orders if not market_buy_orders: market_buy_orders = self.get_market_buy_orders(depth=self.fetch_depth) - market_fee = self.get_market_fee() + market_fee = self.market['base'].market_fee_percent target_amount = asset_amount * (1 + market_fee) @@ -801,7 +794,7 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, exclude_own_order if not market_sell_orders: market_sell_orders = self.get_market_sell_orders(depth=self.fetch_depth) - market_fee = self.get_market_fee() + market_fee = self.market['quote'].market_fee_percent target_amount = asset_amount * (1 + market_fee) From f011c7681a62c41297a4df96f81d3180d23fc3a2 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 8 Feb 2019 00:32:28 +0500 Subject: [PATCH 1158/1846] Enforce `spread > increment` check Require diff between spread and increment to be greater than sum of both QUOTE and BASE market fees. Closes: #450 --- dexbot/strategies/staggered_orders.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 5f35244c5..8c4fe6329 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -114,8 +114,10 @@ def __init__(self, *args, **kwargs): else: self.center_price = self.worker['center_price'] - if self.target_spread < self.increment: - self.log.error('Spread must be more than increment, refusing to work because worker will make losses') + fee_sum = self.market['base'].market_fee_percent + self.market['quote'].market_fee_percent + if self.target_spread - self.increment < fee_sum: + self.log.error('Spread must be greater than increment by at least {}, refusing to work because worker' + ' will make losses'.format(fee_sum)) self.disabled = True if self.operational_depth < 2: From c1132b4ee9b2b4c50ede8654e02fba5db08a9b21 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 12 Feb 2019 23:03:13 +0500 Subject: [PATCH 1159/1846] Bump uptick version to fix tabulate issue File "/home/vvk/devel/DEXBot/venv/lib/python3.6/site-packages/uptick-0.2.0-py3.6.egg/uptick/ui.py", line 7, in from tabulate import tabulate ModuleNotFoundError: No module named 'tabulate' Closes: #431 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b5bb25fd2..b058ac8db 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ console_scripts = ['dexbot-cli = dexbot.cli:main'] install_requires = [ 'bitshares==0.2.1', - 'uptick==0.2.0', + 'uptick==0.2.1', 'click', 'sqlalchemy', 'ruamel.yaml>=0.15.37', From afb5a22717559285157b02b37d885e48b56f26b4 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 14 Feb 2019 00:36:55 +0500 Subject: [PATCH 1160/1846] Prevent ZeroDivisionError in update_gui_profit() Closes: #462 --- dexbot/strategies/base.py | 75 +++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index bf6faa6e7..8f8f69231 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1225,6 +1225,48 @@ def get_profit_estimation_data(self, seconds): return self.get_balance_history(self.config['workers'][self.worker_name].get('account'), self.worker_name, seconds) + def calc_profit(self): + """ Calculate relative profit for the current worker + """ + profit = 0 + time_range = 60 * 60 * 24 * 7 # 7 days + current_time = time.time() + timestamp = current_time - time_range + + # Fetch the balance from history + old_data = self.get_balance_history(self.config['workers'][self.worker_name].get('account'), self.worker_name, + timestamp, self.base_asset, self.quote_asset) + if old_data: + earlier_base = old_data.base_total + earlier_quote = old_data.quote_total + old_center_price = old_data.center_price + center_price = self.get_market_center_price() + + if not (old_center_price or center_price): + return profit + + # Calculate max theoretical balances based on starting price + old_max_quantity_base = earlier_base + earlier_quote * old_center_price + old_max_quantity_quote = earlier_quote + earlier_base / old_center_price + + if not (old_max_quantity_base or old_max_quantity_quote): + return profit + + # Current balances + balance = self.count_asset() + base_balance = balance['base'] + quote_balance = balance['quote'] + + # Calculate max theoretical current balances + max_quantity_base = base_balance + quote_balance * center_price + max_quantity_quote = quote_balance + base_balance / center_price + + base_roi = max_quantity_base / old_max_quantity_base + quote_roi = max_quantity_quote / old_max_quantity_quote + profit = round(math.sqrt(base_roi * quote_roi) - 1, 4) + + return profit + def write_order_log(self, worker_name, order): """ Write order log to csv file @@ -1441,37 +1483,8 @@ def update_gui_slider(self): self['slider'] = percentage def update_gui_profit(self): - profit = 0 - time_range = 60 * 60 * 24 * 7 # 7 days - current_time = time.time() - timestamp = current_time - time_range - - # Fetch the balance from history - old_data = self.get_balance_history(self.config['workers'][self.worker_name].get('account'), self.worker_name, - timestamp, self.base_asset, self.quote_asset) - if old_data: - earlier_base = old_data.base_total - earlier_quote = old_data.quote_total - old_center_price = old_data.center_price - - # Calculate max theoretical balances based on starting price - old_max_quantity_base = earlier_base + earlier_quote * old_center_price - old_max_quantity_quote = earlier_quote + earlier_base / old_center_price - - # Current balances - balance = self.count_asset() - base_balance = balance['base'] - quote_balance = balance['quote'] - - # Calculate max theoretical current balances - center_price = self.get_market_center_price() - max_quantity_base = base_balance + quote_balance * center_price - max_quantity_quote = quote_balance + base_balance / center_price - - base_roi = max_quantity_base / old_max_quantity_base - quote_roi = max_quantity_quote / old_max_quantity_quote - profit = round(math.sqrt(base_roi * quote_roi) - 1, 4) + profit = self.calc_profit() - # Add to idle que + # Add to idle queue idle_add(self.view.set_worker_profit, self.worker_name, float(profit)) self['profit'] = profit From 56780ce02348505cf7f732ca635528c383c7e9c5 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 14 Feb 2019 17:31:47 -0800 Subject: [PATCH 1161/1846] allow pyqt >=5.10 for OSX compatibility --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f7811eb18..dd152c8ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pyqt5==5.11.3 +pyqt5>=5.10 pyqt-distutils==0.7.3 pyinstaller==3.4 click-datetime==0.2 From cbc0db4be3fefc2de1548afe18d59a0eaa7a41c5 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 14 Feb 2019 17:31:47 -0800 Subject: [PATCH 1162/1846] allow pyqt >=5.10 for OSX compatibility --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f7811eb18..dd152c8ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pyqt5==5.11.3 +pyqt5>=5.10 pyqt-distutils==0.7.3 pyinstaller==3.4 click-datetime==0.2 From ff33b426b7a2da2a21da0c013e4abbbc6b195c87 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 14 Feb 2019 21:50:57 -0800 Subject: [PATCH 1163/1846] move setup.py install pkgs to requirements.txt --- requirements.txt | 9 +++++++++ setup.py | 9 --------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/requirements.txt b/requirements.txt index dd152c8ee..4ea8d9b94 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,12 @@ requests>=2.20.0 yarl==1.1.0 ccxt==1.17.434 pywaves==0.8.20 +bitshares==0.2.1 +uptick==0.2.0 +ruamel.yaml>=0.15.37 +appdirs>=1.4.3 +pycryptodomex==3.6.4 +websocket-client==0.54.0 +sdnotify==0.3.2 +sqlalchemy==1.2.11 +click==7.0 diff --git a/setup.py b/setup.py index b5bb25fd2..5e5f79182 100755 --- a/setup.py +++ b/setup.py @@ -7,15 +7,6 @@ cmd_class = {} console_scripts = ['dexbot-cli = dexbot.cli:main'] install_requires = [ - 'bitshares==0.2.1', - 'uptick==0.2.0', - 'click', - 'sqlalchemy', - 'ruamel.yaml>=0.15.37', - 'sdnotify', - 'appdirs>=1.4.3', - 'pycryptodomex==3.6.4', - 'websocket-client==0.54.0' ] From b6e4eba8555b538808512ecd44907ac9f57de711 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 15 Feb 2019 12:21:45 +0200 Subject: [PATCH 1164/1846] Fix PEP8 warning Removed extra space --- 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 28edb9382..29a4a5fa0 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -155,7 +155,7 @@ def get_statusbar_message(self): latency = -1 if latency != -1: - return "ver {} - Node delay: {:.2f}ms".format(__version__, latency) + return "ver {} - Node delay: {:.2f}ms".format(__version__, latency) else: return "ver {} - Node disconnected".format(__version__) From f9ead7c0b26eae9494314beb2fb14d01206ae11d Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 15 Feb 2019 12:43:57 +0200 Subject: [PATCH 1165/1846] Change pyqt5 version requirement Force using pyqt5 version 5.10 since it is the most stable on all supported systems. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4ea8d9b94..d951a327e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pyqt5>=5.10 +pyqt5==5.10 pyqt-distutils==0.7.3 pyinstaller==3.4 click-datetime==0.2 From 2083ea4a6c862c1a736d58fbe5d56082a7e18427 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 15 Feb 2019 13:09:32 +0200 Subject: [PATCH 1166/1846] Change dexbot version number to 0.9.6 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 6fc775a48..9a2d33f0a 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.5' +VERSION = '0.9.6' AUTHOR = 'Codaone Oy' __version__ = VERSION From 8ce0b0190d50f1e35fc2880b3177bb3d5e554d1d Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 15 Feb 2019 14:06:45 +0200 Subject: [PATCH 1167/1846] Change dexbot version number to 0.9.7 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 9a2d33f0a..c966d2229 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.6' +VERSION = '0.9.7' AUTHOR = 'Codaone Oy' __version__ = VERSION From e8be9a5901ded4d12eb9118a273900ed4e8ad1e5 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 15 Feb 2019 14:16:03 +0200 Subject: [PATCH 1168/1846] Change dexbot version number to 0.9.8 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index c966d2229..6b30beb73 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.7' +VERSION = '0.9.8' AUTHOR = 'Codaone Oy' __version__ = VERSION From ae8ac26cfc5684fb6ca395e0d8c108a405d18d12 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 15 Feb 2019 15:01:45 +0200 Subject: [PATCH 1169/1846] Change dexbot version number to 0.9.9 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 6b30beb73..b21c4e88e 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.8' +VERSION = '0.9.9' AUTHOR = 'Codaone Oy' __version__ = VERSION From 35ab49b3a0400360865f9f14c5d00f1c65ef4589 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 15 Feb 2019 15:19:51 +0200 Subject: [PATCH 1170/1846] Change dexbot version number to 0.9.10 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index b21c4e88e..4b4b451c8 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.9' +VERSION = '0.9.10' AUTHOR = 'Codaone Oy' __version__ = VERSION From 8de6d6fab6b451219cc3b27051d72d615aa57d8e Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 15 Feb 2019 15:32:31 +0200 Subject: [PATCH 1171/1846] Change dexbot version number to 0.9.11 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 4b4b451c8..7018f6b31 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.10' +VERSION = '0.9.11' AUTHOR = 'Codaone Oy' __version__ = VERSION From ca71d523707c27029d952e01e86eb6a2884eb0e7 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 15 Feb 2019 15:42:56 +0200 Subject: [PATCH 1172/1846] Change dexbot version number to 0.9.12 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 7018f6b31..3681d1f75 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.11' +VERSION = '0.9.12' AUTHOR = 'Codaone Oy' __version__ = VERSION From 080762be3228fd8ad625c3f194818e8ecd57ccfb Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 15 Feb 2019 15:55:10 +0200 Subject: [PATCH 1173/1846] Change dexbot version number to 0.9.13 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 3681d1f75..70112cb44 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.12' +VERSION = '0.9.13' AUTHOR = 'Codaone Oy' __version__ = VERSION From 62d73b61e744c927e1996043b14c0cbbf00c9254 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 15 Feb 2019 16:11:23 +0200 Subject: [PATCH 1174/1846] Change dexbot version number to 0.9.14 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 70112cb44..d8aa6b579 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.13' +VERSION = '0.9.14' AUTHOR = 'Codaone Oy' __version__ = VERSION From 4075a980ccfdab3fd5a0ad1d30c7a49a5ef299ea Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 15 Feb 2019 16:19:18 +0200 Subject: [PATCH 1175/1846] Change dexbot version number to 0.9.15 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index d8aa6b579..e7fc4f293 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.14' +VERSION = '0.9.15' AUTHOR = 'Codaone Oy' __version__ = VERSION From 9a0fc56b417fac1c4dbde43a65bdb941709a2a58 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Sun, 17 Feb 2019 23:43:15 -0800 Subject: [PATCH 1176/1846] allow numeric in asset names --- dexbot/strategies/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index c44e0a843..73b971bad 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -155,7 +155,7 @@ def configure(cls, return_base_config=True): ''), ConfigElement('market', 'string', 'USD:BTS', 'Market', 'BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"', - r'[A-Z\.]+[:\/][A-Z\.]+'), + r'[A-Z0-9\.]+[:\/][A-Z0-9\.]+'), ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset', 'Asset to be used to pay transaction fees', r'[A-Z\.]+') From 56a5f8a2542aab5cfd3991ef0666cc0181832499 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 18 Feb 2019 16:29:17 +0500 Subject: [PATCH 1177/1846] Add is_partially_filled() --- dexbot/strategies/base.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index c44e0a843..44e47fa43 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1034,6 +1034,30 @@ def is_sell_order(self, order): else: return False + def is_partially_filled(self, order, threshold=0.3): + """ Checks whether order was partially filled + + :param dict | order: Order instance + :param float | fill_threshold: Order fill threshold, relative + :return: bool | True = Order is filled more than threshold + False = Order is not partially filled + """ + if self.is_buy_order(order): + order_type = 'buy' + price = order['price'] + else: + order_type = 'sell' + price = order['price'] ** -1 + + if order['for_sale']['amount'] != order['base']['amount']: + diff_abs = order['base']['amount'] - order['for_sale']['amount'] + diff_rel = diff_abs / order['base']['amount'] + if diff_rel > threshold: + self.log.debug('Partially filled {} order: {} {} @ {:.8f}, filled: {:.2%}'.format( + order_type, order['base']['amount'], order['base']['symbol'], price, diff_rel)) + return True + return False + def pause(self): """ Pause the worker From fb0678c8078653d581d0d78574cb81f0491141b5 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 18 Feb 2019 16:29:43 +0500 Subject: [PATCH 1178/1846] Add is_too_small_amounts() --- dexbot/strategies/base.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 44e47fa43..ece292b9b 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1034,6 +1034,19 @@ def is_sell_order(self, order): else: return False + def is_too_small_amounts(self, amount_quote, amount_base): + """ Check whether amounts are within asset precision limits + :param float | amount_quote: QUOTE asset amount + :param float | amount_base: BASE asset amount + :return: bool | True = amounts are too small + False = amounts are within limits + """ + if (amount_quote < 2 * 10 ** -self.market['quote']['precision'] or + amount_base < 2 * 10 ** -self.market['base']['precision']): + return True + + return False + def is_partially_filled(self, order, threshold=0.3): """ Checks whether order was partially filled From 7e7806f3a2e084705c7cf97cc5239add5cafb7bd Mon Sep 17 00:00:00 2001 From: octomatic Date: Tue, 19 Feb 2019 01:00:12 +0000 Subject: [PATCH 1179/1846] add uptick description to wallet password create for cli and .ui --- dexbot/cli_conf.py | 2 +- dexbot/ui.py | 12 +++++++----- dexbot/views/ui/create_wallet_window.ui | 6 +++--- dexbot/views/ui/unlock_wallet_window.ui | 4 ++-- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index a5014eb98..16b2958eb 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -151,7 +151,7 @@ def setup_systemd(whiptail, config): path = os.path.expanduser(path) pathlib.Path(path).mkdir(parents=True, exist_ok=True) password = whiptail.prompt( - "The wallet password\n" + "The uptick wallet password\n" "NOTE: this will be saved on disc so the worker can run unattended. " "This means anyone with access to this computer's files can spend all your money", password=True) diff --git a/dexbot/ui.py b/dexbot/ui.py index 9c0018500..4408f1f0f 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -99,19 +99,21 @@ def new_func(ctx, *args, **kwargs): else: if systemd: # No user available to interact with - log.critical("Passphrase not available, exiting") + log.critical("Uptick Passphrase not available, exiting") sys.exit(78) # 'configuration error' in sysexits.h pwd = click.prompt( - "Current Wallet Passphrase", hide_input=True) + "Current Uptick Wallet Passphrase", hide_input=True) ctx.bitshares.wallet.unlock(pwd) else: if systemd: # No user available to interact with - log.critical("Wallet not installed, cannot run") + log.critical("Uptick Wallet not installed, cannot run") sys.exit(78) - click.echo("No wallet installed yet. Creating ...") + click.echo("No Uptick wallet installed yet. \n" + + "This is a password for encrypting " + + "the file that contains your private keys. Creating ...") pwd = click.prompt( - "Wallet Encryption Passphrase", + "Uptick Wallet Encryption Passphrase", hide_input=True, confirmation_prompt=True) ctx.bitshares.wallet.create(pwd) diff --git a/dexbot/views/ui/create_wallet_window.ui b/dexbot/views/ui/create_wallet_window.ui index 6e79eb32d..35991e248 100644 --- a/dexbot/views/ui/create_wallet_window.ui +++ b/dexbot/views/ui/create_wallet_window.ui @@ -32,7 +32,7 @@ - Before you can start using DEXBot, you need to create a wallet. + Before you can start using DEXBot, you need to create an uptick wallet. Qt::AlignCenter @@ -42,7 +42,7 @@ - Wallet password is used to encrypt your BitShares account private keys. + The Uptick Wallet password is used to encrypt your BitShares account private keys. Qt::AlignCenter @@ -88,7 +88,7 @@ - Wallet password + Uptick Wallet password password_input diff --git a/dexbot/views/ui/unlock_wallet_window.ui b/dexbot/views/ui/unlock_wallet_window.ui index f6989a8c3..b8a7cd1ae 100644 --- a/dexbot/views/ui/unlock_wallet_window.ui +++ b/dexbot/views/ui/unlock_wallet_window.ui @@ -32,7 +32,7 @@ - Please enter your wallet password before continuing. + Please enter your Uptick wallet password before continuing. Qt::AlignCenter @@ -81,7 +81,7 @@ - Wallet password + Uptick Wallet password password_input From 2bf6a37e415cb6b60b26256118b4f4bec928f39d Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 12 Feb 2019 00:27:24 +0500 Subject: [PATCH 1180/1846] Fix obtaining balance in update_gui_slider --- dexbot/strategies/base.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index c44e0a843..cb848a82b 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1471,13 +1471,7 @@ def update_gui_slider(self): if not latest_price: return - order_ids = None - orders = self.fetch_orders() - - if orders: - order_ids = orders.keys() - - total_balance = self.count_asset(order_ids) + total_balance = self.count_asset() total = (total_balance['quote'] * latest_price) + total_balance['base'] if not total: # Prevent division by zero From 3c391fde228b5d8b3da820b35354a00dc615974b Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 4 Feb 2019 01:01:57 +0500 Subject: [PATCH 1181/1846] Simplify fallback logic On the sell side, don't cancel furthest orders ever, instead just place closer sell order using available (not allocated) funds. If target spread still not reached, cancel furthest buy side order. Closes: #443 --- dexbot/strategies/staggered_orders.py | 60 +++++++-------------------- 1 file changed, 15 insertions(+), 45 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 9a6ad9409..67b7a33d3 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -281,21 +281,17 @@ def maintain_strategy(self, *args, **kwargs): # BASE asset check if self.base_balance > self.base_asset_threshold: # Allocate available BASE funds - base_allocated = False self.allocate_asset('base', self.base_balance) - else: - base_allocated = True # QUOTE asset check if self.quote_balance > self.quote_asset_threshold: # Allocate available QUOTE funds - quote_allocated = False self.allocate_asset('quote', self.quote_balance) - else: - quote_allocated = True # Send pending operations + trx_executed = False if not self.bitshares.txbuffer.is_empty(): + trx_executed = True try: self.execute() except bitsharesapi.exceptions.RPCError: @@ -337,7 +333,8 @@ def maintain_strategy(self, *args, **kwargs): # Do not continue whether balances are changing or bootstrap is on if (self.bootstrapping or self.base_balance_history[0] != self.base_balance_history[2] or - self.quote_balance_history[0] != self.quote_balance_history[2]): + self.quote_balance_history[0] != self.quote_balance_history[2] or + trx_executed): self.last_check = datetime.now() self.log_maintenance_time() return @@ -364,42 +361,11 @@ def maintain_strategy(self, *args, **kwargs): self.last_check = datetime.now() self.log_maintenance_time() return - - # What amount of quote may be obtained if buy using avail base balance - can_obtain_quote = self.base_balance['amount'] * self.market_center_price - side_to_cancel = None - - """ The logic is following: compare on which side we have bigger free balance, then cancel furthest order on - that side to be able to place closer order. This is for situations when amount obtained from previous trade - is not enough to place closer order. - """ - if can_obtain_quote > self.quote_balance['amount'] and not base_allocated: - side_to_cancel = 'buy' - elif self.quote_balance['amount'] > can_obtain_quote and not quote_allocated: - side_to_cancel = 'sell' - - if not side_to_cancel: - """ Balance-based cancel logic didn't give a result, so use logic based on distance to market center - """ - # Measure which price is closer to the center - buy_distance = self.market_center_price - highest_buy_price - sell_distance = lowest_sell_price - self.market_center_price - - if buy_distance > sell_distance: - side_to_cancel = 'buy' - else: - side_to_cancel = 'sell' - - if side_to_cancel == 'buy' and self.buy_orders: - self.log.info('Free balances are not changing, bootstrap is off and target spread is not reached. ' - 'Cancelling lowest buy order as a fallback') - self.cancel_orders_wrapper(self.buy_orders[-1]) - elif side_to_cancel == 'sell' and self.sell_orders: - self.log.info('Free balances are not changing, bootstrap is off and target spread is not reached. ' - 'Cancelling highest sell order as a fallback') - self.cancel_orders_wrapper(self.sell_orders[-1]) - else: - self.log.info('Target spread is not reached but cannot determine what furthest order to cancel') + elif self.buy_orders: + # If target spread is not reached and no balance to allocate, cancel lowest buy order + self.log.info('Free balances are not changing, bootstrap is off and target spread is not reached. ' + 'Cancelling lowest buy order as a fallback') + self.cancel_orders_wrapper(self.buy_orders[-1]) self.last_check = datetime.now() self.log_maintenance_time() @@ -783,8 +749,9 @@ def allocate_asset(self, asset, asset_balance): own_asset_limit = None self.log.debug('Limiting {} order by opposite order: {:.{prec}f} {}'.format( order_type, opposite_asset_limit, opposite_symbol, prec=opposite_precision)) + allow_partial = True if asset == 'quote' else False self.place_closer_order(asset, closest_own_order, own_asset_limit=own_asset_limit, - opposite_asset_limit=opposite_asset_limit, allow_partial=False) + opposite_asset_limit=opposite_asset_limit, allow_partial=allow_partial) else: # Opposite side probably reached range bound, allow to place partial order self.place_closer_order(asset, closest_own_order, allow_partial=True) @@ -1433,7 +1400,10 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False self.log.debug('Not enough balance to place closer {} order; need/avail: {:.{prec}f}/{:.{prec}f}' .format(order_type, limiter, balance, prec=precision)) place_order = False - elif allow_partial and balance > hard_limit: + # Closer order should not be less than threshold + elif (allow_partial and + balance > hard_limit and + balance > order['base']['amount'] * self.partial_fill_threshold): self.log.debug('Limiting {} order amount to available asset balance: {:.{prec}f} {}' .format(order_type, balance, symbol, prec=precision)) if asset == 'base': From 14b0f5fe7cd7d5d79c12ee8b07d63ad8f0defd54 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 4 Feb 2019 23:24:09 +0500 Subject: [PATCH 1182/1846] Implement saving virtual orders into state Closes: #442 --- dexbot/strategies/staggered_orders.py | 62 +++++++++++++++++++-------- 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 9a6ad9409..12b277ddc 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -546,21 +546,33 @@ def remove_outside_orders(self, sell_orders, buy_orders): def restore_virtual_orders(self): """ Create virtual further orders in batch manner. This helps to place further orders quickly on startup. """ - if self.buy_orders: - furthest_order = self.real_buy_orders[-1] - while furthest_order['price'] > self.lower_bound * (1 + self.increment): - furthest_order = self.place_further_order('base', furthest_order, virtual=True) - if not isinstance(furthest_order, VirtualOrder): - # Failed to place order - break - - if self.sell_orders: - furthest_order = self.real_sell_orders[-1] - while furthest_order['price'] ** -1 < self.upper_bound / (1 + self.increment): - furthest_order = self.place_further_order('quote', furthest_order, virtual=True) - if not isinstance(furthest_order, VirtualOrder): - # Failed to place order - break + if self.buy_orders and self.sell_orders: + # Load orders from the database + orders = self.fetch_orders() + if orders: + self.log.info('Loading virtual orders from database') + for k, v in orders.items(): + self.virtual_orders.append(VirtualOrder(v)) + else: + if self.buy_orders: + furthest_order = self.real_buy_orders[-1] + while furthest_order['price'] > self.lower_bound * (1 + self.increment): + furthest_order = self.place_further_order('base', furthest_order, virtual=True) + if not isinstance(furthest_order, VirtualOrder): + # Failed to place order + break + + if self.sell_orders: + furthest_order = self.real_sell_orders[-1] + while furthest_order['price'] ** -1 < self.upper_bound / (1 + self.increment): + furthest_order = self.place_further_order('quote', furthest_order, virtual=True) + if not isinstance(furthest_order, VirtualOrder): + # Failed to place order + break + else: + # No real orders, assume we need to bootstrap, purge old orders + self.log.info('No real orders, purging old virtual orders') + self.clear_orders() # Set "restored" flag anyway to not break initial bootstrap self.virtual_orders_restored = True @@ -1849,6 +1861,8 @@ def place_virtual_buy_order(self, amount, price): order = VirtualOrder() order['price'] = price + # Assign price as id because we just need some unique id + order['id'] = order['price'] quote_asset = Amount(amount, self.market['quote']['symbol']) order['quote'] = quote_asset @@ -1864,6 +1878,8 @@ def place_virtual_buy_order(self, amount, price): # Immediately lower avail balance self.base_balance['amount'] -= order['base']['amount'] + self.save_order(order) + return order def place_virtual_sell_order(self, amount, price): @@ -1877,6 +1893,8 @@ def place_virtual_sell_order(self, amount, price): precision = self.market['quote']['precision'] order = VirtualOrder() + # Use not inverted price as unique id (inverted will cause intersections with buy orders) + order['id'] = price order['price'] = price ** -1 quote_asset = Amount(amount * price, self.market['base']['symbol']) @@ -1893,6 +1911,8 @@ def place_virtual_sell_order(self, amount, price): # Immediately lower avail balance self.quote_balance['amount'] -= order['base']['amount'] + self.save_order(order) + return order def cancel_orders_wrapper(self, orders, **kwargs): @@ -1902,11 +1922,15 @@ def cancel_orders_wrapper(self, orders, **kwargs): if not isinstance(orders, (list, set, tuple)): orders = [orders] - virtual_orders = [order['price'] for order in orders if isinstance(order, VirtualOrder)] - real_orders = [order for order in orders if 'id' in order] + virtual_orders = [order for order in orders if isinstance(order, VirtualOrder)] + real_orders = [order for order in orders if not isinstance(order, VirtualOrder)] - # Just rebuild virtual orders list to avoid calling Asset's __eq__ method - self.virtual_orders = [order for order in self.virtual_orders if order['price'] not in virtual_orders] + if virtual_orders: + # Just rebuild virtual orders list to avoid calling Asset's __eq__ method + self.virtual_orders = [order for order in self.virtual_orders if order not in virtual_orders] + # Also remove virtual order from database + for order in virtual_orders: + self.remove_order(order) if real_orders: return self.cancel_orders(real_orders, **kwargs) From 33d089d15e56f75c5a2289ec5ff530dfcfb388db Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 20 Feb 2019 15:00:17 +0500 Subject: [PATCH 1183/1846] Raise trx expiration time Default expiration time of 30 seconds may be too low in some cases (slow connection, time differences between local machine and the node). Also, time to irreversible block is more than 30 seconds. In case of chain microforks a node can recover transactions from shorter chain if they are not expired yet. E.g., with bigger expiration trx inclusion in irreversible block is more reliable. --- dexbot/gui.py | 2 +- dexbot/ui.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dexbot/gui.py b/dexbot/gui.py index d1543f70b..859525082 100644 --- a/dexbot/gui.py +++ b/dexbot/gui.py @@ -16,7 +16,7 @@ def __init__(self, sys_argv): super(App, self).__init__(sys_argv) config = Config() - bitshares_instance = BitShares(config['node'], num_retries=-1) + bitshares_instance = BitShares(config['node'], num_retries=-1, expiration=60) # Wallet unlock unlock_ctrl = WalletController(bitshares_instance) diff --git a/dexbot/ui.py b/dexbot/ui.py index 9c0018500..21e569c3c 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -81,6 +81,7 @@ def new_func(ctx, *args, **kwargs): ctx.bitshares = BitShares( ctx.config["node"], num_retries=-1, + expiration=60, **ctx.obj ) set_shared_bitshares_instance(ctx.bitshares) From a3dcfbc4111275e19cd92386c895a6a7a58bd0d5 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 20 Feb 2019 15:05:33 +0500 Subject: [PATCH 1184/1846] Handle trx expiration too much in future Trx expiration must fit into limit "head block time + 1 day". If it's not, the node is not properly synced (blockchain state) or client machine time is too much in the future. As a solution, try to switch node, and if it didn't help raise error to the user. Closes: #250 --- dexbot/strategies/base.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index c44e0a843..8037f1a81 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -22,6 +22,7 @@ from bitshares.instance import shared_bitshares_instance from bitshares.market import Market from bitshares.price import FilledOrder, Order, UpdateCallOrder +from bitshares.utils import formatTime # Number of maximum retries used to retry action before failing MAX_TRIES = 3 @@ -1197,6 +1198,18 @@ def retry_action(self, action, *args, **kwargs): self.log.warning("retrying on '{}'".format(str(exception))) self.bitshares.txbuffer.clear() time.sleep(6) # Wait at least a BitShares block + elif "trx.expiration <= now + chain_parameters.maximum_time_until_expiration" in str(exception): + if tries > MAX_TRIES: + info = self.bitshares.info() + raise Exception('Too much difference between node block time and trx expiration, please change ' + 'the node. Block time: {}, local time: {}' + .format(info['time'], formatTime(datetime.datetime.utcnow()))) + else: + tries += 1 + self.log.warning('Too much difference between node block time and trx expiration, switching ' + 'node') + self.bitshares.txbuffer.clear() + self.bitshares.rpc.next() elif "Assert Exception: delta.amount > 0: Insufficient Balance" in str(exception): self.log.critical('Insufficient balance of fee asset') raise From b3e8ff104abbf3804dee761a0508f6c1a86c7816 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 22 Feb 2019 00:23:25 +0500 Subject: [PATCH 1185/1846] Store log in installation directory for cli version Closes: #490 --- dexbot/ui.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dexbot/ui.py b/dexbot/ui.py index 9c0018500..826aab1ef 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -1,4 +1,5 @@ import os +import os.path import sys import logging import logging.config @@ -43,7 +44,8 @@ def new_func(ctx, *args, **kwargs): logger.addHandler(ch) # Logging to a file - fh = logging.FileHandler('dexbot.log') + filename = os.path.join(os.path.dirname(sys.argv[0]), 'dexbot.log') + fh = logging.FileHandler(filename) fh.setFormatter(formatter2) logger.addHandler(fh) From b5959e1ea3cb449c626adb525283beff83638c09 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 22 Feb 2019 09:21:21 +0200 Subject: [PATCH 1186/1846] Change dexbot version number to 0.9.16 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index e7fc4f293..12072403c 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.15' +VERSION = '0.9.16' AUTHOR = 'Codaone Oy' __version__ = VERSION From 9b00fcfdad4a57c81a161c8772539068bf460e13 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 22 Feb 2019 10:00:19 +0200 Subject: [PATCH 1187/1846] Change dexbot version number to 0.9.17 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 12072403c..1eb8b21c7 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.16' +VERSION = '0.9.17' AUTHOR = 'Codaone Oy' __version__ = VERSION From fde66e735cb0573ba000beb752dbc0990a7ba0cb Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 18 Feb 2019 17:00:23 +0500 Subject: [PATCH 1188/1846] Refactor cancel_orders() cancel_orders() should return True only when cancel was success. Closes: #467 --- dexbot/strategies/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index c44e0a843..9d6ee5e37 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -444,8 +444,10 @@ def cancel_orders(self, orders, batch_only=False): if not success and len(orders) > 1 and not batch_only: # One of the order cancels failed, cancel the orders one by one for order in orders: - self._cancel_orders(order) - return True + success = self._cancel_orders(order) + if not success: + return False + return success def count_asset(self, order_ids=None, return_asset=False): """ Returns the combined amount of the given order ids and the account balance From 3dacce6170170f8d00759b2c0278d67233c050ad Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 23 Feb 2019 14:06:23 +0500 Subject: [PATCH 1189/1846] Add '--logfile' command-line option --- dexbot/cli.py | 5 +++++ dexbot/ui.py | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index e778f4644..709e99de4 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -45,6 +45,11 @@ "--configfile", default=DEFAULT_CONFIG_FILE, ) +@click.option( + '--logfile', + default=None, + help='Override logfile location' +) @click.option( '--verbose', '-v', diff --git a/dexbot/ui.py b/dexbot/ui.py index 826aab1ef..b541ea9d4 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -44,7 +44,9 @@ def new_func(ctx, *args, **kwargs): logger.addHandler(ch) # Logging to a file - filename = os.path.join(os.path.dirname(sys.argv[0]), 'dexbot.log') + filename = ctx.obj.get('logfile') + if not filename: + filename = os.path.join(os.path.dirname(sys.argv[0]), 'dexbot.log') fh = logging.FileHandler(filename) fh.setFormatter(formatter2) logger.addHandler(fh) From 6d2a54e722fb6df61079ff6b0c58bf26cfabf414 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 20 Jan 2019 00:32:37 +0500 Subject: [PATCH 1190/1846] Add method for caclulating assets intersections --- dexbot/config.py | 61 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/dexbot/config.py b/dexbot/config.py index b5ac113d2..8016525dd 100644 --- a/dexbot/config.py +++ b/dexbot/config.py @@ -5,7 +5,7 @@ import appdirs from ruamel import yaml -from collections import OrderedDict +from collections import OrderedDict, defaultdict DEFAULT_CONFIG_DIR = appdirs.user_config_dir(APP_NAME, appauthor=AUTHOR) @@ -41,6 +41,8 @@ def __init__(self, config=None, path=None): self._config['node'] = self.node_list self.save_config() + self.intersections_data = None + def __setitem__(self, key, value): self._config[key] = value @@ -165,6 +167,63 @@ def construct_mapping(mapping_loader, node): construct_mapping) return yaml.load(stream, OrderedLoader) + @staticmethod + def assets_intersections(config): + """ Collect intersections of assets on the same account across multiple workers + + :return: defaultdict instance representing dict with intersections + + The goal of calculating assets intersections is to be able to use single account on multiple workers and + trade some common assets. For example, trade BTS/USD, BTC/BTS, ETH/BTC markets on same account. + + Configuration variable `operational_percent_xxx` defines what percent of total account balance should be + available for the worker. It may be set or omitted. + + The logic of splitting balance is following: workers who define `operational_percent_xxx` will take this + defined percent, and remaining workers will just split the remaining balance between each other. For + example, 3 workers with 30% 30% 30%, and 2 workers with 0. These 2 workers will take the remaining `(100 - + 3*30) / 2 = 5`. + + Example return as a dict: + {'foo': {'RUBLE': {'sum_pct': 0, 'zero_workers': 0}, + 'USD': {'sum_pct': 0, 'zero_workers': 0}, + 'CNY': {'sum_pct': 0, 'zero_workers': 0} + } + } + """ + def update_data(asset, operational_percent): + if isinstance(data[account][asset]['sum_pct'], float): + # Existing dict key + data[account][asset]['sum_pct'] += operational_percent + if not operational_percent: + # Increase count of workers with 0 op percent + data[account][asset]['num_zero_workers'] += 1 + else: + # Create new dict key + data[account][asset]['sum_pct'] = operational_percent + if operational_percent: + data[account][asset]['num_zero_workers'] = 0 + else: + data[account][asset]['num_zero_workers'] = 1 + + if data[account][asset]['sum_pct'] > 1: + raise ValueError('Operational percent for asset {} is more than 100%' + .format(asset)) + + tree = lambda: defaultdict(tree) + data = tree() + + for worker_name, worker in config['workers'].items(): + account = worker['account'] + quote_asset = worker['market'].split('/')[0] + base_asset = worker['market'].split('/')[1] + operational_percent_quote = worker.get('operational_percent_quote', 0) / 100 + operational_percent_base = worker.get('operational_percent_base', 0) / 100 + update_data(quote_asset, operational_percent_quote) + update_data(base_asset, operational_percent_base) + + return data + @property def node_list(self): """ A pre-defined list of Bitshares nodes. """ From 658d313a20a03b5515e34b5cf7dc361237471765 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 20 Jan 2019 00:40:03 +0500 Subject: [PATCH 1191/1846] Support assets intersection in StrategyBase() Add method for caclulating operational percent Method for caclulating operational percent of asset balance available to the worker when trading same assets on different markets on single account. Add configuration settings for operational balances. --- dexbot/strategies/base.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 73b971bad..0fae02e77 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -158,7 +158,11 @@ def configure(cls, return_base_config=True): r'[A-Z0-9\.]+[:\/][A-Z0-9\.]+'), ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset', 'Asset to be used to pay transaction fees', - r'[A-Z\.]+') + r'[A-Z\.]+'), + ConfigElement('operational_percent_quote', 'float', 0, 'QUOTE balance %', + 'Max % of QUOTE asset available to this worker', (0, None, 2, '%')), + ConfigElement('operational_percent_base', 'float', 0, 'BASE balance %', + 'Max % of BASE asset available to this worker', (0, None, 2, '%')), ] if return_base_config: @@ -230,8 +234,10 @@ def __init__(self, # Redirect this event to also call order placed and order matched self.onMarketUpdate += self._callbackPlaceFillOrders + self.assets_intersections_data = None if config: self.config = config + self.assets_intersections_data = Config.assets_intersections(config) else: self.config = config = Config.get_worker_config_file(name) @@ -248,6 +254,9 @@ def __init__(self, # Count of orders to be fetched from the API self.fetch_depth = 8 + self.operational_percent_quote = self.worker.get('operational_percent_quote', 0) / 100 + self.operational_percent_base = self.worker.get('operational_percent_base', 0) / 100 + # Set fee asset fee_asset_symbol = self.worker.get('fee_asset') @@ -516,6 +525,27 @@ def get_allocated_assets(self, order_ids=None, return_asset=False): return {'quote': quote, 'base': base} + def get_worker_share_for_asset(self, asset): + """ Returns operational percent of asset available to the worker + + :param str | asset: Which asset should be checked + :return: float: a value between 0-1 representing a percent + """ + intersections_data = self.assets_intersections_data[self.account.name][asset] + + if asset == self.market['base']['symbol']: + if self.operational_percent_base: + return self.operational_percent_base + else: + return (1 - intersections_data['sum_pct']) / intersections_data['num_zero_workers'] + elif asset == self.market['quote']['symbol']: + if self.operational_percent_quote: + return self.operational_percent_quote + else: + return (1 - intersections_data['sum_pct']) / intersections_data['num_zero_workers'] + else: + self.log.error('Got asset which is not used by this worker') + def get_market_buy_orders(self, depth=10): """ Fetches most recent data and returns list of buy orders. From 123eaa938de7aac4ec741f8d4bf175cc2e7bac27 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 20 Jan 2019 00:48:24 +0500 Subject: [PATCH 1192/1846] Refactor methods for getting own orders Small refactor to get unified names for properties and methods and to be able use methods instead of properties when `refresh=False` kwarg is needed. --- dexbot/strategies/base.py | 72 +++++++++++++++----------- dexbot/strategies/strategy_template.py | 2 +- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 0fae02e77..2818bb1fc 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -408,7 +408,7 @@ def calculate_worker_value(self, unit_of_measure): quote_total += balance['amount'] # Calculate value of the orders in unit of measure - orders = self.get_own_orders + orders = self.own_orders for order in orders: if order['base']['symbol'] == self.quote_asset: # Pick sell orders order's BASE amount, which is same as worker's QUOTE, to worker's BASE @@ -479,7 +479,7 @@ def count_asset(self, order_ids=None, return_asset=False): if order_ids is None: # Get all orders from Blockchain - order_ids = [order['id'] for order in self.get_own_orders] + order_ids = [order['id'] for order in self.own_orders] if order_ids: orders_balance = self.get_allocated_assets(order_ids) quote += orders_balance['quote'] @@ -948,6 +948,40 @@ def filter_sell_orders(self, orders, sort=None, invert=True): return sell_orders + def get_own_orders(self, refresh=True): + """ Return the account's open orders in the current market + + :param bool | refresh: Use most recent data + :return: List of Order objects + """ + orders = [] + + # Refresh account data + if refresh: + self.account.refresh() + + for order in self.account.openorders: + if self.worker["market"] == order.market and self.account.openorders: + orders.append(order) + + return orders + + def get_all_own_orders(self, refresh=True): + """ Return the worker's open orders in all markets + + :param bool | refresh: Use most recent data + :return: List of Order objects + """ + # Refresh account data + if refresh: + self.account.refresh() + + orders = [] + for order in self.account.openorders: + orders.append(order) + + return orders + def get_own_buy_orders(self, orders=None): """ Get own buy orders from current market, or from a set of orders passed for this function. @@ -955,7 +989,7 @@ def get_own_buy_orders(self, orders=None): """ if not orders: # List of orders was not given so fetch everything from the market - orders = self.get_own_orders + orders = self.own_orders return self.filter_buy_orders(orders) @@ -966,7 +1000,7 @@ def get_own_sell_orders(self, orders=None): """ if not orders: # List of orders was not given so fetch everything from the market - orders = self.get_own_orders + orders = self.own_orders return self.filter_sell_orders(orders) @@ -1356,38 +1390,16 @@ def quote_asset(self): return self.worker['market'].split('/')[0] @property - def all_own_orders(self, refresh=True): + def all_own_orders(self): """ Return the worker's open orders in all markets - - :param bool | refresh: Use most recent data - :return: List of Order objects """ - # Refresh account data - if refresh: - self.account.refresh() - - orders = [] - for order in self.account.openorders: - orders.append(order) - - return orders + return self.get_all_own_orders() @property - def get_own_orders(self): + def own_orders(self): """ Return the account's open orders in the current market - - :return: List of Order objects """ - orders = [] - - # Refresh account data - self.account.refresh() - - for order in self.account.openorders: - if self.worker["market"] == order.market and self.account.openorders: - orders.append(order) - - return orders + return self.get_own_orders() @property def market(self): diff --git a/dexbot/strategies/strategy_template.py b/dexbot/strategies/strategy_template.py index a1701e3e7..ff493cce1 100644 --- a/dexbot/strategies/strategy_template.py +++ b/dexbot/strategies/strategy_template.py @@ -173,7 +173,7 @@ def update_gui_slider(self): return order_ids = None - orders = self.get_own_orders + orders = self.own_orders if orders: order_ids = [order['id'] for order in orders if 'id' in order] From c799f0ce34556b89f88b6aeb1d1b9222383d4538 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 20 Jan 2019 00:51:07 +0500 Subject: [PATCH 1193/1846] Small update of docstring and comment --- dexbot/strategies/base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 2818bb1fc..2c14b1061 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -494,8 +494,8 @@ def count_asset(self, order_ids=None, return_asset=False): def get_allocated_assets(self, order_ids=None, return_asset=False): """ Returns the amount of QUOTE and BASE allocated in orders, and that do not show up in available balance - :param list | order_ids: - :param bool | return_asset: + :param list | order_ids: order ids to analyze + :param bool | return_asset: true if returned values should be Amount instances :return: Dictionary of QUOTE and BASE amounts """ if not order_ids: @@ -510,6 +510,7 @@ def get_allocated_assets(self, order_ids=None, return_asset=False): for order_id in order_ids: order = self.get_updated_order(order_id) + # get_updated_order() may return None, so filter out if not order: continue asset_id = order['base']['asset']['id'] From 422785bbe8672d399c965d817d91f825a25f2e40 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 20 Jan 2019 00:52:26 +0500 Subject: [PATCH 1194/1846] Refactor SO to handle single account with multiple workers This commit brings support for handling multiple workers on single account with assets intersections into Staggered Orders strategy. See #434 for discussion. --- dexbot/strategies/staggered_orders.py | 110 ++++++++++++++++---------- 1 file changed, 68 insertions(+), 42 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 9a6ad9409..05f9f2d1b 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -310,7 +310,7 @@ def maintain_strategy(self, *args, **kwargs): # Maintain the history of free balances after maintenance runs. # Save exactly key values instead of full key because it may be modified later on. - self.refresh_balances(total_balances=False) + self.refresh_balances() self.base_balance_history.append(self.base_balance['amount']) self.quote_balance_history.append(self.quote_balance['amount']) if len(self.base_balance_history) > 3: @@ -436,19 +436,72 @@ def calculate_asset_thresholds(self): self.base_asset_threshold = reserve_ratio * 10 ** -self.market['base']['precision'] self.quote_asset_threshold = self.base_asset_threshold / self.market_center_price - def refresh_balances(self, total_balances=True, use_cached_orders=False): + def refresh_balances(self, use_cached_orders=False): """ This function is used to refresh account balances - :param bool | total_balances: refresh total balance or skip it + :param bool | use_cached_orders: when calculating orders balance, use cached orders from self.cached_orders + + This version supports using same bitshares account across multiple workers with assets intersections. """ + # Balances in orders on all related markets + orders = self.get_all_own_orders(refresh=not use_cached_orders) + order_ids = [order['id'] for order in orders] + orders_balance = self.get_allocated_assets(order_ids) + + # Balances in own orders + own_orders = self.get_own_orders(refresh=False) + order_ids = [order['id'] for order in own_orders] + own_orders_balance = self.get_allocated_assets(order_ids) + + # Get account free balances (not allocated into orders) + account_balances = self.count_asset(order_ids=[], return_asset=True) + + # Calculate full asset balance on account + quote_full_balance = account_balances['quote']['amount'] + orders_balance['quote'] + base_full_balance = account_balances['base']['amount'] + orders_balance['base'] + + # Calculate operational balance for current worker + # Operational balance is a part of the whole account balance which should be designated to this worker + op_quote_balance = quote_full_balance + op_base_balance = base_full_balance + op_percent_quote = self.get_worker_share_for_asset(self.market['quote']['symbol']) + op_percent_base = self.get_worker_share_for_asset(self.market['base']['symbol']) + if op_percent_quote < 1: + op_quote_balance *= op_percent_quote + self.log.debug('Using {:.2%} of QUOTE balance ({:.{prec}f} {})' + .format(op_percent_quote, op_quote_balance, self.market['quote']['symbol'], + prec=self.market['quote']['precision'])) + if op_percent_base < 1: + op_base_balance *= op_percent_base + self.log.debug('Using {:.2%} of BASE balance ({:.{prec}f} {})' + .format(op_percent_base, op_base_balance, self.market['base']['symbol'], + prec=self.market['base']['precision'])) + + # Count balances allocated into virtual orders virtual_orders_base_balance = 0 virtual_orders_quote_balance = 0 + if self.virtual_orders: + # Todo: can we use filtered orders from refresh_orders() here? + buy_orders = self.filter_buy_orders(self.virtual_orders) + sell_orders = self.filter_sell_orders(self.virtual_orders, invert=False) + virtual_orders_base_balance = reduce((lambda x, order: x + order['base']['amount']), buy_orders, 0) + virtual_orders_quote_balance = reduce((lambda x, order: x + order['base']['amount']), sell_orders, 0) - # Get current account balances - account_balances = self.count_asset(order_ids=[], return_asset=True) + # Total balance per asset (orders balance and available balance) + # Total balance should be: max(operational, real_orders + virtual_orders) + # Total balance used when increasing least/closest orders + self.quote_total_balance = max(op_quote_balance, own_orders_balance['quote'] + virtual_orders_quote_balance) + self.base_total_balance = max(op_base_balance, own_orders_balance['base'] + virtual_orders_base_balance) - self.base_balance = account_balances['base'] + # Prepare variables with free balance available to the worker self.quote_balance = account_balances['quote'] + self.base_balance = account_balances['base'] + + # Calc avail balance; avail balances used in maintain_strategy to pass into allocate_asset + # avail = total - real_orders - virtual_orders + self.quote_balance['amount'] = self.quote_total_balance - own_orders_balance['quote'] \ + - virtual_orders_quote_balance + self.base_balance['amount'] = self.base_total_balance - own_orders_balance['base'] - virtual_orders_base_balance # Reserve fees for N orders reserve_num_orders = 200 @@ -456,40 +509,14 @@ def refresh_balances(self, total_balances=True, use_cached_orders=False): # Finally, reserve only required asset if self.fee_asset['id'] == self.market['base']['id']: - self.base_balance['amount'] = self.base_balance['amount'] - fee_reserve + self.base_balance['amount'] -= fee_reserve elif self.fee_asset['id'] == self.market['quote']['id']: - self.quote_balance['amount'] = self.quote_balance['amount'] - fee_reserve - - # Exclude balances allocated into virtual orders - if self.virtual_orders: - buy_orders = self.filter_buy_orders(self.virtual_orders) - sell_orders = self.filter_sell_orders(self.virtual_orders, invert=False) - virtual_orders_base_balance = reduce((lambda x, order: x + order['base']['amount']), buy_orders, 0) - virtual_orders_quote_balance = reduce((lambda x, order: x + order['base']['amount']), sell_orders, 0) - self.base_balance['amount'] -= virtual_orders_base_balance - self.quote_balance['amount'] -= virtual_orders_quote_balance - - if not total_balances: - # Caller doesn't interesting in balances of real orders - return - - # Balance per asset from orders - if use_cached_orders and self.cached_orders: - orders = self.cached_orders - else: - orders = self.get_own_orders - order_ids = [order['id'] for order in orders] - orders_balance = self.get_allocated_assets(order_ids) - - # Total balance per asset (orders balance and available balance) - self.quote_total_balance = orders_balance['quote'] + self.quote_balance['amount'] + virtual_orders_quote_balance - self.base_total_balance = orders_balance['base'] + self.base_balance['amount'] + virtual_orders_base_balance + self.quote_balance['amount'] -= fee_reserve def refresh_orders(self): """ Updates buy and sell orders """ - orders = self.get_own_orders - self.cached_orders = orders + orders = self.get_own_orders() # Sort virtual orders self.virtual_buy_orders = self.filter_buy_orders(self.virtual_orders, sort='DESC') @@ -499,12 +526,9 @@ def refresh_orders(self): self.real_buy_orders = self.filter_buy_orders(orders, sort='DESC') self.real_sell_orders = self.filter_sell_orders(orders, sort='DESC', invert=False) - # Concatenate orders and virtual_orders - orders = orders + self.virtual_orders - - # Sort orders so that order with index 0 is closest to the center price and -1 is furthers - self.buy_orders = self.filter_buy_orders(orders, sort='DESC') - self.sell_orders = self.filter_sell_orders(orders, sort='DESC', invert=False) + # Concatenate real orders and virtual_orders + self.buy_orders = self.real_buy_orders + self.virtual_buy_orders + self.sell_orders = self.real_sell_orders + self.virtual_sell_orders def remove_outside_orders(self, sell_orders, buy_orders): """ Remove orders that exceed boundaries @@ -632,6 +656,8 @@ def store_profit_estimation_data(self, force=False): """ Stores balance history entry if center price moved enough :param bool | force: True = force store data, False = store data only on center price change + + Todo: this method is inaccurate when using single account accross multiple workers """ need_store = False account = self.config['workers'][self.worker_name].get('account') @@ -1312,7 +1338,7 @@ def replace_partially_filled_order(self, order): price = order['price'] ** -1 self.place_market_sell_order(order['base']['amount'], price) if self.returnOrderId: - self.refresh_balances(total_balances=False) + self.refresh_balances() else: needed = order['base']['amount'] - order['for_sale']['amount'] self.log.debug('Unable to replace partially filled {} order: avail/needed: {:.{prec}f}/{:.{prec}f} {}' From 98af8340728a3b5484d93270167593d38dba9ee9 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 23 Feb 2019 14:23:02 +0500 Subject: [PATCH 1195/1846] Allow to use same account in multiple workers --- dexbot/controllers/worker_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index 0d6913af4..6371b1e1d 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -185,6 +185,8 @@ def validate_private_key_type(self, account, private_key): @classmethod def validate_account_not_in_use(cls, account): + """ Check whether account is used by another worker + """ workers = Config().workers_data for worker_name, worker in workers.items(): if worker['account'] == account: @@ -216,8 +218,6 @@ def validate_form(self): private_key = self.view.private_key_input.text() if not self.validate_account_name(account): error_texts.append("Account doesn't exist.") - if not self.validate_account_not_in_use(account): - error_texts.append('Use a different account. "{}" is already in use.'.format(account)) if not self.validate_private_key(account, private_key): error_texts.append('Private key is invalid.') elif private_key and not self.validate_private_key_type(account, private_key): From c4d15a0187c9f1fc06742fb92c4c4975a84bb81b Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 18 Feb 2019 16:31:19 +0500 Subject: [PATCH 1196/1846] Add King Of The Hill strategy Closes: #454 --- dexbot/cli_conf.py | 6 +- dexbot/controllers/strategy_controller.py | 65 +++++ dexbot/controllers/worker_controller.py | 4 + dexbot/strategies/king_of_the_hill.py | 305 ++++++++++++++++++++++ 4 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 dexbot/strategies/king_of_the_hill.py diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index a5014eb98..b044d9e97 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -31,7 +31,11 @@ 'name': 'Relative Orders'}, {'tag': 'stagger', 'class': 'dexbot.strategies.staggered_orders', - 'name': 'Staggered Orders'}] + 'name': 'Staggered Orders'}, + {'tag': 'koth', + 'class': 'dexbot.strategies.king_of_the_hill', + 'name': 'King of the Hill'}, +] tags_so_far = {'stagger', 'relative'} for desc, module in dexbot.helper.find_external_strategies(): diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index f2aef1be9..9bfe08c0d 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -295,3 +295,68 @@ def validation_errors(self): if not self.view.strategy_widget.lower_bound_input.value(): error_texts.append("Lower bound can't be 0") return error_texts + +class KingOfTheHillController(StrategyController): + + def __init__(self, view, configure, worker_controller, worker_data): + self.view = view + self.configure = configure + self.worker_controller = worker_controller + + # Check if there is worker data. This prevents error when multiplying None type when creating worker. + super().__init__(view, configure, worker_controller, worker_data) + + widget = self.view.strategy_widget + + # Event connecting + widget.relative_order_size_input.clicked.connect(self.onchange_relative_order_size_input) + widget.mode_input.currentIndexChanged.connect(self.onchange_mode_input) + + # Trigger the onchange events once + self.onchange_relative_order_size_input(widget.relative_order_size_input.isChecked()) + self.onchange_mode_input(widget.mode_input.currentIndex()) + + def onchange_relative_order_size_input(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.view.strategy_widget.buy_order_amount_input.setSuffix('%') + self.view.strategy_widget.buy_order_amount_input.setDecimals(2) + self.view.strategy_widget.buy_order_amount_input.setMaximum(100.00) + self.view.strategy_widget.buy_order_amount_input.setMinimumWidth(170) + + self.view.strategy_widget.sell_order_amount_input.setSuffix('%') + self.view.strategy_widget.sell_order_amount_input.setDecimals(2) + self.view.strategy_widget.sell_order_amount_input.setMaximum(100.00) + self.view.strategy_widget.sell_order_amount_input.setMinimumWidth(170) + + def order_size_input_to_static(self): + self.view.strategy_widget.buy_order_amount_input.setSuffix('') + self.view.strategy_widget.buy_order_amount_input.setDecimals(8) + self.view.strategy_widget.buy_order_amount_input.setMaximum(1000000000.000000) + + self.view.strategy_widget.sell_order_amount_input.setSuffix('') + self.view.strategy_widget.sell_order_amount_input.setDecimals(8) + self.view.strategy_widget.sell_order_amount_input.setMaximum(1000000000.000000) + + def onchange_mode_input(self, index): + assert index < 3, 'Impossible mode' + + if index == 0: + self.view.strategy_widget.buy_order_amount_input.setDisabled(False) + self.view.strategy_widget.sell_order_amount_input.setDisabled(False) + self.view.strategy_widget.buy_order_size_threshold_input.setDisabled(False) + self.view.strategy_widget.sell_order_size_threshold_input.setDisabled(False) + elif index == 1: + self.view.strategy_widget.buy_order_amount_input.setDisabled(False) + self.view.strategy_widget.sell_order_amount_input.setDisabled(True) + self.view.strategy_widget.buy_order_size_threshold_input.setDisabled(False) + self.view.strategy_widget.sell_order_size_threshold_input.setDisabled(True) + elif index == 2: + self.view.strategy_widget.buy_order_amount_input.setDisabled(True) + self.view.strategy_widget.sell_order_amount_input.setDisabled(False) + self.view.strategy_widget.buy_order_size_threshold_input.setDisabled(True) + self.view.strategy_widget.sell_order_size_threshold_input.setDisabled(False) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index 0d6913af4..ecf61f6a9 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -43,6 +43,10 @@ def strategies(self): 'name': 'Staggered Orders', 'form_module': '' } + strategies['dexbot.strategies.king_of_the_hill'] = { + 'name': 'King of the Hill', + 'form_module': '' + } for desc, module in find_external_strategies(): strategies[module] = {'name': desc, 'form_module': module} # if there is no UI form in the module then GUI will gracefully revert to auto-ui diff --git a/dexbot/strategies/king_of_the_hill.py b/dexbot/strategies/king_of_the_hill.py new file mode 100644 index 000000000..807adc3f9 --- /dev/null +++ b/dexbot/strategies/king_of_the_hill.py @@ -0,0 +1,305 @@ +# Python imports +from datetime import datetime, timedelta + +# Project imports +from dexbot.strategies.base import StrategyBase, ConfigElement + +STRATEGY_NAME = 'King of the Hill' + + +class Strategy(StrategyBase): + """ King of the Hill strategy + + This worker will place a buy or sell order for an asset and update so that the users order stays closest to the + opposing order book. + + Moving forward: If any other orders are placed closer to the opposing order book the worker will cancel the + users order and replace it with one that is the smallest possible increment closer to the opposing order book + than any other orders. + + Moving backward: If the users order is the closest to the opposing order book but a gap opens up on the order + book behind the users order the worker will cancel the order and place it at the smallest possible increment + closer to the opposing order book than any other order. + """ + + @classmethod + def configure(cls, return_base_config=True): + return StrategyBase.configure(return_base_config) + [ + ConfigElement('mode', 'choice', 'both', 'Mode', + 'Operational mode', ([ + ('both', 'Buy + sell'), + ('buy', 'Buy only'), + ('sell', 'Sell only')])), + ConfigElement('lower_bound', 'float', 0, 'Lower bound', + 'Do not place sell orders lower than this bound', + (0, 10000000, 8, '')), + ConfigElement('upper_bound', 'float', 0, 'Upper bound', + 'Do not place buy orders higher than this bound', + (0, 10000000, 8, '')), + ConfigElement('buy_order_amount', 'float', 0, 'Amount (BASE)', + 'Fixed order size for buy orders, expressed in BASE asset, unless "relative order size"' + ' selected', (0, None, 8, '')), + ConfigElement('sell_order_amount', 'float', 0, 'Amount (QUOTE)', + 'Fixed order size for sell orders, expressed in QUOTE asset, unless "relative order size"' + ' selected', (0, None, 8, '')), + ConfigElement('relative_order_size', 'bool', False, 'Relative order size', + 'Amount is expressed as a percentage of the account balance of quote/base asset', None), + ConfigElement('buy_order_size_threshold', 'float', 0, 'Ignore smaller buy orders', + 'Ignore buy orders which are smaller than this threshold (BASE). ' + 'If unset, use own order size as a threshold', (0, None, 8, '')), + ConfigElement('sell_order_size_threshold', 'float', 0, 'Ignore smaller sell orders', + 'Ignore sell orders which are smaller than this threshold (QUOTE). ' + 'If unset, use own order size as a threshold', (0, None, 8, '')), + ConfigElement('min_order_lifetime', 'int', 6, 'Min order lifetime', + 'Minimum order lifetime before order reset, seconds', (1, None, '')) + ] + + @classmethod + def configure_details(cls, include_default_tabs=True): + return StrategyBase.configure_details(include_default_tabs) + [] + + def __init__(self, *args, **kwargs): + # Initializes StrategyBase class + super().__init__(*args, **kwargs) + + self.log.info("Initializing {}...".format(STRATEGY_NAME)) + + # Tick counter + self.counter = 0 + + # Define Callbacks + self.onMarketUpdate += self.maintain_strategy + self.ontick += self.tick + + self.error_ontick = self.error + self.error_onMarketUpdate = self.error + self.error_onAccount = self.error + + # Get view + self.view = kwargs.get('view') + + self.worker_name = kwargs.get('name') + + self.mode = self.worker.get('mode', 'both') + self.buy_order_amount = float(self.worker.get('buy_order_amount', 0)) + self.sell_order_amount = float(self.worker.get('sell_order_amount', 0)) + self.is_relative_order_size = self.worker.get('relative_order_size', False) + self.buy_order_size_threshold = self.worker.get('buy_order_size_threshold', 0) + self.sell_order_size_threshold = self.worker.get('sell_order_size_threshold', 0) + self.upper_bound = self.worker.get('upper_bound', 0) + self.lower_bound = self.worker.get('lower_bound', 0) + self.min_order_lifetime = self.worker.get('min_order_lifetime', 1) + + self.orders = [] + # Current order we must be higher + self.buy_order_to_beat = None + self.sell_order_to_beat = None + # We put an order to be higher than that order + self.beaten_buy_order = None + self.beaten_sell_order = None + # Set last check in the past to get immediate check at startup + self.last_check = datetime(2000, 1, 1) + self.min_check_interval = self.min_order_lifetime + + if self.view: + self.update_gui_slider() + + # Make sure we're starting from scratch as we don't keeping orders in the db + self.cancel_all_orders() + + self.log.info("{} initialized.".format(STRATEGY_NAME)) + + def maintain_strategy(self, *args): + """ Strategy main logic + """ + delta = datetime.now() - self.last_check + # Only allow to check orders whether minimal time passed + if delta < timedelta(seconds=self.min_check_interval): + return + + self.calc_order_prices() + + if self.orders: + self.check_orders() + else: + self.place_orders() + + self.last_check = datetime.now() + + def check_orders(self): + """ Check whether own orders needs intervention + """ + orders_to_delete = [] + for stored_order in self.orders: + order = self.get_order(stored_order['id']) + order_type = self.get_order_type(stored_order) + + if order: + is_partially_filled = self.is_partially_filled(order, threshold=0.8) + if is_partially_filled: + # If own order filled too much, replace it with new order + # TODO: check cancel_orders() return value after #467 fix + self.log.info('Own {} order filled too much, resetting'.format(order_type)) + self.cancel_orders(order) + orders_to_delete.append(stored_order['id']) + self.place_order(order_type) + # Check if someone put order above ours or beaten order was canceled + elif ((order_type == 'buy' and (not self.get_order(self.beaten_buy_order) or + stored_order['price'] < self.buy_price)) or + (order_type == 'sell' and (not self.get_order(self.beaten_sell_order) or + stored_order['price'] ** -1 > self.sell_price))): + self.log.debug('Moving {} order'.format(order_type)) + self.cancel_orders(order) + orders_to_delete.append(stored_order['id']) + self.place_order(order_type) + # Own order is not there + else: + self.log.info('Own {} order filled, placing a new one'.format(order_type)) + orders_to_delete.append(stored_order['id']) + self.place_order(order_type) + + self.orders = [order for order in self.orders if order['id'] not in orders_to_delete] + + def get_order_type(self, order): + """ Determine order type and return it as a string + """ + if self.is_buy_order(order): + return 'buy' + else: + return 'sell' + + def calc_order_prices(self): + """ Calculate prices of our orders + """ + # Obtain orderbook orders excluding our orders + market_orders = self.get_market_orders(depth=100) + own_orders_ids = [order['id'] for order in self.get_own_orders] + market_orders = [order for order in market_orders if order['id'] not in own_orders_ids] + buy_orders = self.filter_buy_orders(market_orders) + sell_orders = self.filter_sell_orders(market_orders, invert=True) + + # xxx_order_size_threshold indicates order price we need to beat + sell_order_size_threshold = self.sell_order_size_threshold + if not sell_order_size_threshold: + sell_order_size_threshold = self.amount_quote + + buy_order_size_threshold = self.buy_order_size_threshold + if not buy_order_size_threshold: + buy_order_size_threshold = self.amount_base + + # Note that we're operating on inverted orders here + for order in sell_orders: + if order['quote']['amount'] > sell_order_size_threshold: + # Calculate price to beat order by slightly decreasing BASE amount + new_base = order['base']['amount'] - 2 * 10 ** -self.market['base']['precision'] + # Don't let to place sell orders lower than lower bound + self.sell_price = max(new_base / order['quote']['amount'], self.lower_bound) + self.sell_order_to_beat = order['id'] + self.log.debug('Sell price to be higher: {:.8f}'.format(self.sell_price)) + break + + for order in buy_orders: + if order['base']['amount'] > buy_order_size_threshold: + new_quote = order['quote']['amount'] - 2 * 10 ** -self.market['quote']['precision'] + # Don't let to place buy orders above upper bound + self.buy_price = min(order['base']['amount'] / new_quote, self.upper_bound) + self.buy_order_to_beat = order['id'] + self.log.debug('Buy price to be higher: {:.8f}'.format(self.buy_price)) + break + + def place_order(self, order_type): + """ Place single order + """ + new_order = None + + if order_type == 'buy': + if not self.buy_price: + self.log.error('Cannot determine buy price, correct your bounds and/or ignore thresholds') + self.disabled = True + return + amount_base = self.amount_base + if not amount_base: + self.log.error('Cannot place {} order with 0 amount. Adjust your settings!'.format(order_type)) + self.disabled = True + return + amount_quote = amount_base / self.buy_price + # Prevent too small amounts + if self.is_too_small_amounts(amount_quote, amount_base): + self.log.error('Amount for {} order is too small'.format(order_type)) + return + new_order = self.place_market_buy_order(amount_quote, self.buy_price) + self.beaten_buy_order = self.buy_order_to_beat + elif order_type == 'sell': + if not self.sell_price: + self.log.error('Cannot determine sell price, correct your bounds and/or ignore thresholds') + self.disabled = True + return + amount_quote = self.amount_quote + if not amount_quote: + self.log.error('Cannot place {} order with 0 amount. Adjust your settings!'.format(order_type)) + self.disabled = True + return + amount_base = amount_quote * self.sell_price + # Prevent too small amounts + if self.is_too_small_amounts(amount_quote, amount_base): + self.log.error('Amount for {} order is too small'.format(order_type)) + return + new_order = self.place_market_sell_order(amount_quote, self.sell_price) + self.beaten_sell_order = self.sell_order_to_beat + + if new_order: + # Store own orders into list to perform checks later + self.orders.append(new_order) + else: + self.log.error('Failed to place {} order'.format(order_type)) + + def place_orders(self): + """ Place new orders + """ + place_buy = False + place_sell = False + + if self.mode == 'both': + place_buy = True + place_sell = True + elif self.mode == 'buy': + place_buy = True + elif self.mode == 'sell': + place_sell = True + + if place_buy: + self.place_order('buy') + if place_sell: + self.place_order('sell') + + @property + def amount_quote(self): + """ Get quote amount, calculate if order size is relative + """ + amount = self.sell_order_amount + if self.is_relative_order_size: + quote_balance = float(self.balance(self.market['quote'])) + amount = quote_balance * (amount / 100) + + return amount + + @property + def amount_base(self): + """ Get base amount, calculate if order size is relative + """ + amount = self.buy_order_amount + if self.is_relative_order_size: + base_balance = float(self.balance(self.market['base'])) + amount = base_balance * (amount / 100) + + return amount + + def error(self, *args, **kwargs): + """ Defines what happens when error occurs """ + self.disabled = True + + def tick(self, d): + """ Ticks come in on every block """ + if not (self.counter or 0) % 4: + self.maintain_strategy() + self.counter += 1 From 04ed9590ec9585426f71a247b02d44351f0135f0 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 20 Feb 2019 15:42:05 +0500 Subject: [PATCH 1197/1846] Fix .invert() consistency in place_market_sell_order() When returning an order, invert() must be called not only when deleted order is reconstructed, but in normal case too. Don't invert by default as it may break backward compatibility. --- dexbot/strategies/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index c44e0a843..166d5a05d 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1112,12 +1112,13 @@ def place_market_buy_order(self, amount, price, return_none=False, *args, **kwar else: return True - def place_market_sell_order(self, amount, price, return_none=False, *args, **kwargs): + def place_market_sell_order(self, amount, price, return_none=False, invert=False, *args, **kwargs): """ Places a sell order in the market :param float | amount: Order amount in QUOTE :param float | price: Order price in BASE :param bool | return_none: + :param bool | invert: True = return inverted sell order :param args: :param kwargs: :return: @@ -1161,8 +1162,9 @@ def place_market_sell_order(self, amount, price, return_none=False, *args, **kwa if sell_order and 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 + if sell_order and invert: + sell_order.invert() return sell_order else: return True From 4871eccda4efa8355d7dd184cd3b8ef45e1e0af7 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 23 Feb 2019 15:46:09 +0500 Subject: [PATCH 1198/1846] Auto-uncheck reset_on_price_change Uncheck reset_on_price_change when center_price_dynamic set to False. This prevents setting of bad options combination, thus avoiding further error. --- dexbot/controllers/strategy_controller.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index f2aef1be9..7ea6e614a 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -181,7 +181,11 @@ def onchange_center_price_dynamic_input(self, checked): else: self.view.strategy_widget.center_price_input.setDisabled(False) self.view.strategy_widget.center_price_depth_input.setDisabled(True) + + # Disable and uncheck reset_on_price_change self.view.strategy_widget.reset_on_price_change_input.setDisabled(True) + self.view.strategy_widget.reset_on_price_change_input.setChecked(False) + self.view.strategy_widget.price_change_threshold_input.setDisabled(True) self.view.strategy_widget.external_feed_input.setChecked(False) self.view.strategy_widget.external_feed_input.setDisabled(True) From 97de1b6f964d1f8dfbda31041440c17c522c2b8f Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 4 Feb 2019 15:27:53 +0500 Subject: [PATCH 1199/1846] Introduce dust orders handling If order was filled partially it may stay as a dust order. Instead of waiting until it will be fully filled, just cancel it and use obtained funds to place closer order on opposite side. Closes: #432 --- dexbot/strategies/staggered_orders.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 9a6ad9409..be94ebee1 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -757,6 +757,11 @@ def allocate_asset(self, asset, asset_balance): .format(order_type, self.actual_spread, self.target_spread + self.increment)) if self.bootstrapping: self.place_closer_order(asset, closest_own_order) + elif opposite_orders and self.actual_spread - self.increment < self.target_spread + self.increment: + """ Place max-sized closer order if only one order needed to reach target spread (avoid unneeded + increases) + """ + self.place_closer_order(asset, closest_own_order, allow_partial=True) elif opposite_orders: # Place order limited by size of the opposite-side order if self.mode == 'mountain': @@ -860,6 +865,15 @@ def allocate_asset(self, asset, asset_balance): # Refresh balances to make "reserved" funds available self.refresh_balances(use_cached_orders=True) self.replace_partially_filled_order(closest_own_order) + elif (increase_finished and not self.check_partial_fill(closest_opposite_order, fill_threshold=( + 1 - self.partial_fill_threshold)) and self.bitshares.txbuffer.is_empty()): + # Dust order on opposite side, cancel dust order and place closer order + # Require empty txbuffer to avoid rare condition when order may be already canceled from + # replace_partially_filled_order() call + self.log.info('Cancelling dust order at opposite side, placing closer {} order'.format(order_type)) + self.cancel_orders_wrapper(closest_opposite_order) + self.refresh_balances(use_cached_orders=True) + self.place_closer_order(asset, closest_own_order, allow_partial=True) else: # Place first buy order as close to the lower bound as possible self.bootstrapping = True From 3e28d546f84ee5b7114508b596f71777abfdc213 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 25 Feb 2019 15:40:00 +0500 Subject: [PATCH 1200/1846] Fix exception logging Closes: #497 --- dexbot/strategies/staggered_orders.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 9a6ad9409..40dfad18f 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1,7 +1,6 @@ import sys import time import math -import traceback import bitsharesapi.exceptions from datetime import datetime, timedelta from functools import reduce @@ -302,9 +301,7 @@ def maintain_strategy(self, *args, **kwargs): """ Handle exception without stopping the worker. The goal is to handle race condition when partially filled order was further filled before we actually replaced them. """ - self.log.warning('Got exception during broadcasting trx:') - traceback.print_exc(file=sys.stdout) - self.log.warning('Ignoring that exception and continue') + self.log.exception('Got exception during broadcasting trx:') return self.bitshares.bundle = False From 64c4be5c57c9429c0fb1e50f207bb0ca0db49f38 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 25 Feb 2019 15:58:01 +0500 Subject: [PATCH 1201/1846] Cancel only own market orders in cancel_all_orders() When using single account for multiple workers, add/remove of a worker must not touch other workers orders. --- dexbot/strategies/base.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 2c14b1061..94c92fd44 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -425,13 +425,23 @@ def calculate_worker_value(self, unit_of_measure): # Fixme: Make sure that decimal precision is correct. return base_total + quote_total - def cancel_all_orders(self): - """ Cancel all orders of the worker's account + def cancel_all_orders(self, all_markets=False): + """ Cancel all orders of the worker's market or all markets + + :param bool | all_markets: True = cancel orders on all markets """ - self.log.info('Canceling all orders') + orders_to_cancel = [] + + if all_markets: + self.log.info('Canceling all account orders') + orders_to_cancel = self.all_own_orders + else: + self.log.info('Canceling all orders on market {}/{}' + .format(self.market['quote']['symbol'], self.market['base']['symbol'])) + orders_to_cancel = self.own_orders - if self.all_own_orders: - self.cancel_orders(self.all_own_orders) + if orders_to_cancel: + self.cancel_orders(orders_to_cancel) self.log.info("Orders canceled") From 9610da5b18601009e6abb3ed77361df5dcc0d72e Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 27 Feb 2019 15:18:28 +0500 Subject: [PATCH 1202/1846] Prevent exiting from list lenght bound in whiptail menu If allowed input is in range 1-5 and user entered 6 or more, treat this as 5. Closes: #445 --- dexbot/whiptail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/whiptail.py b/dexbot/whiptail.py index 181b15b28..eac2f51ee 100644 --- a/dexbot/whiptail.py +++ b/dexbot/whiptail.py @@ -144,8 +144,8 @@ def menu(self, msg='', items=(), default=0): i += 1 click.echo("\n") ret = click.prompt("Your choice:", type=int, default=default + 1) - ret = items[ret - 1] - return ret[0] + element_number = min(ret - 1, len(items) - 1) + return items[element_number][0] def radiolist(self, msg='', items=()): d = 0 From ff7d0b0e8477fd69a7eb766cb74a16db7a8116da Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 28 Feb 2019 18:19:08 -0800 Subject: [PATCH 1203/1846] add wif --- dexbot/strategies/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index c44e0a843..a53a9b6b1 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -153,6 +153,9 @@ def configure(cls, return_base_config=True): ConfigElement('account', 'string', '', 'Account', 'BitShares account name for the bot to operate with', ''), + ConfigElement('wif', 'string', '', 'WIF Key', + 'BitShares WIF Key for the bot to operate with', + ''), ConfigElement('market', 'string', 'USD:BTS', 'Market', 'BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"', r'[A-Z\.]+[:\/][A-Z\.]+'), @@ -240,6 +243,8 @@ def __init__(self, # Get Bitshares account and market for this worker self._account = Account(self.worker["account"], full=True, bitshares_instance=self.bitshares) + self._wifkey = self.worker.get('wif') + self._market = Market(config["workers"][name]["market"], bitshares_instance=self.bitshares) # Recheck flag - Tell the strategy to check for updated orders From 1e8ed0094477102d26448a530a300b4ab6660cec Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 28 Feb 2019 19:03:32 -0800 Subject: [PATCH 1204/1846] change default setting for ext feed to gecko, to prevent index error --- dexbot/strategies/relative_orders.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 15628d565..8863fa279 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -12,10 +12,10 @@ class Strategy(StrategyBase): @classmethod def configure(cls, return_base_config=True): return StrategyBase.configure(return_base_config) + [ - ConfigElement('external_price_source', 'choice', EXCHANGES[0], 'External price source', - 'The bot will try to get price information from this source', EXCHANGES), ConfigElement('external_feed', 'bool', False, 'External price feed', 'Use external reference price instead of center price acquired from the market', None), + ConfigElement('external_price_source', 'choice', EXCHANGES[0], 'External price source', + 'The bot will try to get price information from this source', EXCHANGES), ConfigElement('amount', 'float', 1, 'Amount', 'Fixed order size, expressed in quote asset, unless "relative order size" selected', (0, None, 8, '')), @@ -87,7 +87,7 @@ def __init__(self, *args, **kwargs): # Set external price source, defaults to False if not found self.external_feed = self.worker.get('external_feed', False) - self.external_price_source = self.worker.get('external_price_source', None) + self.external_price_source = self.worker.get('external_price_source', 'gecko') if self.external_feed: # Get external center price from given source From 9cfbd6c4300b2228135d3fce73833018ede091a6 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 1 Mar 2019 08:36:37 +0200 Subject: [PATCH 1205/1846] Change dexbot version number to 0.9.18 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 1eb8b21c7..d206a10a2 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.17' +VERSION = '0.9.18' AUTHOR = 'Codaone Oy' __version__ = VERSION From ed4bb4207edc0dd906b90848baa11410b8e4bdbb Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 1 Mar 2019 10:01:39 +0200 Subject: [PATCH 1206/1846] Change dexbot version number to 0.9.19 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index d206a10a2..a3d8ed568 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.18' +VERSION = '0.9.19' AUTHOR = 'Codaone Oy' __version__ = VERSION From bcf3d46a5a622f4f83eb9265e76f741db36e565d Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 2 Mar 2019 00:18:09 +0500 Subject: [PATCH 1207/1846] Make tags_so_far from STRATEGIES No sense to fill tags_so_far list manually while it may be populated automatically. --- dexbot/cli_conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index b044d9e97..714f5873e 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -37,7 +37,8 @@ 'name': 'King of the Hill'}, ] -tags_so_far = {'stagger', 'relative'} +# Todo: tags must be unique. Are they really a tags? +tags_so_far = [strategy['tag'] for strategy in STRATEGIES] for desc, module in dexbot.helper.find_external_strategies(): tag = desc.split()[0].lower() # make sure tag is unique From b94dcc2e9d4315b6acedb34fb97cf2a4acf3d991 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Sat, 2 Mar 2019 23:40:36 -0800 Subject: [PATCH 1208/1846] incorrect password; catch exception --- dexbot/ui.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dexbot/ui.py b/dexbot/ui.py index 9c0018500..3b017cd30 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -103,7 +103,11 @@ def new_func(ctx, *args, **kwargs): sys.exit(78) # 'configuration error' in sysexits.h pwd = click.prompt( "Current Wallet Passphrase", hide_input=True) - ctx.bitshares.wallet.unlock(pwd) + try: + ctx.bitshares.wallet.unlock(pwd) + except Exception as exception: + log.critical("Password error, exiting") + sys.exit(78) else: if systemd: # No user available to interact with @@ -114,7 +118,7 @@ def new_func(ctx, *args, **kwargs): "Wallet Encryption Passphrase", hide_input=True, confirmation_prompt=True) - ctx.bitshares.wallet.create(pwd) + ctx.bitshares.wallet.create(pwd) return ctx.invoke(f, *args, **kwargs) return update_wrapper(new_func, f) From cb21db31b452a06a67982f0485b238bac5d650e9 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Mon, 4 Mar 2019 12:48:41 -0800 Subject: [PATCH 1209/1846] add bitshares private key option to cli menu --- dexbot/cli_conf.py | 73 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index a5014eb98..a9c862ce4 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -25,6 +25,11 @@ from dexbot.strategies.base import StrategyBase import dexbot.helper +from bitshares.account import Account +from bitshares.exceptions import KeyAlreadyInStoreException +from bitsharesbase.account import PrivateKey + + STRATEGIES = [ {'tag': 'relative', 'class': 'dexbot.strategies.relative_orders', @@ -88,6 +93,7 @@ def process_config_element(elem, whiptail, config): txt = whiptail.prompt( title, config.get( elem.key, elem.default)) + print("string ", elem.key, txt, sep=":") ## debug statement config[elem.key] = txt if elem.type == "bool": @@ -241,6 +247,51 @@ def configure_worker(whiptail, worker_config): return worker_config + +def add_private_key(bitshares, private_key): + wallet = bitshares.wallet + try: + wallet.addPrivateKey(private_key) + except KeyAlreadyInStoreException: + # Private key already added + pass + + +def validate_account_name(bitshares, account): + if not account: + return False + try: + Account(account, bitshares_instance=bitshares) + return True + except bitshares.exceptions.AccountDoesNotExistsException: + return False + + +def validate_private_key(bitshares, account, private_key): + wallet = bitshares.wallet + if not private_key: + # Check if the private key is already in the database + accounts = wallet.getAccounts() + if any(account == d['name'] for d in accounts): + return True + return False + + try: + pubkey = format(PrivateKey(private_key).pubkey,bitshares.prefix) + except ValueError: + return False + + accounts = wallet.getAllAccounts(pubkey) + account_names = [account['name'] for account in accounts] + print("Account names found in wallet:", account_names, sep=':') + + if account in account_names: + return True + else: + return False + + + def configure_dexbot(config, ctx): whiptail = get_whiptail('DEXBot configure') workers = config.get('workers', {}) @@ -258,7 +309,8 @@ def configure_dexbot(config, ctx): [('NEW', 'Create a new worker'), ('DEL', 'Delete a worker'), ('EDIT', 'Edit a worker'), - ('CONF', 'Redo general config')]) + ('ADD', 'Add a bitshares account'), + ('NODES', 'Redo node config')]) if action == 'EDIT': worker_name = whiptail.menu("Select worker to edit", [(index, index) for index in workers]) @@ -275,7 +327,24 @@ def configure_dexbot(config, ctx): elif action == 'NEW': txt = whiptail.prompt("Your name for the new worker") config['workers'][txt] = configure_worker(whiptail, {}) - elif action == 'CONF': + elif action == 'ADD': + account = whiptail.prompt("Your Account Name") + private_key = whiptail.prompt("Your Private Key") + print(account, private_key, sep=":") + + if not validate_account_name(bitshares_instance, account): + print("account name does not exist") + else: + print("account name is valid") + + if validate_private_key(bitshares_instance, account, private_key): + print("Private key and account are valid") + add_private_key(private_key) + print("adding private key") + else: + print("private key and account do not match") + + elif action == 'NODES': choice = whiptail.node_radiolist( msg="Choose node", items=select_choice(config['node'][0], [(index, index) for index in config['node']]) From dae09799a865703d2eba87ed42b95224e1936e72 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Mon, 4 Mar 2019 21:00:52 -0800 Subject: [PATCH 1210/1846] add menu items for add node, select workers --- dexbot/cli_conf.py | 52 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index a9c862ce4..f104fe870 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -247,7 +247,6 @@ def configure_worker(whiptail, worker_config): return worker_config - def add_private_key(bitshares, private_key): wallet = bitshares.wallet try: @@ -274,7 +273,7 @@ def validate_private_key(bitshares, account, private_key): accounts = wallet.getAccounts() if any(account == d['name'] for d in accounts): return True - return False + return False try: pubkey = format(PrivateKey(private_key).pubkey,bitshares.prefix) @@ -284,13 +283,21 @@ def validate_private_key(bitshares, account, private_key): accounts = wallet.getAllAccounts(pubkey) account_names = [account['name'] for account in accounts] print("Account names found in wallet:", account_names, sep=':') - + if account in account_names: return True else: return False +def validate_private_key_type(self, account, private_key): + account = Account(account) + pubkey = format(PrivateKey(private_key).pubkey, self.bitshares.prefix) + key_type = self.bitshares.wallet.getKeyType(account, pubkey) + if key_type != 'active' and key_type != 'owner': + return False + return True + def configure_dexbot(config, ctx): whiptail = get_whiptail('DEXBot configure') @@ -307,12 +314,24 @@ def configure_dexbot(config, ctx): action = whiptail.menu( "You have an existing configuration.\nSelect an action:", [('NEW', 'Create a new worker'), + ('LIST', 'Select your active workers'), ('DEL', 'Delete a worker'), ('EDIT', 'Edit a worker'), ('ADD', 'Add a bitshares account'), - ('NODES', 'Redo node config')]) - - if action == 'EDIT': + ('NODES', 'Edit Node Selection'), + ('ADD_NODE', 'Add Node')]) + + if action =='LIST': + active_workers = [index for index in workers] +# active_workers = [(index, index) for index in workers] + print(active_workers) + + my_choices = whiptail.checklist('Select Active Workers', + items=active_workers, + prefix=' - ') + print(my_choices) + + elif action == 'EDIT': worker_name = whiptail.menu("Select worker to edit", [(index, index) for index in workers]) config['workers'][worker_name] = configure_worker(whiptail, config['workers'][worker_name]) @@ -329,21 +348,17 @@ def configure_dexbot(config, ctx): config['workers'][txt] = configure_worker(whiptail, {}) elif action == 'ADD': account = whiptail.prompt("Your Account Name") - private_key = whiptail.prompt("Your Private Key") + private_key = whiptail.prompt("Your Private Key", password=True) print(account, private_key, sep=":") if not validate_account_name(bitshares_instance, account): - print("account name does not exist") - else: - print("account name is valid") - + whiptail.alert("Account name does not exist.") if validate_private_key(bitshares_instance, account, private_key): - print("Private key and account are valid") - add_private_key(private_key) - print("adding private key") + whiptail.alert("Private key and account are valid, Adding private Key") + add_private_key(bitshares_instance, private_key) else: - print("private key and account do not match") - + whiptail.alert("Private key and account do not match.") + elif action == 'NODES': choice = whiptail.node_radiolist( msg="Choose node", @@ -353,6 +368,11 @@ def configure_dexbot(config, ctx): config['node'].remove(choice) config['node'].insert(0, choice) + elif action == 'ADD_NODE': + txt = whiptail.prompt("Your name for the new node: e.g. wss://dexnode.net/ws") + config['node'][0] = txt + ## overrides the top position + setup_systemd(whiptail, config) whiptail.clear() return config From db92e9b6a356a792aaa527ca9ca2194a66686ad3 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 5 Mar 2019 14:14:13 -0800 Subject: [PATCH 1211/1846] add loop and exit option to menu --- dexbot/cli_conf.py | 121 ++++++++++++++++++++++----------------------- 1 file changed, 59 insertions(+), 62 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index f104fe870..94db43a4e 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -311,68 +311,65 @@ def configure_dexbot(config, ctx): setup_systemd(whiptail, config) else: bitshares_instance = ctx.bitshares - action = whiptail.menu( - "You have an existing configuration.\nSelect an action:", - [('NEW', 'Create a new worker'), - ('LIST', 'Select your active workers'), - ('DEL', 'Delete a worker'), - ('EDIT', 'Edit a worker'), - ('ADD', 'Add a bitshares account'), - ('NODES', 'Edit Node Selection'), - ('ADD_NODE', 'Add Node')]) - - if action =='LIST': - active_workers = [index for index in workers] -# active_workers = [(index, index) for index in workers] - print(active_workers) - - my_choices = whiptail.checklist('Select Active Workers', - items=active_workers, - prefix=' - ') - print(my_choices) - - elif action == 'EDIT': - worker_name = whiptail.menu("Select worker to edit", [(index, index) for index in workers]) - config['workers'][worker_name] = configure_worker(whiptail, config['workers'][worker_name]) - - strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) - strategy.clear_all_worker_data() - elif action == 'DEL': - worker_name = whiptail.menu("Select worker to delete", [(index, index) for index in workers]) - del config['workers'][worker_name] - - strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) - strategy.clear_all_worker_data() - elif action == 'NEW': - txt = whiptail.prompt("Your name for the new worker") - config['workers'][txt] = configure_worker(whiptail, {}) - elif action == 'ADD': - account = whiptail.prompt("Your Account Name") - private_key = whiptail.prompt("Your Private Key", password=True) - print(account, private_key, sep=":") - - if not validate_account_name(bitshares_instance, account): - whiptail.alert("Account name does not exist.") - if validate_private_key(bitshares_instance, account, private_key): - whiptail.alert("Private key and account are valid, Adding private Key") - add_private_key(bitshares_instance, private_key) - else: - whiptail.alert("Private key and account do not match.") - - elif action == 'NODES': - choice = whiptail.node_radiolist( - msg="Choose node", - items=select_choice(config['node'][0], [(index, index) for index in config['node']]) - ) - # Move selected node as first item in the config file's node list - config['node'].remove(choice) - config['node'].insert(0, choice) - - elif action == 'ADD_NODE': - txt = whiptail.prompt("Your name for the new node: e.g. wss://dexnode.net/ws") - config['node'][0] = txt - ## overrides the top position + + while True: + action = whiptail.menu( + "You have an existing configuration.\nSelect an action:", + [('NEW', 'Create a new worker'), + ('LIST', 'List your workers'), + ('DEL', 'Delete a worker'), + ('EDIT', 'Edit a worker'), + ('ADD', 'Add a bitshares account'), + ('SHOW', 'Show bitshares accounts'), + ('NODES', 'Edit Node Selection'), + ('ADD_NODE', 'Add Your Node'), + ('EXIT', 'Quit this application')]) + + if action == 'EXIT': + break + elif action =='LIST': + my_list = whiptail.menu("List of Your Workers", [(index, index) for index in workers]) + elif action == 'EDIT': + worker_name = whiptail.menu("Select worker to edit", [(index, index) for index in workers]) + config['workers'][worker_name] = configure_worker(whiptail, config['workers'][worker_name]) + strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) + strategy.clear_all_worker_data() + elif action == 'DEL': + worker_name = whiptail.menu("Select worker to delete", [(index, index) for index in workers]) + del config['workers'][worker_name] + strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) + strategy.clear_all_worker_data() + elif action == 'NEW': + txt = whiptail.prompt("Your name for the new worker") + config['workers'][txt] = configure_worker(whiptail, {}) + elif action == 'ADD': + account = whiptail.prompt("Your Account Name") + private_key = whiptail.prompt("Your Private Key", password=True) + if not validate_account_name(bitshares_instance, account): + whiptail.alert("Account name does not exist.") + if validate_private_key(bitshares_instance, account, private_key): + whiptail.alert("Private key and account are valid, Adding private Key") + add_private_key(bitshares_instance, private_key) + else: + whiptail.alert("Private key and account do not match.") + elif action =='SHOW': + wallet = bitshares_instance.wallet + accounts = wallet.getAccounts() + account_list = [(i['name'], i['type']) for i in accounts] + action = whiptail.menu("Bitshares Account List (Name - Type)", account_list) + elif action == 'ADD_NODE': + txt = whiptail.prompt("Your name for the new node: e.g. wss://dexnode.net/ws") + config['node'][0] = txt + ## overrides the top position + elif action == 'NODES': + choice = whiptail.node_radiolist( + msg="Choose node", + items=select_choice(config['node'][0], + [(index, index) for index in config['node']])) + # Move selected node as first item in the config file's node list + config['node'].remove(choice) + config['node'].insert(0, choice) + setup_systemd(whiptail, config) - setup_systemd(whiptail, config) whiptail.clear() return config From 4eb85887c38d25dc37adecdc468adb32f7977aff Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 5 Mar 2019 14:42:10 -0800 Subject: [PATCH 1212/1846] add comment about exit --- dexbot/cli_conf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 94db43a4e..7e263fa8f 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -326,6 +326,8 @@ def configure_dexbot(config, ctx): ('EXIT', 'Quit this application')]) if action == 'EXIT': + ## cancel will also exit the application. but this is a clearer label + ## todo: modify cancel to be "Quit" or "Exit" break elif action =='LIST': my_list = whiptail.menu("List of Your Workers", [(index, index) for index in workers]) From 24ad29023c4ee57342159e17e295878b1951d9b2 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 5 Mar 2019 15:07:10 -0800 Subject: [PATCH 1213/1846] remove debug statements, delete wif from base.py --- dexbot/cli_conf.py | 3 +-- dexbot/strategies/base.py | 4 ---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 7e263fa8f..9a9bcaf66 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -93,7 +93,7 @@ def process_config_element(elem, whiptail, config): txt = whiptail.prompt( title, config.get( elem.key, elem.default)) - print("string ", elem.key, txt, sep=":") ## debug statement +# print("string ", elem.key, txt, sep=":") ## debug statement config[elem.key] = txt if elem.type == "bool": @@ -282,7 +282,6 @@ def validate_private_key(bitshares, account, private_key): accounts = wallet.getAllAccounts(pubkey) account_names = [account['name'] for account in accounts] - print("Account names found in wallet:", account_names, sep=':') if account in account_names: return True diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index a53a9b6b1..a349b3c05 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -153,9 +153,6 @@ def configure(cls, return_base_config=True): ConfigElement('account', 'string', '', 'Account', 'BitShares account name for the bot to operate with', ''), - ConfigElement('wif', 'string', '', 'WIF Key', - 'BitShares WIF Key for the bot to operate with', - ''), ConfigElement('market', 'string', 'USD:BTS', 'Market', 'BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"', r'[A-Z\.]+[:\/][A-Z\.]+'), @@ -243,7 +240,6 @@ def __init__(self, # Get Bitshares account and market for this worker self._account = Account(self.worker["account"], full=True, bitshares_instance=self.bitshares) - self._wifkey = self.worker.get('wif') self._market = Market(config["workers"][name]["market"], bitshares_instance=self.bitshares) From 043e85dfd5d1e9e3c1792c9e1f1205fbc2ae98a2 Mon Sep 17 00:00:00 2001 From: octomatic <39811582+thehapax@users.noreply.github.com> Date: Wed, 6 Mar 2019 15:09:55 -0800 Subject: [PATCH 1214/1846] issue 470 market setting (#478) * edit market offered to end user as QUOTE/BASE instead of vice versa * edit default value to use / as separator * switch pair order * allow numerics in symbols --- dexbot/strategies/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index f8951d029..7a8fa6784 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -153,8 +153,8 @@ def configure(cls, return_base_config=True): ConfigElement('account', 'string', '', 'Account', 'BitShares account name for the bot to operate with', ''), - ConfigElement('market', 'string', 'USD:BTS', 'Market', - 'BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"', + ConfigElement('market', 'string', 'BTS/USD', 'Market', + 'BitShares market to operate on, in the format QUOTE/BASE, for example \"BTS/USD\"', r'[A-Z0-9\.]+[:\/][A-Z0-9\.]+'), ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset', 'Asset to be used to pay transaction fees', From ffe61f94a486f8904394db5fc57d9052af91b2bb Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Wed, 6 Mar 2019 22:21:41 -0800 Subject: [PATCH 1215/1846] add help menu item --- dexbot/cli_conf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 9a9bcaf66..85a94c730 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -322,6 +322,7 @@ def configure_dexbot(config, ctx): ('SHOW', 'Show bitshares accounts'), ('NODES', 'Edit Node Selection'), ('ADD_NODE', 'Add Your Node'), + ('HELP', 'Where to get help'), ('EXIT', 'Quit this application')]) if action == 'EXIT': @@ -330,6 +331,7 @@ def configure_dexbot(config, ctx): break elif action =='LIST': my_list = whiptail.menu("List of Your Workers", [(index, index) for index in workers]) + elif action == 'EDIT': worker_name = whiptail.menu("Select worker to edit", [(index, index) for index in workers]) config['workers'][worker_name] = configure_worker(whiptail, config['workers'][worker_name]) @@ -371,6 +373,8 @@ def configure_dexbot(config, ctx): config['node'].remove(choice) config['node'].insert(0, choice) setup_systemd(whiptail, config) - + elif action == 'HELP': + whiptail.alert("Please see https://github.com/Codaone/DEXBot/wiki") + whiptail.clear() return config From 0d3a008a7fdefe2fc680cbce75ed84a0f27c5b24 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Wed, 6 Mar 2019 22:48:54 -0800 Subject: [PATCH 1216/1846] check for zero case for workers and accounts --- dexbot/cli_conf.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 85a94c730..1ac1b1933 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -93,7 +93,6 @@ def process_config_element(elem, whiptail, config): txt = whiptail.prompt( title, config.get( elem.key, elem.default)) -# print("string ", elem.key, txt, sep=":") ## debug statement config[elem.key] = txt if elem.type == "bool": @@ -325,12 +324,14 @@ def configure_dexbot(config, ctx): ('HELP', 'Where to get help'), ('EXIT', 'Quit this application')]) + my_workers = [(index, index) for index in workers]) + if action == 'EXIT': ## cancel will also exit the application. but this is a clearer label ## todo: modify cancel to be "Quit" or "Exit" break - elif action =='LIST': - my_list = whiptail.menu("List of Your Workers", [(index, index) for index in workers]) + elif action =='LIST': + my_list = whiptail.menu("List of Your Workers", my_workers) elif action == 'EDIT': worker_name = whiptail.menu("Select worker to edit", [(index, index) for index in workers]) @@ -359,7 +360,10 @@ def configure_dexbot(config, ctx): wallet = bitshares_instance.wallet accounts = wallet.getAccounts() account_list = [(i['name'], i['type']) for i in accounts] + if len(account_list) == 0: + account_list = [('none', 'none')] action = whiptail.menu("Bitshares Account List (Name - Type)", account_list) + elif action == 'ADD_NODE': txt = whiptail.prompt("Your name for the new node: e.g. wss://dexnode.net/ws") config['node'][0] = txt From cc6dd672f377b7647fbbb3d9cef502966113c4ea Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Wed, 6 Mar 2019 23:17:02 -0800 Subject: [PATCH 1217/1846] pull workers array out from switch statements --- dexbot/cli_conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 1ac1b1933..3de294150 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -324,7 +324,7 @@ def configure_dexbot(config, ctx): ('HELP', 'Where to get help'), ('EXIT', 'Quit this application')]) - my_workers = [(index, index) for index in workers]) + my_workers = [(index, index) for index in workers] if action == 'EXIT': ## cancel will also exit the application. but this is a clearer label @@ -344,7 +344,7 @@ def configure_dexbot(config, ctx): strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) strategy.clear_all_worker_data() elif action == 'NEW': - txt = whiptail.prompt("Your name for the new worker") + txt = whiptail.prompt("Your name for the new worker. Don't forget to add your bitshares key (main menu)") config['workers'][txt] = configure_worker(whiptail, {}) elif action == 'ADD': account = whiptail.prompt("Your Account Name") From 311c51a6a7e00b423807799122bb896bed42ebcf Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 8 Mar 2019 18:12:56 +0500 Subject: [PATCH 1218/1846] Change saved virtual orders purge logic Purge saved virtual orders only if no buy AND sell orders. If we have both buy and sell real orders, restore both. If we have only one type of orders, restore corresponding virtual orders and purge opposite orders. --- dexbot/strategies/staggered_orders.py | 82 +++++++++++++++++++-------- 1 file changed, 58 insertions(+), 24 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 12b277ddc..290086ac7 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -545,34 +545,68 @@ def remove_outside_orders(self, sell_orders, buy_orders): def restore_virtual_orders(self): """ Create virtual further orders in batch manner. This helps to place further orders quickly on startup. + + If we have both buy and sell real orders, restore both. If we have only one type of orders, restore + corresponding virtual orders and purge opposite orders. """ - if self.buy_orders and self.sell_orders: - # Load orders from the database - orders = self.fetch_orders() - if orders: - self.log.info('Loading virtual orders from database') - for k, v in orders.items(): - self.virtual_orders.append(VirtualOrder(v)) - else: - if self.buy_orders: - furthest_order = self.real_buy_orders[-1] - while furthest_order['price'] > self.lower_bound * (1 + self.increment): - furthest_order = self.place_further_order('base', furthest_order, virtual=True) - if not isinstance(furthest_order, VirtualOrder): - # Failed to place order - break - - if self.sell_orders: - furthest_order = self.real_sell_orders[-1] - while furthest_order['price'] ** -1 < self.upper_bound / (1 + self.increment): - furthest_order = self.place_further_order('quote', furthest_order, virtual=True) - if not isinstance(furthest_order, VirtualOrder): - # Failed to place order - break - else: + def place_further_buy_orders(): + furthest_order = self.real_buy_orders[-1] + while furthest_order['price'] > self.lower_bound * (1 + self.increment): + furthest_order = self.place_further_order('base', furthest_order, virtual=True) + if not isinstance(furthest_order, VirtualOrder): + # Failed to place order + break + + def place_further_sell_orders(): + furthest_order = self.real_sell_orders[-1] + while furthest_order['price'] ** -1 < self.upper_bound / (1 + self.increment): + furthest_order = self.place_further_order('quote', furthest_order, virtual=True) + if not isinstance(furthest_order, VirtualOrder): + # Failed to place order + break + + # Load orders from the database. fetch_orders() return dict, we're transforming it into list + stored_orders = self.fetch_orders().values() + stored_buy_orders = self.filter_buy_orders(stored_orders) + stored_sell_orders = self.filter_sell_orders(stored_orders, invert=False) + + if not self.buy_orders and not self.sell_orders: # No real orders, assume we need to bootstrap, purge old orders self.log.info('No real orders, purging old virtual orders') self.clear_orders() + elif self.buy_orders and self.sell_orders: + if stored_orders: + self.log.info('Loading virtual orders from database') + for order in stored_orders: + self.virtual_orders.append(VirtualOrder(order)) + else: + place_further_buy_orders() + place_further_sell_orders() + elif self.buy_orders and not self.sell_orders: + # Only buy orders, purge stored sell orders + if stored_sell_orders: + self.log.info('Purging virtual sell orders because of no real sell orders') + for order in stored_sell_orders: + self.remove_order(order) + + if stored_buy_orders: + self.log.info('Loading virtual buy orders from database') + for order in stored_buy_orders: + self.virtual_orders.append(VirtualOrder(order)) + else: + place_further_buy_orders() + elif not self.buy_orders and self.sell_orders: + if stored_buy_orders: + self.log.info('Purging virtual buy orders because of no real buy orders') + for order in stored_buy_orders: + self.remove_order(order) + + if stored_sell_orders: + self.log.info('Loading virtual sell orders from database') + for order in stored_sell_orders: + self.virtual_orders.append(VirtualOrder(order)) + else: + place_further_sell_orders() # Set "restored" flag anyway to not break initial bootstrap self.virtual_orders_restored = True From 3db709acb5eb84def4e14a68415ba4de2164519d Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 8 Mar 2019 20:43:06 +0500 Subject: [PATCH 1219/1846] Prevent AttributeError when getting stored orders AttributeError: 'NoneType' object has no attribute 'values' --- 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 290086ac7..ee1f5bdc3 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -566,7 +566,8 @@ def place_further_sell_orders(): break # Load orders from the database. fetch_orders() return dict, we're transforming it into list - stored_orders = self.fetch_orders().values() + orders = self.fetch_orders() + stored_orders = orders.values() if orders else [] stored_buy_orders = self.filter_buy_orders(stored_orders) stored_sell_orders = self.filter_sell_orders(stored_orders, invert=False) From fc0d6f87df7c2c25067570a0b4e90aefac98af81 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Mon, 11 Mar 2019 18:00:28 -0700 Subject: [PATCH 1220/1846] refactor config elements out of strategies, refactor cli config, add wif key to config --- dexbot/cli.py | 4 +- dexbot/cli_conf.py | 207 +++++++------------------ dexbot/cli_helper.py | 168 ++++++++++++++++++++ dexbot/strategies/base.py | 118 ++------------ dexbot/strategies/base_config.py | 101 ++++++++++++ dexbot/strategies/relative_config.py | 95 ++++++++++++ dexbot/strategies/relative_orders.py | 56 +------ dexbot/strategies/staggered_config.py | 70 +++++++++ dexbot/strategies/staggered_orders.py | 69 +-------- dexbot/strategies/strategy_config.py | 42 +++++ dexbot/strategies/strategy_template.py | 40 +---- 11 files changed, 558 insertions(+), 412 deletions(-) create mode 100644 dexbot/cli_helper.py create mode 100644 dexbot/strategies/base_config.py create mode 100644 dexbot/strategies/relative_config.py create mode 100644 dexbot/strategies/staggered_config.py create mode 100644 dexbot/strategies/strategy_config.py diff --git a/dexbot/cli.py b/dexbot/cli.py index e778f4644..92fb1c36d 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -93,7 +93,8 @@ 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: +# except ValueError: log.debug("Cannot set all signals -- not available on this platform") if ctx.obj['systemd']: try: @@ -128,6 +129,7 @@ def runservice(ctx): click.echo("Starting dexbot daemon") os.system("systemctl --user start dexbot") + @main.command() @click.pass_context @configfile diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 3de294150..1fcfeefb8 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -18,16 +18,14 @@ import os import os.path import sys -import re import subprocess from dexbot.whiptail import get_whiptail from dexbot.strategies.base import StrategyBase -import dexbot.helper +from dexbot.strategies.base_config import BaseConfig +from dexbot.cli_helper import ConfigValidator, select_choice, process_config_element -from bitshares.account import Account -from bitshares.exceptions import KeyAlreadyInStoreException -from bitsharesbase.account import PrivateKey +import dexbot.helper STRATEGIES = [ @@ -68,65 +66,6 @@ WantedBy=default.target """ - -def select_choice(current, choices): - """ For the radiolist, get us a list with the current value selected """ - return [(tag, text, (current == tag and "ON") or "OFF") - for tag, text in choices] - - -def process_config_element(elem, whiptail, config): - """ Process an item of configuration metadata display a widget as appropriate - d: the Dialog object - config: the config dictionary for this worker - """ - if elem.description: - title = '{} - {}'.format(elem.title, elem.description) - else: - title = elem.title - - if elem.type == "string": - txt = whiptail.prompt(title, config.get(elem.key, elem.default)) - if elem.extra: - while not re.match(elem.extra, txt): - whiptail.alert("The value is not valid") - txt = whiptail.prompt( - title, config.get( - elem.key, elem.default)) - config[elem.key] = txt - - if elem.type == "bool": - value = config.get(elem.key, elem.default) - value = 'yes' if value else 'no' - config[elem.key] = whiptail.confirm(title, value) - - if elem.type in ("float", "int"): - while True: - if elem.type == 'int': - template = '{}' - else: - template = '{:.8f}' - txt = whiptail.prompt(title, template.format(config.get(elem.key, elem.default))) - try: - if elem.type == "int": - val = int(txt) - else: - val = float(txt) - if val < elem.extra[0]: - whiptail.alert("The value is too low") - elif elem.extra[1] and val > elem.extra[1]: - whiptail.alert("the value is too high") - else: - break - except ValueError: - whiptail.alert("Not a valid value") - config[elem.key] = val - - if elem.type == "choice": - config[elem.key] = whiptail.radiolist(title, select_choice( - config.get(elem.key, elem.default), elem.extra)) - - def dexbot_service_running(): """ Return True if dexbot service is running """ @@ -180,10 +119,10 @@ def get_strategy_tag(strategy_class): for strategy in STRATEGIES: if strategy_class == strategy['class']: return strategy['tag'] - return None + return None -def configure_worker(whiptail, worker_config): +def configure_worker(whiptail, worker_config, bitshares_instance): # By default always editing editing = True @@ -219,9 +158,8 @@ def configure_worker(whiptail, worker_config): # Check if strategy has changed and editing existing worker if editing and default_strategy != get_strategy_tag(worker_config['module']): new_worker_config = {} - # If strategy has changed, create new config where base elements stay the same - for config_item in StrategyBase.configure(): + for config_item in BaseConfig.configure(): try: key = config_item[0] new_worker_config[key] = worker_config[key] @@ -235,88 +173,52 @@ def configure_worker(whiptail, worker_config): # Use class metadata for per-worker configuration config_elems = strategy_class.configure() + validator = ConfigValidator(whiptail, bitshares_instance) + if config_elems: # Strategy options for elem in config_elems: - process_config_element(elem, whiptail, worker_config) + if not editing and (elem.key == "account"): + # only allow WIF addition for new workers + account_name = validator.add_account() + if account_name is False: + return # quit configuration if can't get WIF added + else: + worker_config[elem.key] = account_name + else: # account name only for edit worker + process_config_element(elem, whiptail, worker_config) else: whiptail.alert( "This worker type does not have configuration information. " "You will have to check the worker code and add configuration values to config.yml if required") - return worker_config - - -def add_private_key(bitshares, private_key): - wallet = bitshares.wallet - try: - wallet.addPrivateKey(private_key) - except KeyAlreadyInStoreException: - # Private key already added - pass - -def validate_account_name(bitshares, account): - if not account: - return False - try: - Account(account, bitshares_instance=bitshares) - return True - except bitshares.exceptions.AccountDoesNotExistsException: - return False - - -def validate_private_key(bitshares, account, private_key): - wallet = bitshares.wallet - if not private_key: - # Check if the private key is already in the database - accounts = wallet.getAccounts() - if any(account == d['name'] for d in accounts): - return True - return False - - try: - pubkey = format(PrivateKey(private_key).pubkey,bitshares.prefix) - except ValueError: - return False - - accounts = wallet.getAllAccounts(pubkey) - account_names = [account['name'] for account in accounts] - - if account in account_names: - return True - else: - return False - - -def validate_private_key_type(self, account, private_key): - account = Account(account) - pubkey = format(PrivateKey(private_key).pubkey, self.bitshares.prefix) - key_type = self.bitshares.wallet.getKeyType(account, pubkey) - if key_type != 'active' and key_type != 'owner': - return False - return True + return worker_config def configure_dexbot(config, ctx): whiptail = get_whiptail('DEXBot configure') workers = config.get('workers', {}) + bitshares_instance = ctx.bitshares + validator = ConfigValidator(whiptail, bitshares_instance) + if not workers: while True: txt = whiptail.prompt("Your name for the worker") - config['workers'] = {txt: configure_worker(whiptail, {})} - if not whiptail.confirm("Set up another worker?\n(DEXBot can run multiple workers in one instance)"): - break + if len(txt) == 0: + whiptail.alert("Worker name cannot be blank. ") + else: + config['workers'] = {txt: configure_worker(whiptail, {}, bitshares_instance)} + if not whiptail.confirm("Set up another worker?\n(DEXBot can run multiple workers in one instance)"): + break setup_systemd(whiptail, config) else: - bitshares_instance = ctx.bitshares - while True: action = whiptail.menu( "You have an existing configuration.\nSelect an action:", - [('NEW', 'Create a new worker'), - ('LIST', 'List your workers'), - ('DEL', 'Delete a worker'), + [('LIST', 'List your workers'), + ('NEW', 'Create a new worker'), ('EDIT', 'Edit a worker'), + ('DEL', 'Delete a worker'), ('ADD', 'Add a bitshares account'), ('SHOW', 'Show bitshares accounts'), ('NODES', 'Edit Node Selection'), @@ -325,45 +227,42 @@ def configure_dexbot(config, ctx): ('EXIT', 'Quit this application')]) my_workers = [(index, index) for index in workers] - + if action == 'EXIT': ## cancel will also exit the application. but this is a clearer label - ## todo: modify cancel to be "Quit" or "Exit" + ## Todo: modify cancel to be "Quit" or "Exit" for the whiptail menu item. break - elif action =='LIST': - my_list = whiptail.menu("List of Your Workers", my_workers) - + elif action =='LIST': + # list workers, then provide option to list config of workers + worker_name = whiptail.menu("List of Your Workers. Select to view Configuration.", my_workers) + content = config['workers'][worker_name] + worker_content = list(content.items()) + worker_list = [[str(i) for i in pairs] for pairs in worker_content] + worker_list = [tuple(i) for i in worker_list] + whiptail.menu(worker_name, worker_list) + elif action == 'EDIT': - worker_name = whiptail.menu("Select worker to edit", [(index, index) for index in workers]) - config['workers'][worker_name] = configure_worker(whiptail, config['workers'][worker_name]) + worker_name = whiptail.menu("Select worker to edit", my_workers) + config['workers'][worker_name] = configure_worker(whiptail, config['workers'][worker_name], bitshares_instance) strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) strategy.clear_all_worker_data() + elif action == 'DEL': - worker_name = whiptail.menu("Select worker to delete", [(index, index) for index in workers]) + worker_name = whiptail.menu("Select worker to delete", my_workers) del config['workers'][worker_name] strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) strategy.clear_all_worker_data() elif action == 'NEW': - txt = whiptail.prompt("Your name for the new worker. Don't forget to add your bitshares key (main menu)") - config['workers'][txt] = configure_worker(whiptail, {}) - elif action == 'ADD': - account = whiptail.prompt("Your Account Name") - private_key = whiptail.prompt("Your Private Key", password=True) - if not validate_account_name(bitshares_instance, account): - whiptail.alert("Account name does not exist.") - if validate_private_key(bitshares_instance, account, private_key): - whiptail.alert("Private key and account are valid, Adding private Key") - add_private_key(bitshares_instance, private_key) + txt = whiptail.prompt("Your name for the new worker. ") + if len(txt) == 0: + whiptail.alert("Worker name cannot be blank. ") else: - whiptail.alert("Private key and account do not match.") - elif action =='SHOW': - wallet = bitshares_instance.wallet - accounts = wallet.getAccounts() - account_list = [(i['name'], i['type']) for i in accounts] - if len(account_list) == 0: - account_list = [('none', 'none')] + config['workers'][txt] = configure_worker(whiptail, {}, bitshares_instance) + elif action == 'ADD': + validator.add_account() + elif action =='SHOW': + account_list = validator.list_accounts() action = whiptail.menu("Bitshares Account List (Name - Type)", account_list) - elif action == 'ADD_NODE': txt = whiptail.prompt("Your name for the new node: e.g. wss://dexnode.net/ws") config['node'][0] = txt diff --git a/dexbot/cli_helper.py b/dexbot/cli_helper.py new file mode 100644 index 000000000..65e5ae202 --- /dev/null +++ b/dexbot/cli_helper.py @@ -0,0 +1,168 @@ +import re + +from dexbot.whiptail import get_whiptail + +import bitshares +from bitshares.instance import shared_bitshares_instance +from bitshares.asset import Asset +from bitshares.account import Account +from bitshares.exceptions import KeyAlreadyInStoreException, AccountDoesNotExistsException +from bitsharesbase.account import PrivateKey + + +def select_choice(current, choices): + """ For the radiolist, get us a list with the current value selected """ + return [(tag, text, (current == tag and "ON") or "OFF") + for tag, text in choices] + + +def process_config_element(elem, whiptail, config): + """ Process an item of configuration metadata display a widget as appropriate + d: the Dialog object + config: the config dictionary for this worker + """ + if elem.description: + title = '{} - {}'.format(elem.title, elem.description) + else: + title = elem.title + + if elem.type == "string": + txt = whiptail.prompt(title, config.get(elem.key, elem.default)) + if elem.extra: + while not re.match(elem.extra, txt): + whiptail.alert("The value is not valid") + txt = whiptail.prompt( + title, config.get( + elem.key, elem.default)) + config[elem.key] = txt + + if elem.type == "bool": + value = config.get(elem.key, elem.default) + value = 'yes' if value else 'no' + config[elem.key] = whiptail.confirm(title, value) + + if elem.type in ("float", "int"): + while True: + if elem.type == 'int': + template = '{}' + else: + template = '{:.8f}' + txt = whiptail.prompt(title, template.format(config.get(elem.key, elem.default))) + try: + if elem.type == "int": + val = int(txt) + else: + val = float(txt) + if val < elem.extra[0]: + whiptail.alert("The value is too low") + elif elem.extra[1] and val > elem.extra[1]: + whiptail.alert("the value is too high") + else: + break + except ValueError: + whiptail.alert("Not a valid value") + config[elem.key] = val + + if elem.type == "choice": + config[elem.key] = whiptail.radiolist(title, select_choice( + config.get(elem.key, elem.default), elem.extra)) + + +class ConfigValidator: + """ validation methods borrowed from gui WorkerController for Cli + """ + + def __init__(self, whiptail, bitshares_instance): + self.bitshares = bitshares_instance or shared_bitshares_instance() + self.whiptail = whiptail + + @classmethod + def validate_worker_name(cls, worker_name, old_worker_name=None): + if old_worker_name != worker_name: + worker_names = Config().workers_data.keys() + # Check that the name is unique + if worker_name in worker_names: + return False + return True + return True + + def validate_asset(self, asset): + try: + Asset(asset, bitshares_instance=self.bitshares) + return True + except bitshares.exceptions.AssetDoesNotExistsException: + return False + + def validate_account_name(self, account): + if not account: + return False + try: + Account(account, bitshares_instance=self.bitshares) + return True + except bitshares.exceptions.AccountDoesNotExistsException: + return False + + def validate_private_key(self, account, private_key): + wallet = self.bitshares.wallet + if not private_key: + # Check if the private key is already in the database + accounts = wallet.getAccounts() + if any(account == d['name'] for d in accounts): + return True + return False + + try: + pubkey = format(PrivateKey(private_key).pubkey, self.bitshares.prefix) + except ValueError: + return False + + accounts = wallet.getAllAccounts(pubkey) + account_names = [account['name'] for account in accounts] + + if account in account_names: + return True + else: + return False + + def validate_private_key_type(self, account, private_key): + account = Account(account) + pubkey = format(PrivateKey(private_key).pubkey, self.bitshares.prefix) + key_type = self.bitshares.wallet.getKeyType(account, pubkey) + if key_type != 'active' and key_type != 'owner': + return False + return True + + def add_private_key(self, private_key): + wallet = self.bitshares.wallet + try: + wallet.addPrivateKey(private_key) + except KeyAlreadyInStoreException: + # Private key already added + pass + + def list_accounts(self): + accounts = self.bitshares.wallet.getAccounts() + account_list = [(i['name'], i['type']) for i in accounts] + if len(account_list) == 0: + account_list = [('none', 'none')] + return account_list + + def add_account(self): + # this method modeled off of worker_controller in gui + account = self.whiptail.prompt("Your Account Name") + private_key = self.whiptail.prompt("Your Private Key", password=True) + + if not self.validate_account_name(account): + self.whiptail.alert("Account name does not exist.") + return False + if not self.validate_private_key(account, private_key): + self.whiptail.alert("Private key is invalid") + return False + if private_key and not self.validate_private_key_type(account, private_key): + self.whiptail.alert("Please use active private key.") + return False + + self.add_private_key(private_key) + self.whiptail.alert("Private Key added successfully.") + return account + diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index a349b3c05..d2c651e46 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -11,6 +11,7 @@ from dexbot.helper import truncate from dexbot.strategies.external_feeds.price_feed import PriceFeed from dexbot.qt_queue.idle_queue import idle_add +from dexbot.strategies.base_config import BaseConfig from events import Events import bitshares.exceptions @@ -26,63 +27,6 @@ # Number of maximum retries used to retry action before failing MAX_TRIES = 3 -""" Strategies need to specify their own configuration values, so each strategy can have a class method 'configure' - which returns a list of ConfigElement named tuples. - - Tuple fields as follows: - - Key: The key in the bot config dictionary that gets saved back to config.yml - - Type: "int", "float", "bool", "string" or "choice" - - Default: The default value, must be same type as the Type defined - - Title: Name shown to the user, preferably not too long - - Description: Comments to user, full sentences encouraged - - Extra: - :int: a (min, max, suffix) tuple - :float: a (min, max, precision, suffix) tuple - :string: a regular expression, entries must match it, can be None which equivalent to .* - :bool, ignored - :choice: a list of choices, choices are in turn (tag, label) tuples. - NOTE: 'labels' get presented to user, and 'tag' is used as the value saved back to the config dict! -""" -ConfigElement = collections.namedtuple('ConfigElement', 'key type default title description extra') - -""" Strategies have different needs for the details they want to show for the user. These elements help to build a - custom details window for the strategy. - - Tuple fields as follows: - - Type: 'graph', 'text', 'table' - - Name: The name of the tab, shows at the top - - Title: The title is shown inside the tab - - File: Tabs can also show data from files, pass on the file name including the file extension - in strategy's `configure_details`. - - Below folders and representative file types that inside the folders. - - Location File extensions - --------------------------- - dexbot/graphs .png, .jpg - dexbot/data .csv - dexbot/logs .log, .txt (.csv, will print as raw data) - - NOTE: To avoid conflicts with other custom strategies, when generating names for files, use slug or worker's - name when generating files or create custom folders. Add relative path to 'file' parameter if file is in - custom folder inside default folders. Like shown below: - - `DetailElement('log', 'Worker log', 'Log of worker's actions', 'my_custom_folder/example_worker.log')` -""" -DetailElement = collections.namedtuple('DetailTab', 'type name title file') - -# External exchanges used to calculate center price -EXCHANGES = [ - # ('none', 'None. Use Manual or Bitshares DEX Price (default)'), - ('gecko', 'Coingecko'), - ('waves', 'Waves DEX'), - ('kraken', 'Kraken'), - ('bitfinex', 'Bitfinex'), - ('gdax', 'Gdax'), - ('binance', 'Binance') -] - - class StrategyBase(Storage, StateMachine, Events): """ A strategy based on this class is intended to work in one market. This class contains most common methods needed by the strategy. @@ -121,7 +65,16 @@ class StrategyBase(Storage, StateMachine, Events): 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. """ + + @classmethod + def configure(cls, return_base_config=True): + return BaseConfig.configure(return_base_config) + + @classmethod + def configure_details(cls, include_default_tabs=True): + return BaseConfig.configure_details(include_default_tabs) + __events__ = [ 'onAccount', 'onMarketUpdate', @@ -134,57 +87,6 @@ class StrategyBase(Storage, StateMachine, Events): 'error_ontick', ] - @classmethod - def configure(cls, return_base_config=True): - """ Return a list of ConfigElement objects defining the configuration values for this class. - - User interfaces should then generate widgets based on these values, gather data and save back to - the config dictionary for the worker. - - NOTE: When overriding you almost certainly will want to call the ancestor and then - add your config values to the list. - - :param return_base_config: bool: - :return: Returns a list of config elements - """ - - # Common configs - base_config = [ - ConfigElement('account', 'string', '', 'Account', - 'BitShares account name for the bot to operate with', - ''), - ConfigElement('market', 'string', 'USD:BTS', 'Market', - 'BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"', - r'[A-Z\.]+[:\/][A-Z\.]+'), - ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset', - 'Asset to be used to pay transaction fees', - r'[A-Z\.]+') - ] - - if return_base_config: - return base_config - return [] - - @classmethod - def configure_details(cls, include_default_tabs=True): - """ Return a list of ConfigElement objects defining the configuration values for this class. - - User interfaces should then generate widgets based on these values, gather data and save back to - the config dictionary for the worker. - - NOTE: When overriding you almost certainly will want to call the ancestor and then - add your config values to the list. - - :param include_default_tabs: bool: - :return: Returns a list of Detail elements - """ - - # Common configs - details = [] - - if include_default_tabs: - return details - return [] def __init__(self, name, diff --git a/dexbot/strategies/base_config.py b/dexbot/strategies/base_config.py new file mode 100644 index 000000000..a4e37c3c4 --- /dev/null +++ b/dexbot/strategies/base_config.py @@ -0,0 +1,101 @@ +import collections + +""" Strategies need to specify their own configuration values, so each strategy can have a class method 'configure' + which returns a list of ConfigElement named tuples. + + Tuple fields as follows: + - Key: The key in the bot config dictionary that gets saved back to config.yml + - Type: "int", "float", "bool", "string" or "choice" + - Default: The default value, must be same type as the Type defined + - Title: Name shown to the user, preferably not too long + - Description: Comments to user, full sentences encouraged + - Extra: + :int: a (min, max, suffix) tuple + :float: a (min, max, precision, suffix) tuple + :string: a regular expression, entries must match it, can be None which equivalent to .* + :bool, ignored + :choice: a list of choices, choices are in turn (tag, label) tuples. + NOTE: 'labels' get presented to user, and 'tag' is used as the value saved back to the config dict! +""" +ConfigElement = collections.namedtuple('ConfigElement', 'key type default title description extra') + +""" Strategies have different needs for the details they want to show for the user. These elements help to build a + custom details window for the strategy. + + Tuple fields as follows: + - Type: 'graph', 'text', 'table' + - Name: The name of the tab, shows at the top + - Title: The title is shown inside the tab + - File: Tabs can also show data from files, pass on the file name including the file extension + in strategy's `configure_details`. + + Below folders and representative file types that inside the folders. + + Location File extensions + --------------------------- + dexbot/graphs .png, .jpg + dexbot/data .csv + dexbot/logs .log, .txt (.csv, will print as raw data) + + NOTE: To avoid conflicts with other custom strategies, when generating names for files, use slug or worker's + name when generating files or create custom folders. Add relative path to 'file' parameter if file is in + custom folder inside default folders. Like shown below: + + `DetailElement('log', 'Worker log', 'Log of worker's actions', 'my_custom_folder/example_worker.log')` +""" +DetailElement = collections.namedtuple('DetailTab', 'type name title file') + + +class BaseConfig(): + + @classmethod + def configure(cls, return_base_config=True): + """ Return a list of ConfigElement objects defining the configuration values for this class. + + User interfaces should then generate widgets based on these values, gather data and save back to + the config dictionary for the worker. + + NOTE: When overriding you almost certainly will want to call the ancestor and then + add your config values to the list. + + :param return_base_config: bool: + :return: Returns a list of config elements + """ + + # Common configs + base_config = [ + ConfigElement('account', 'string', '', 'Account', + 'BitShares account name for the bot to operate with', + ''), + ConfigElement('market', 'string', 'USD:BTS', 'Market', + 'BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"', + r'[A-Z\.]+[:\/][A-Z\.]+'), + ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset', + 'Asset to be used to pay transaction fees', + r'[A-Z\.]+') + ] + + if return_base_config: + return base_config + return [] + + @classmethod + def configure_details(cls, include_default_tabs=True): + """ Return a list of ConfigElement objects defining the configuration values for this class. + + User interfaces should then generate widgets based on these values, gather data and save back to + the config dictionary for the worker. + + NOTE: When overriding you almost certainly will want to call the ancestor and then + add your config values to the list. + + :param include_default_tabs: bool: + :return: Returns a list of Detail elements + """ + + # Common configs + details = [] + + if include_default_tabs: + return details + return [] diff --git a/dexbot/strategies/relative_config.py b/dexbot/strategies/relative_config.py new file mode 100644 index 000000000..59be2807c --- /dev/null +++ b/dexbot/strategies/relative_config.py @@ -0,0 +1,95 @@ +from dexbot.strategies.base_config import BaseConfig, ConfigElement, DetailElement + +class RelativeConfig(BaseConfig): + + @classmethod + def configure(cls, return_base_config=True): + """ Return a list of ConfigElement objects defining the configuration values for this class. + + User interfaces should then generate widgets based on these values, gather data and save back to + the config dictionary for the worker. + + NOTE: When overriding you almost certainly will want to call the ancestor and then + add your config values to the list. + + :param return_base_config: bool: + :return: Returns a list of config elements + """ + # External exchanges used to calculate center price + EXCHANGES = [ + # ('none', 'None. Use Manual or Bitshares DEX Price (default)'), + ('gecko', 'Coingecko'), + ('waves', 'Waves DEX'), + ('kraken', 'Kraken'), + ('bitfinex', 'Bitfinex'), + ('gdax', 'Gdax'), + ('binance', 'Binance') + ] + + relative_orders_config = [ + ConfigElement('external_feed', 'bool', False, 'External price feed', + 'Use external reference price instead of center price acquired from the market', None), + ConfigElement('external_price_source', 'choice', 'gecko', 'External price source', + 'The bot will try to get price information from this source', EXCHANGES), + ConfigElement('amount', 'float', 1, 'Amount', + 'Fixed order size, expressed in quote asset, unless "relative order size" selected', + (0, None, 8, '')), + ConfigElement('relative_order_size', 'bool', False, 'Relative order size', + 'Amount is expressed as a percentage of the account balance of quote/base asset', None), + ConfigElement('spread', 'float', 5, 'Spread', + 'The percentage difference between buy and sell', (0, 100, 2, '%')), + ConfigElement('dynamic_spread', 'bool', False, 'Dynamic spread', + 'Enable dynamic spread which overrides the spread field', None), + ConfigElement('market_depth_amount', 'float', 0, 'Market depth', + 'From which depth will market spread be measured? (QUOTE amount)', + (0.00000001, 1000000000, 8, '')), + ConfigElement('dynamic_spread_factor', 'float', 1, 'Dynamic spread factor', + 'How many percent will own spread be compared to market spread?', + (0.01, 1000, 2, '%')), + ConfigElement('center_price', 'float', 0, 'Center price', + 'Fixed center price expressed in base asset: base/quote', (0, None, 8, '')), + ConfigElement('center_price_dynamic', 'bool', True, 'Measure center price from market orders', + 'Estimate the center from closest opposite orders or from a depth', None), + ConfigElement('center_price_depth', 'float', 0, 'Measurement depth', + 'Cumulative quote amount from which depth center price will be measured', + (0.00000001, 1000000000, 8, '')), + ConfigElement('center_price_offset', 'bool', False, 'Center price offset based on asset balances', + 'Automatically adjust orders up or down based on the imbalance of your assets', None), + ConfigElement('manual_offset', 'float', 0, 'Manual center price offset', + "Manually adjust orders up or down. " + "Works independently of other offsets and doesn't override them", (-50, 100, 2, '%')), + ConfigElement('reset_on_partial_fill', 'bool', True, 'Reset orders on partial fill', + 'Reset orders when buy or sell order is partially filled', None), + ConfigElement('partial_fill_threshold', 'float', 30, 'Fill threshold', + 'Order fill threshold to reset orders', (0, 100, 2, '%')), + ConfigElement('reset_on_price_change', 'bool', False, 'Reset orders on center price change', + 'Reset orders when center price is changed more than threshold ' + '(set False for external feeds)', None), + ConfigElement('price_change_threshold', 'float', 2, 'Price change threshold', + 'Define center price threshold to react on', (0, 100, 2, '%')), + ConfigElement('custom_expiration', 'bool', False, 'Custom expiration', + 'Override order expiration time to trigger a reset', None), + ConfigElement('expiration_time', 'int', 157680000, 'Order expiration time', + 'Define custom order expiration time to force orders reset more often, seconds', + (30, 157680000, '')) + ] + + if return_base_config: + return BaseConfig.configure(return_base_config) + relative_orders_config + return [] + + + @classmethod + def configure_details(cls, include_default_tabs=True): + """ Return a list of ConfigElement objects defining the configuration values for this class. + + User interfaces should then generate widgets based on these values, gather data and save back to + the config dictionary for the worker. + + NOTE: When overriding you almost certainly will want to call the ancestor and then + add your config values to the list. + + :param include_default_tabs: bool: + :return: Returns a list of Detail elements + """ + return BaseConfig.configure_details(include_default_tabs) + [] diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 15628d565..154fc93e0 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -1,67 +1,21 @@ import math from datetime import datetime, timedelta -from dexbot.strategies.base import StrategyBase, ConfigElement, DetailElement, EXCHANGES +from dexbot.strategies.base import StrategyBase +from dexbot.strategies.relative_config import RelativeConfig from dexbot.qt_queue.idle_queue import idle_add - class Strategy(StrategyBase): """ Relative Orders strategy """ - + @classmethod def configure(cls, return_base_config=True): - return StrategyBase.configure(return_base_config) + [ - ConfigElement('external_price_source', 'choice', EXCHANGES[0], 'External price source', - 'The bot will try to get price information from this source', EXCHANGES), - ConfigElement('external_feed', 'bool', False, 'External price feed', - 'Use external reference price instead of center price acquired from the market', None), - ConfigElement('amount', 'float', 1, 'Amount', - 'Fixed order size, expressed in quote asset, unless "relative order size" selected', - (0, None, 8, '')), - ConfigElement('relative_order_size', 'bool', False, 'Relative order size', - 'Amount is expressed as a percentage of the account balance of quote/base asset', None), - ConfigElement('spread', 'float', 5, 'Spread', - 'The percentage difference between buy and sell', (0, 100, 2, '%')), - ConfigElement('dynamic_spread', 'bool', False, 'Dynamic spread', - 'Enable dynamic spread which overrides the spread field', None), - ConfigElement('market_depth_amount', 'float', 0, 'Market depth', - 'From which depth will market spread be measured? (QUOTE amount)', - (0.00000001, 1000000000, 8, '')), - ConfigElement('dynamic_spread_factor', 'float', 1, 'Dynamic spread factor', - 'How many percent will own spread be compared to market spread?', - (0.01, 1000, 2, '%')), - ConfigElement('center_price', 'float', 0, 'Center price', - 'Fixed center price expressed in base asset: base/quote', (0, None, 8, '')), - ConfigElement('center_price_dynamic', 'bool', True, 'Measure center price from market orders', - 'Estimate the center from closest opposite orders or from a depth', None), - ConfigElement('center_price_depth', 'float', 0, 'Measurement depth', - 'Cumulative quote amount from which depth center price will be measured', - (0.00000001, 1000000000, 8, '')), - ConfigElement('center_price_offset', 'bool', False, 'Center price offset based on asset balances', - 'Automatically adjust orders up or down based on the imbalance of your assets', None), - ConfigElement('manual_offset', 'float', 0, 'Manual center price offset', - "Manually adjust orders up or down. " - "Works independently of other offsets and doesn't override them", (-50, 100, 2, '%')), - ConfigElement('reset_on_partial_fill', 'bool', True, 'Reset orders on partial fill', - 'Reset orders when buy or sell order is partially filled', None), - ConfigElement('partial_fill_threshold', 'float', 30, 'Fill threshold', - 'Order fill threshold to reset orders', (0, 100, 2, '%')), - ConfigElement('reset_on_price_change', 'bool', False, 'Reset orders on center price change', - 'Reset orders when center price is changed more than threshold ' - '(set False for external feeds)', None), - ConfigElement('price_change_threshold', 'float', 2, 'Price change threshold', - 'Define center price threshold to react on', (0, 100, 2, '%')), - ConfigElement('custom_expiration', 'bool', False, 'Custom expiration', - 'Override order expiration time to trigger a reset', None), - ConfigElement('expiration_time', 'int', 157680000, 'Order expiration time', - 'Define custom order expiration time to force orders reset more often, seconds', - (30, 157680000, '')) - ] + return RelativeConfig.configure(return_base_config) @classmethod def configure_details(cls, include_default_tabs=True): - return StrategyBase.configure_details(include_default_tabs) + [] + return RelativeConfig.configure_details(include_default_tabs) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/dexbot/strategies/staggered_config.py b/dexbot/strategies/staggered_config.py new file mode 100644 index 000000000..2f0cfef7f --- /dev/null +++ b/dexbot/strategies/staggered_config.py @@ -0,0 +1,70 @@ +from dexbot.strategies.base_config import BaseConfig, ConfigElement, DetailElement + +class StaggeredConfig(BaseConfig): + + @classmethod + def configure(cls, return_base_config=True): + """ Modes description: + + Mountain: + - Buy orders same QUOTE + - Sell orders same BASE + + Neutral: + - Buy orders lower_order_base * sqrt(1 + increment) + - Sell orders higher_order_quote * sqrt(1 + increment) + + Valley: + - Buy orders same BASE + - Sell orders same QUOTE + + Buy slope: + - All orders same BASE (profit comes in QUOTE) + + Sell slope: + - All orders same QUOTE (profit made in BASE) + """ + modes = [ + ('mountain', 'Mountain'), + ('neutral', 'Neutral'), + ('valley', 'Valley'), + ('buy_slope', 'Buy Slope'), + ('sell_slope', 'Sell Slope') + ] + + return BaseConfig.configure(return_base_config) + [ + ConfigElement( + 'mode', 'choice', 'neutral', 'Strategy mode', + 'How to allocate funds and profits. Doesn\'t effect existing orders, only future ones', modes), + ConfigElement( + 'spread', 'float', 6, 'Spread', + 'The percentage difference between buy and sell', (0, None, 2, '%')), + ConfigElement( + 'increment', 'float', 4, 'Increment', + 'The percentage difference between staggered orders', (0, None, 2, '%')), + ConfigElement( + 'center_price_dynamic', 'bool', True, 'Market center price', + 'Begin strategy with center price obtained from the market. Use with mature markets', None), + ConfigElement( + 'center_price', 'float', 0, 'Manual center price', + 'In an immature market, give a center price manually to begin with. BASE/QUOTE', + (0, 1000000000, 8, '')), + ConfigElement( + 'lower_bound', 'float', 1, 'Lower bound', + 'The bottom price in the range', + (0, 1000000000, 8, '')), + ConfigElement( + 'upper_bound', 'float', 1000000, 'Upper bound', + 'The top price in the range', + (0, 1000000000, 8, '')), + ConfigElement( + 'instant_fill', 'bool', True, 'Allow instant fill', + 'Allow to execute orders by market', None), + ConfigElement( + 'operational_depth', 'int', 10, 'Operational depth', + 'Order depth to maintain on books', (2, 9999999, None)) + ] + + @classmethod + def configure_details(cls, include_default_tabs=True): + return BaseConfig.configure_details(include_default_tabs) + [] diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 9a6ad9409..ad9b172e4 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -8,79 +8,20 @@ from bitshares.dex import Dex from bitshares.amount import Amount -from dexbot.strategies.base import StrategyBase, ConfigElement, DetailElement - +from dexbot.strategies.base import StrategyBase +from dexbot.strategies.staggered_config import StaggeredConfig class Strategy(StrategyBase): """ Staggered Orders strategy """ @classmethod def configure(cls, return_base_config=True): - """ Modes description: - - Mountain: - - Buy orders same QUOTE - - Sell orders same BASE - - Neutral: - - Buy orders lower_order_base * sqrt(1 + increment) - - Sell orders higher_order_quote * sqrt(1 + increment) - - Valley: - - Buy orders same BASE - - Sell orders same QUOTE - - Buy slope: - - All orders same BASE (profit comes in QUOTE) - - Sell slope: - - All orders same QUOTE (profit made in BASE) - """ - modes = [ - ('mountain', 'Mountain'), - ('neutral', 'Neutral'), - ('valley', 'Valley'), - ('buy_slope', 'Buy Slope'), - ('sell_slope', 'Sell Slope') - ] - - return StrategyBase.configure(return_base_config) + [ - ConfigElement( - 'mode', 'choice', 'neutral', 'Strategy mode', - 'How to allocate funds and profits. Doesn\'t effect existing orders, only future ones', modes), - ConfigElement( - 'spread', 'float', 6, 'Spread', - 'The percentage difference between buy and sell', (0, None, 2, '%')), - ConfigElement( - 'increment', 'float', 4, 'Increment', - 'The percentage difference between staggered orders', (0, None, 2, '%')), - ConfigElement( - 'center_price_dynamic', 'bool', True, 'Market center price', - 'Begin strategy with center price obtained from the market. Use with mature markets', None), - ConfigElement( - 'center_price', 'float', 0, 'Manual center price', - 'In an immature market, give a center price manually to begin with. BASE/QUOTE', - (0, 1000000000, 8, '')), - ConfigElement( - 'lower_bound', 'float', 1, 'Lower bound', - 'The bottom price in the range', - (0, 1000000000, 8, '')), - ConfigElement( - 'upper_bound', 'float', 1000000, 'Upper bound', - 'The top price in the range', - (0, 1000000000, 8, '')), - ConfigElement( - 'instant_fill', 'bool', True, 'Allow instant fill', - 'Allow to execute orders by market', None), - ConfigElement( - 'operational_depth', 'int', 10, 'Operational depth', - 'Order depth to maintain on books', (2, 9999999, None)) - ] + return StaggeredConfig.configure(return_base_config) @classmethod def configure_details(cls, include_default_tabs=True): - return StrategyBase.configure_details(include_default_tabs) + [] - + return StaggeredConfig.configure_details(include_default_tabs) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/dexbot/strategies/strategy_config.py b/dexbot/strategies/strategy_config.py new file mode 100644 index 000000000..323b9c0c0 --- /dev/null +++ b/dexbot/strategies/strategy_config.py @@ -0,0 +1,42 @@ +from dexbot.strategies.base_config import BaseConfig, ConfigElement, DetailElement + +class StrategyConfig(BaseConfig): + """ this is the configuration template for the strategy_template class + """ + + @classmethod + def configure(cls, return_base_config=True): + """ This function is used to auto generate fields for GUI + + :param return_base_config: If base config is used in addition to this configuration. + :return: List of ConfigElement(s) + """ + + """ As a demonstration this template has two fields in the worker configuration. Upper and lower bound. + Documentation of ConfigElements can be found from base.py. + """ + return BaseConfig.configure(return_base_config) + [ + ConfigElement('lower_bound', 'float', 1, 'Lower bound', + 'The bottom price in the range', + (0, 10000000, 8, '')), + ConfigElement('upper_bound', 'float', 10, 'Upper bound', + 'The top price in the range', + (0, 10000000, 8, '')), + ] + + + @classmethod + def configure_details(cls, include_default_tabs=True): + """ This function defines the tabs for detailed view of the worker. Further documentation is found in base.py + + :param include_default_tabs: If default tabs are included as well + :return: List of DetailElement(s) + + NOTE: Add files to user data folders to see how they behave as an example. + """ + return BaseConfig.configure_details(include_default_tabs) + [ + DetailElement('graph', 'Graph', 'Graph', 'graph.jpg'), + DetailElement('table', 'Orders', 'Data from csv file', 'example.csv'), + DetailElement('text', 'Log', 'Log data', 'example.log') + ] + diff --git a/dexbot/strategies/strategy_template.py b/dexbot/strategies/strategy_template.py index a1701e3e7..99bb464ee 100644 --- a/dexbot/strategies/strategy_template.py +++ b/dexbot/strategies/strategy_template.py @@ -2,7 +2,8 @@ import math # Project imports -from dexbot.strategies.base import StrategyBase, ConfigElement, DetailElement +from dexbot.strategies.base import StrategyBase +from dexbot.strategies.strategy_config import StrategyConfig from dexbot.qt_queue.idle_queue import idle_add # Third party imports @@ -10,11 +11,10 @@ STRATEGY_NAME = 'Strategy Template' - class Strategy(StrategyBase): """ - Replace with the name of the strategy. + Replace with the name of the strategy. This is a template strategy which can be used to create custom strategies easier. The base for the strategy is ready. It is recommended comment the strategy and functions to help other developers to make changes. @@ -40,42 +40,14 @@ class Strategy(StrategyBase): NOTE: Change this comment section to describe the strategy. """ - @classmethod def configure(cls, return_base_config=True): - """ This function is used to auto generate fields for GUI - - :param return_base_config: If base config is used in addition to this configuration. - :return: List of ConfigElement(s) - """ - - """ As a demonstration this template has two fields in the worker configuration. Upper and lower bound. - Documentation of ConfigElements can be found from base.py. - """ - return StrategyBase.configure(return_base_config) + [ - ConfigElement('lower_bound', 'float', 1, 'Lower bound', - 'The bottom price in the range', - (0, 10000000, 8, '')), - ConfigElement('upper_bound', 'float', 10, 'Upper bound', - 'The top price in the range', - (0, 10000000, 8, '')), - ] + return StrategyConfig.configure(return_base_config) @classmethod def configure_details(cls, include_default_tabs=True): - """ This function defines the tabs for detailed view of the worker. Further documentation is found in base.py - - :param include_default_tabs: If default tabs are included as well - :return: List of DetailElement(s) - - NOTE: Add files to user data folders to see how they behave as an example. - """ - return StrategyBase.configure_details(include_default_tabs) + [ - DetailElement('graph', 'Graph', 'Graph', 'graph.jpg'), - DetailElement('table', 'Orders', 'Data from csv file', 'example.csv'), - DetailElement('text', 'Log', 'Log data', 'example.log') - ] - + return StrategyConfig.configure_details(return_base_config) + def __init__(self, *args, **kwargs): # Initializes StrategyBase class super().__init__(*args, **kwargs) From 4e6d84da826a72ac7c67706b5991c3770da98a38 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Mon, 11 Mar 2019 21:58:53 -0700 Subject: [PATCH 1221/1846] move 2 methods back to cli_conf --- dexbot/cli_conf.py | 64 +++++++++++++++++++++++++++++-- dexbot/cli_helper.py | 89 ++++++-------------------------------------- 2 files changed, 72 insertions(+), 81 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 1fcfeefb8..add9889aa 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -18,12 +18,12 @@ import os import os.path import sys +import re import subprocess from dexbot.whiptail import get_whiptail from dexbot.strategies.base import StrategyBase -from dexbot.strategies.base_config import BaseConfig -from dexbot.cli_helper import ConfigValidator, select_choice, process_config_element +from dexbot.cli_helper import ConfigValidator import dexbot.helper @@ -122,6 +122,64 @@ def get_strategy_tag(strategy_class): return None +def select_choice(current, choices): + """ For the radiolist, get us a list with the current value selected """ + return [(tag, text, (current == tag and "ON") or "OFF") + for tag, text in choices] + + +def process_config_element(elem, whiptail, config): + """ Process an item of configuration metadata display a widget as appropriate + d: the Dialog object + config: the config dictionary for this worker + """ + if elem.description: + title = '{} - {}'.format(elem.title, elem.description) + else: + title = elem.title + + if elem.type == "string": + txt = whiptail.prompt(title, config.get(elem.key, elem.default)) + if elem.extra: + while not re.match(elem.extra, txt): + whiptail.alert("The value is not valid") + txt = whiptail.prompt( + title, config.get( + elem.key, elem.default)) + config[elem.key] = txt + + if elem.type == "bool": + value = config.get(elem.key, elem.default) + value = 'yes' if value else 'no' + config[elem.key] = whiptail.confirm(title, value) + + if elem.type in ("float", "int"): + while True: + if elem.type == 'int': + template = '{}' + else: + template = '{:.8f}' + txt = whiptail.prompt(title, template.format(config.get(elem.key, elem.default))) + try: + if elem.type == "int": + val = int(txt) + else: + val = float(txt) + if val < elem.extra[0]: + whiptail.alert("The value is too low") + elif elem.extra[1] and val > elem.extra[1]: + whiptail.alert("the value is too high") + else: + break + except ValueError: + whiptail.alert("Not a valid value") + config[elem.key] = val + + if elem.type == "choice": + config[elem.key] = whiptail.radiolist(title, select_choice( + config.get(elem.key, elem.default), elem.extra)) + + def configure_worker(whiptail, worker_config, bitshares_instance): # By default always editing editing = True @@ -159,7 +217,7 @@ def configure_worker(whiptail, worker_config, bitshares_instance): if editing and default_strategy != get_strategy_tag(worker_config['module']): new_worker_config = {} # If strategy has changed, create new config where base elements stay the same - for config_item in BaseConfig.configure(): + for config_item in StrategyBase.configure(): try: key = config_item[0] new_worker_config[key] = worker_config[key] diff --git a/dexbot/cli_helper.py b/dexbot/cli_helper.py index 65e5ae202..4050b35ac 100644 --- a/dexbot/cli_helper.py +++ b/dexbot/cli_helper.py @@ -1,5 +1,3 @@ -import re - from dexbot.whiptail import get_whiptail import bitshares @@ -9,64 +7,6 @@ from bitshares.exceptions import KeyAlreadyInStoreException, AccountDoesNotExistsException from bitsharesbase.account import PrivateKey - -def select_choice(current, choices): - """ For the radiolist, get us a list with the current value selected """ - return [(tag, text, (current == tag and "ON") or "OFF") - for tag, text in choices] - - -def process_config_element(elem, whiptail, config): - """ Process an item of configuration metadata display a widget as appropriate - d: the Dialog object - config: the config dictionary for this worker - """ - if elem.description: - title = '{} - {}'.format(elem.title, elem.description) - else: - title = elem.title - - if elem.type == "string": - txt = whiptail.prompt(title, config.get(elem.key, elem.default)) - if elem.extra: - while not re.match(elem.extra, txt): - whiptail.alert("The value is not valid") - txt = whiptail.prompt( - title, config.get( - elem.key, elem.default)) - config[elem.key] = txt - - if elem.type == "bool": - value = config.get(elem.key, elem.default) - value = 'yes' if value else 'no' - config[elem.key] = whiptail.confirm(title, value) - - if elem.type in ("float", "int"): - while True: - if elem.type == 'int': - template = '{}' - else: - template = '{:.8f}' - txt = whiptail.prompt(title, template.format(config.get(elem.key, elem.default))) - try: - if elem.type == "int": - val = int(txt) - else: - val = float(txt) - if val < elem.extra[0]: - whiptail.alert("The value is too low") - elif elem.extra[1] and val > elem.extra[1]: - whiptail.alert("the value is too high") - else: - break - except ValueError: - whiptail.alert("Not a valid value") - config[elem.key] = val - - if elem.type == "choice": - config[elem.key] = whiptail.radiolist(title, select_choice( - config.get(elem.key, elem.default), elem.extra)) - class ConfigValidator: """ validation methods borrowed from gui WorkerController for Cli @@ -76,23 +16,6 @@ def __init__(self, whiptail, bitshares_instance): self.bitshares = bitshares_instance or shared_bitshares_instance() self.whiptail = whiptail - @classmethod - def validate_worker_name(cls, worker_name, old_worker_name=None): - if old_worker_name != worker_name: - worker_names = Config().workers_data.keys() - # Check that the name is unique - if worker_name in worker_names: - return False - return True - return True - - def validate_asset(self, asset): - try: - Asset(asset, bitshares_instance=self.bitshares) - return True - except bitshares.exceptions.AssetDoesNotExistsException: - return False - def validate_account_name(self, account): if not account: return False @@ -146,7 +69,7 @@ def list_accounts(self): if len(account_list) == 0: account_list = [('none', 'none')] return account_list - + def add_account(self): # this method modeled off of worker_controller in gui account = self.whiptail.prompt("Your Account Name") @@ -166,3 +89,13 @@ def add_account(self): self.whiptail.alert("Private Key added successfully.") return account + + def del_account(self): + # Todo: implement in the cli_conf + account = self.whiptail.prompt("Account Name") + public_key = self.whiptail.prompt("Public Key", password=True) + wallet = self.bitshares.wallet + try: + wallet.removePrivateKeyFromPublicKey(public_key) + except Exception: + pass From 29725cbc036a690c590f2e274f3b39c645a38570 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 13 Mar 2019 15:08:06 +0500 Subject: [PATCH 1222/1846] Fix undefined variable --- dexbot/strategies/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 665d9f0e0..0bd341a21 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1321,7 +1321,7 @@ def get_order(order_id, return_none=True): try: order = Order(order_id) except Exception: - log.error('Got an exception getting order id {}'.format(order_id)) + logging.getLogger(__name__).error('Got an exception getting order id {}'.format(order_id)) raise if return_none and order['deleted']: return None From be94a98b2fdb3d1feb3017ea4ecf6e36ea917173 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 13 Mar 2019 15:46:54 +0500 Subject: [PATCH 1223/1846] Fix pep8 warnings --- dexbot/cli.py | 6 +++--- dexbot/cli_conf.py | 30 ++++++++++++++------------ dexbot/cli_helper.py | 18 ++++++---------- dexbot/strategies/base.py | 12 +++++------ dexbot/strategies/base_config.py | 26 +++++++++++----------- dexbot/strategies/relative_config.py | 16 +++++++------- dexbot/strategies/relative_orders.py | 6 +++--- dexbot/strategies/staggered_config.py | 5 +++-- dexbot/strategies/staggered_orders.py | 16 ++++++++------ dexbot/strategies/strategy_config.py | 7 +++--- dexbot/strategies/strategy_template.py | 13 +++++------ dexbot/ui.py | 6 +++--- 12 files changed, 80 insertions(+), 81 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index 92fb1c36d..a8d079ef6 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -22,7 +22,7 @@ # We need to do this before importing click if "LANG" not in os.environ: os.environ['LANG'] = 'C.UTF-8' -import click +import click # noqa: E402 log = logging.getLogger(__name__) @@ -94,7 +94,7 @@ def run(ctx): # TODO: reload config on SIGUSR1 # signal.signal(signal.SIGUSR1, lambda x, y: worker.do_next_tick(worker.reread_config)) except AttributeError: -# except ValueError: + # except ValueError: log.debug("Cannot set all signals -- not available on this platform") if ctx.obj['systemd']: try: @@ -129,7 +129,7 @@ def runservice(ctx): click.echo("Starting dexbot daemon") os.system("systemctl --user start dexbot") - + @main.command() @click.pass_context @configfile diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 60ff36b7b..3d2347ada 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -66,6 +66,7 @@ WantedBy=default.target """ + def dexbot_service_running(): """ Return True if dexbot service is running """ @@ -119,7 +120,7 @@ def get_strategy_tag(strategy_class): for strategy in STRATEGIES: if strategy_class == strategy['class']: return strategy['tag'] - return None + return None def select_choice(current, choices): @@ -221,7 +222,7 @@ def configure_worker(whiptail, worker_config, bitshares_instance): try: key = config_item[0] new_worker_config[key] = worker_config[key] - except KeyError as error: + except KeyError: # In case using old configuration file and there are new fields, this passes missing key pass @@ -243,8 +244,8 @@ def configure_worker(whiptail, worker_config, bitshares_instance): return # quit configuration if can't get WIF added else: worker_config[elem.key] = account_name - else: # account name only for edit worker - process_config_element(elem, whiptail, worker_config) + else: # account name only for edit worker + process_config_element(elem, whiptail, worker_config) else: whiptail.alert( "This worker type does not have configuration information. " @@ -258,7 +259,7 @@ def configure_dexbot(config, ctx): workers = config.get('workers', {}) bitshares_instance = ctx.bitshares validator = ConfigValidator(whiptail, bitshares_instance) - + if not workers: while True: txt = whiptail.prompt("Your name for the worker") @@ -287,21 +288,22 @@ def configure_dexbot(config, ctx): my_workers = [(index, index) for index in workers] if action == 'EXIT': - ## cancel will also exit the application. but this is a clearer label - ## Todo: modify cancel to be "Quit" or "Exit" for the whiptail menu item. + # cancel will also exit the application. but this is a clearer label + # Todo: modify cancel to be "Quit" or "Exit" for the whiptail menu item. break - elif action =='LIST': - # list workers, then provide option to list config of workers + elif action == 'LIST': + # list workers, then provide option to list config of workers worker_name = whiptail.menu("List of Your Workers. Select to view Configuration.", my_workers) content = config['workers'][worker_name] - worker_content = list(content.items()) + worker_content = list(content.items()) worker_list = [[str(i) for i in pairs] for pairs in worker_content] worker_list = [tuple(i) for i in worker_list] whiptail.menu(worker_name, worker_list) elif action == 'EDIT': worker_name = whiptail.menu("Select worker to edit", my_workers) - config['workers'][worker_name] = configure_worker(whiptail, config['workers'][worker_name], bitshares_instance) + config['workers'][worker_name] = configure_worker(whiptail, config['workers'][worker_name], + bitshares_instance) strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) strategy.clear_all_worker_data() @@ -318,13 +320,13 @@ def configure_dexbot(config, ctx): config['workers'][txt] = configure_worker(whiptail, {}, bitshares_instance) elif action == 'ADD': validator.add_account() - elif action =='SHOW': + elif action == 'SHOW': account_list = validator.list_accounts() action = whiptail.menu("Bitshares Account List (Name - Type)", account_list) elif action == 'ADD_NODE': txt = whiptail.prompt("Your name for the new node: e.g. wss://dexnode.net/ws") config['node'][0] = txt - ## overrides the top position + # overrides the top position elif action == 'NODES': choice = whiptail.node_radiolist( msg="Choose node", @@ -336,6 +338,6 @@ def configure_dexbot(config, ctx): setup_systemd(whiptail, config) elif action == 'HELP': whiptail.alert("Please see https://github.com/Codaone/DEXBot/wiki") - + whiptail.clear() return config diff --git a/dexbot/cli_helper.py b/dexbot/cli_helper.py index 4050b35ac..7ff91e1db 100644 --- a/dexbot/cli_helper.py +++ b/dexbot/cli_helper.py @@ -1,13 +1,10 @@ -from dexbot.whiptail import get_whiptail - import bitshares from bitshares.instance import shared_bitshares_instance -from bitshares.asset import Asset from bitshares.account import Account -from bitshares.exceptions import KeyAlreadyInStoreException, AccountDoesNotExistsException +from bitshares.exceptions import KeyAlreadyInStoreException from bitsharesbase.account import PrivateKey - + class ConfigValidator: """ validation methods borrowed from gui WorkerController for Cli """ @@ -62,11 +59,11 @@ def add_private_key(self, private_key): except KeyAlreadyInStoreException: # Private key already added pass - + def list_accounts(self): accounts = self.bitshares.wallet.getAccounts() account_list = [(i['name'], i['type']) for i in accounts] - if len(account_list) == 0: + if len(account_list) == 0: account_list = [('none', 'none')] return account_list @@ -74,7 +71,7 @@ def add_account(self): # this method modeled off of worker_controller in gui account = self.whiptail.prompt("Your Account Name") private_key = self.whiptail.prompt("Your Private Key", password=True) - + if not self.validate_account_name(account): self.whiptail.alert("Account name does not exist.") return False @@ -84,15 +81,14 @@ def add_account(self): if private_key and not self.validate_private_key_type(account, private_key): self.whiptail.alert("Please use active private key.") return False - + self.add_private_key(private_key) self.whiptail.alert("Private Key added successfully.") return account - def del_account(self): # Todo: implement in the cli_conf - account = self.whiptail.prompt("Account Name") + # account = self.whiptail.prompt("Account Name") public_key = self.whiptail.prompt("Public Key", password=True) wallet = self.bitshares.wallet try: diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 0bd341a21..e29586e67 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1,6 +1,5 @@ import datetime import copy -import collections import logging import math import time @@ -27,6 +26,7 @@ # Number of maximum retries used to retry action before failing MAX_TRIES = 3 + class StrategyBase(Storage, StateMachine, Events): """ A strategy based on this class is intended to work in one market. This class contains most common methods needed by the strategy. @@ -65,16 +65,15 @@ class StrategyBase(Storage, StateMachine, Events): 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. """ - + @classmethod def configure(cls, return_base_config=True): return BaseConfig.configure(return_base_config) - + @classmethod def configure_details(cls, include_default_tabs=True): return BaseConfig.configure_details(include_default_tabs) - __events__ = [ 'onAccount', 'onMarketUpdate', @@ -87,7 +86,6 @@ def configure_details(cls, include_default_tabs=True): 'error_ontick', ] - def __init__(self, name, config=None, @@ -543,7 +541,7 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors self.log.critical("Cannot estimate center price, there is no highest bid.") self.disabled = True return None - + if sell_price is None or sell_price == 0.0: if not suppress_errors: self.log.critical("Cannot estimate center price, there is no lowest ask.") @@ -551,7 +549,7 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors return None # Calculate and return market center price. make sure buy_price has value if buy_price: - center_price = buy_price * math.sqrt(sell_price / buy_price) + center_price = buy_price * math.sqrt(sell_price / buy_price) self.log.debug('Center price in get_market_center_price: {:.8f} '.format(center_price)) return center_price diff --git a/dexbot/strategies/base_config.py b/dexbot/strategies/base_config.py index 941648ddf..4d16a85be 100644 --- a/dexbot/strategies/base_config.py +++ b/dexbot/strategies/base_config.py @@ -1,8 +1,8 @@ import collections -""" Strategies need to specify their own configuration values, so each strategy can have a class method 'configure' +""" Strategies need to specify their own configuration values, so each strategy can have a class method 'configure' which returns a list of ConfigElement named tuples. - + Tuple fields as follows: - Key: The key in the bot config dictionary that gets saved back to config.yml - Type: "int", "float", "bool", "string" or "choice" @@ -19,36 +19,36 @@ """ ConfigElement = collections.namedtuple('ConfigElement', 'key type default title description extra') -""" Strategies have different needs for the details they want to show for the user. These elements help to build a - custom details window for the strategy. +""" Strategies have different needs for the details they want to show for the user. These elements help to build a + custom details window for the strategy. Tuple fields as follows: - Type: 'graph', 'text', 'table' - Name: The name of the tab, shows at the top - Title: The title is shown inside the tab - File: Tabs can also show data from files, pass on the file name including the file extension - in strategy's `configure_details`. - + in strategy's `configure_details`. + Below folders and representative file types that inside the folders. - + Location File extensions --------------------------- dexbot/graphs .png, .jpg dexbot/data .csv dexbot/logs .log, .txt (.csv, will print as raw data) - - NOTE: To avoid conflicts with other custom strategies, when generating names for files, use slug or worker's + + NOTE: To avoid conflicts with other custom strategies, when generating names for files, use slug or worker's name when generating files or create custom folders. Add relative path to 'file' parameter if file is in custom folder inside default folders. Like shown below: - + `DetailElement('log', 'Worker log', 'Log of worker's actions', 'my_custom_folder/example_worker.log')` """ DetailElement = collections.namedtuple('DetailTab', 'type name title file') -class BaseConfig(): +class BaseConfig(): - @classmethod + @classmethod def configure(cls, return_base_config=True): """ Return a list of ConfigElement objects defining the configuration values for this class. @@ -78,7 +78,7 @@ def configure(cls, return_base_config=True): if return_base_config: return base_config return [] - + @classmethod def configure_details(cls, include_default_tabs=True): """ Return a list of ConfigElement objects defining the configuration values for this class. diff --git a/dexbot/strategies/relative_config.py b/dexbot/strategies/relative_config.py index 59be2807c..33cd72fb7 100644 --- a/dexbot/strategies/relative_config.py +++ b/dexbot/strategies/relative_config.py @@ -1,8 +1,9 @@ -from dexbot.strategies.base_config import BaseConfig, ConfigElement, DetailElement +from dexbot.strategies.base_config import BaseConfig, ConfigElement + class RelativeConfig(BaseConfig): - @classmethod + @classmethod def configure(cls, return_base_config=True): """ Return a list of ConfigElement objects defining the configuration values for this class. @@ -25,8 +26,8 @@ def configure(cls, return_base_config=True): ('gdax', 'Gdax'), ('binance', 'Binance') ] - - relative_orders_config = [ + + relative_orders_config = [ ConfigElement('external_feed', 'bool', False, 'External price feed', 'Use external reference price instead of center price acquired from the market', None), ConfigElement('external_price_source', 'choice', 'gecko', 'External price source', @@ -73,22 +74,21 @@ def configure(cls, return_base_config=True): 'Define custom order expiration time to force orders reset more often, seconds', (30, 157680000, '')) ] - + if return_base_config: return BaseConfig.configure(return_base_config) + relative_orders_config return [] - @classmethod def configure_details(cls, include_default_tabs=True): """ Return a list of ConfigElement objects defining the configuration values for this class. - + User interfaces should then generate widgets based on these values, gather data and save back to the config dictionary for the worker. NOTE: When overriding you almost certainly will want to call the ancestor and then add your config values to the list. - + :param include_default_tabs: bool: :return: Returns a list of Detail elements """ diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 154fc93e0..947530e24 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -3,12 +3,12 @@ from dexbot.strategies.base import StrategyBase from dexbot.strategies.relative_config import RelativeConfig -from dexbot.qt_queue.idle_queue import idle_add + class Strategy(StrategyBase): """ Relative Orders strategy """ - + @classmethod def configure(cls, return_base_config=True): return RelativeConfig.configure(return_base_config) @@ -60,7 +60,7 @@ def __init__(self, *args, **kwargs): else: # Use manually set center price self.center_price = self.worker["center_price"] - + self.is_relative_order_size = self.worker.get('relative_order_size', False) self.is_asset_offset = self.worker.get('center_price_offset', False) self.manual_offset = self.worker.get('manual_offset', 0) / 100 diff --git a/dexbot/strategies/staggered_config.py b/dexbot/strategies/staggered_config.py index 2f0cfef7f..d9d4bca70 100644 --- a/dexbot/strategies/staggered_config.py +++ b/dexbot/strategies/staggered_config.py @@ -1,7 +1,8 @@ -from dexbot.strategies.base_config import BaseConfig, ConfigElement, DetailElement +from dexbot.strategies.base_config import BaseConfig, ConfigElement + class StaggeredConfig(BaseConfig): - + @classmethod def configure(cls, return_base_config=True): """ Modes description: diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index ad9b172e4..3a8e17388 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -11,6 +11,7 @@ from dexbot.strategies.base import StrategyBase from dexbot.strategies.staggered_config import StaggeredConfig + class Strategy(StrategyBase): """ Staggered Orders strategy """ @@ -21,7 +22,7 @@ def configure(cls, return_base_config=True): @classmethod def configure_details(cls, include_default_tabs=True): return StaggeredConfig.configure_details(include_default_tabs) - + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -550,7 +551,7 @@ def replace_virtual_order_with_real(self, order): self.log.info('Replacing virtual buy order with real order') try: new_order = self.place_market_buy_order(quote_amount, price, returnOrderId=True) - except bitsharesapi.exceptions.RPCError as e: + except bitsharesapi.exceptions.RPCError: self.log.exception('Error broadcasting trx:') return False else: @@ -559,7 +560,7 @@ def replace_virtual_order_with_real(self, order): self.log.info('Replacing virtual sell order with real order') try: new_order = self.place_market_sell_order(quote_amount, price, returnOrderId=True) - except bitsharesapi.exceptions.RPCError as e: + except bitsharesapi.exceptions.RPCError: self.log.exception('Error broadcasting trx:') return False @@ -869,8 +870,9 @@ def increase_single_order(asset, order, new_order_amount): order_type = 'sell' price = (order['price'] ** -1) # New order amount must be at least x2 precision bigger - new_order_amount = max(new_order_amount, - order['base']['amount'] + 2 * 10 ** -self.market['quote']['precision']) + new_order_amount = max( + new_order_amount, order['base']['amount'] + 2 * 10 ** -self.market['quote']['precision'] + ) quote_amount = new_order_amount elif asset == 'base': order_type = 'buy' @@ -1062,7 +1064,7 @@ def increase_single_order(asset, order, new_order_amount): else: """ Special processing for the closest order. - Calculate new order amount based on orders count, but do not allow to perform too small + Calculate new order amount based on orders count, but do not allow to perform too small increase rounds. New lowest buy / highest sell should be higher by at least one increment. """ closer_order_bound = closest_order_bound @@ -1103,7 +1105,7 @@ def increase_single_order(asset, order, new_order_amount): elif (order_amount_normalized < closer_order_bound and closer_order_bound - order_amount >= order_amount * self.increment / 2): """ Check whether order amount is less than closer or order and the diff is more than 50% of one - increment. Note: we can use only 50% or less diffs. Bigger will not work. For example, with + increment. Note: we can use only 50% or less diffs. Bigger will not work. For example, with diff 80% an order may have an actual difference like 30% from closer and 70% from further. """ new_order_amount = closer_order_bound diff --git a/dexbot/strategies/strategy_config.py b/dexbot/strategies/strategy_config.py index 323b9c0c0..ae3ae1bdc 100644 --- a/dexbot/strategies/strategy_config.py +++ b/dexbot/strategies/strategy_config.py @@ -1,5 +1,6 @@ from dexbot.strategies.base_config import BaseConfig, ConfigElement, DetailElement + class StrategyConfig(BaseConfig): """ this is the configuration template for the strategy_template class """ @@ -7,12 +8,12 @@ class StrategyConfig(BaseConfig): @classmethod def configure(cls, return_base_config=True): """ This function is used to auto generate fields for GUI - + :param return_base_config: If base config is used in addition to this configuration. :return: List of ConfigElement(s) """ - """ As a demonstration this template has two fields in the worker configuration. Upper and lower bound. + """ As a demonstration this template has two fields in the worker configuration. Upper and lower bound. Documentation of ConfigElements can be found from base.py. """ return BaseConfig.configure(return_base_config) + [ @@ -24,7 +25,6 @@ def configure(cls, return_base_config=True): (0, 10000000, 8, '')), ] - @classmethod def configure_details(cls, include_default_tabs=True): """ This function defines the tabs for detailed view of the worker. Further documentation is found in base.py @@ -39,4 +39,3 @@ def configure_details(cls, include_default_tabs=True): DetailElement('table', 'Orders', 'Data from csv file', 'example.csv'), DetailElement('text', 'Log', 'Log data', 'example.log') ] - diff --git a/dexbot/strategies/strategy_template.py b/dexbot/strategies/strategy_template.py index 99bb464ee..0f4514a4b 100644 --- a/dexbot/strategies/strategy_template.py +++ b/dexbot/strategies/strategy_template.py @@ -1,5 +1,5 @@ # Python imports -import math +# import math # Project imports from dexbot.strategies.base import StrategyBase @@ -7,10 +7,11 @@ from dexbot.qt_queue.idle_queue import idle_add # Third party imports -from bitshares.market import Market +# from bitshares.market import Market STRATEGY_NAME = 'Strategy Template' + class Strategy(StrategyBase): """ @@ -47,7 +48,7 @@ def configure(cls, return_base_config=True): @classmethod def configure_details(cls, include_default_tabs=True): return StrategyConfig.configure_details(return_base_config) - + def __init__(self, *args, **kwargs): # Initializes StrategyBase class super().__init__(*args, **kwargs) @@ -73,14 +74,14 @@ def __init__(self, *args, **kwargs): - Market has been updated = self.onMarketUpdate These events are tied to methods which decide how the loop goes, unless the strategy is static, which - means that it will only do one thing and never do + means that it will only do one thing and never do """ # Get view self.view = kwargs.get('view') """ Worker parameters - + There values are taken from the worker's config file. Name of the worker is passed in the **kwargs. """ @@ -90,7 +91,7 @@ def __init__(self, *args, **kwargs): self.lower_bound = self.worker.get('lower_bound') """ Strategy variables - + These variables are for the strategy only and should be initialized here if wanted into self's scope. """ self.market_center_price = 0 diff --git a/dexbot/ui.py b/dexbot/ui.py index c04bfee8e..4658ef267 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -105,7 +105,7 @@ def new_func(ctx, *args, **kwargs): "Current Uptick Wallet Passphrase", hide_input=True) try: ctx.bitshares.wallet.unlock(pwd) - except Exception as exception: + except Exception: log.critical("Password error, exiting") sys.exit(78) else: @@ -113,14 +113,14 @@ def new_func(ctx, *args, **kwargs): # No user available to interact with log.critical("Uptick Wallet not installed, cannot run") sys.exit(78) - click.echo("No Uptick wallet installed yet. \n" + + click.echo("No Uptick wallet installed yet. \n" + "This is a password for encrypting " + "the file that contains your private keys. Creating ...") pwd = click.prompt( "Uptick Wallet Encryption Passphrase", hide_input=True, confirmation_prompt=True) - ctx.bitshares.wallet.create(pwd) + ctx.bitshares.wallet.create(pwd) return ctx.invoke(f, *args, **kwargs) return update_wrapper(new_func, f) From 62247ecc7d000b7f0b014cb040ff9a466467effb Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 13 Mar 2019 16:04:04 +0500 Subject: [PATCH 1224/1846] Fix variable name --- dexbot/strategies/strategy_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/strategy_template.py b/dexbot/strategies/strategy_template.py index 0f4514a4b..be3f49ba7 100644 --- a/dexbot/strategies/strategy_template.py +++ b/dexbot/strategies/strategy_template.py @@ -47,7 +47,7 @@ def configure(cls, return_base_config=True): @classmethod def configure_details(cls, include_default_tabs=True): - return StrategyConfig.configure_details(return_base_config) + return StrategyConfig.configure_details(include_default_tabs) def __init__(self, *args, **kwargs): # Initializes StrategyBase class From 70efefd7437a86ce5fbddbb1ae6a74a3472cb6dd Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 13 Mar 2019 16:08:13 +0500 Subject: [PATCH 1225/1846] Restore functions ordering inside file --- dexbot/cli_conf.py | 112 ++++++++++++++++++++++----------------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 3d2347ada..40437a7d8 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -67,62 +67,6 @@ """ -def dexbot_service_running(): - """ Return True if dexbot service is running - """ - cmd = 'systemctl --user status dexbot' - output = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) - for line in output.stdout.readlines(): - if b'Active:' in line and b'(running)' in line: - return True - return False - - -def setup_systemd(whiptail, config): - if not os.path.exists("/etc/systemd"): - return # No working systemd - - if not whiptail.confirm( - "Do you want to run dexbot as a background (daemon) process?"): - config['systemd_status'] = 'disabled' - return - - redo_setup = False - if os.path.exists(SYSTEMD_SERVICE_NAME): - redo_setup = whiptail.confirm('Redo systemd setup?', 'no') - - if not os.path.exists(SYSTEMD_SERVICE_NAME) or redo_setup: - path = '~/.local/share/systemd/user' - path = os.path.expanduser(path) - pathlib.Path(path).mkdir(parents=True, exist_ok=True) - password = whiptail.prompt( - "The uptick wallet password\n" - "NOTE: this will be saved on disc so the worker can run unattended. " - "This means anyone with access to this computer's files can spend all your money", - password=True) - - # Because we hold password be restrictive - fd = os.open(SYSTEMD_SERVICE_NAME, os.O_WRONLY | os.O_CREAT, 0o600) - with open(fd, "w") as fp: - fp.write( - SYSTEMD_SERVICE_FILE.format( - exe=sys.argv[0], - passwd=password, - homedir=os.path.expanduser("~"))) - # The dexbot service file was edited, reload the daemon configs - os.system('systemctl --user daemon-reload') - - # Signal cli.py to set the unit up after writing config file - config['systemd_status'] = 'enabled' - - -def get_strategy_tag(strategy_class): - for strategy in STRATEGIES: - if strategy_class == strategy['class']: - return strategy['tag'] - return None - - def select_choice(current, choices): """ For the radiolist, get us a list with the current value selected """ return [(tag, text, (current == tag and "ON") or "OFF") @@ -181,6 +125,62 @@ def process_config_element(elem, whiptail, config): config.get(elem.key, elem.default), elem.extra)) +def dexbot_service_running(): + """ Return True if dexbot service is running + """ + cmd = 'systemctl --user status dexbot' + output = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) + for line in output.stdout.readlines(): + if b'Active:' in line and b'(running)' in line: + return True + return False + + +def setup_systemd(whiptail, config): + if not os.path.exists("/etc/systemd"): + return # No working systemd + + if not whiptail.confirm( + "Do you want to run dexbot as a background (daemon) process?"): + config['systemd_status'] = 'disabled' + return + + redo_setup = False + if os.path.exists(SYSTEMD_SERVICE_NAME): + redo_setup = whiptail.confirm('Redo systemd setup?', 'no') + + if not os.path.exists(SYSTEMD_SERVICE_NAME) or redo_setup: + path = '~/.local/share/systemd/user' + path = os.path.expanduser(path) + pathlib.Path(path).mkdir(parents=True, exist_ok=True) + password = whiptail.prompt( + "The uptick wallet password\n" + "NOTE: this will be saved on disc so the worker can run unattended. " + "This means anyone with access to this computer's files can spend all your money", + password=True) + + # Because we hold password be restrictive + fd = os.open(SYSTEMD_SERVICE_NAME, os.O_WRONLY | os.O_CREAT, 0o600) + with open(fd, "w") as fp: + fp.write( + SYSTEMD_SERVICE_FILE.format( + exe=sys.argv[0], + passwd=password, + homedir=os.path.expanduser("~"))) + # The dexbot service file was edited, reload the daemon configs + os.system('systemctl --user daemon-reload') + + # Signal cli.py to set the unit up after writing config file + config['systemd_status'] = 'enabled' + + +def get_strategy_tag(strategy_class): + for strategy in STRATEGIES: + if strategy_class == strategy['class']: + return strategy['tag'] + return None + + def configure_worker(whiptail, worker_config, bitshares_instance): # By default always editing editing = True From 12cb40fd612bd6cdbd89d8eb78ffeb16d1816e71 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 13 Mar 2019 22:44:45 +0500 Subject: [PATCH 1226/1846] Add docstrings --- dexbot/cli_conf.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 40437a7d8..d21c5f62d 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -74,9 +74,11 @@ def select_choice(current, choices): def process_config_element(elem, whiptail, config): - """ Process an item of configuration metadata display a widget as appropriate - d: the Dialog object - config: the config dictionary for this worker + """ Process an item of configuration metadata, display a widget as appropriate + + :param base_config.ConfigElement elem: config element + :param whiptail.Whiptail whiptail: instance of Whiptail or NoWhiptail + :param collections.OrderedDict config: the config dictionary for this worker """ if elem.description: title = '{} - {}'.format(elem.title, elem.description) @@ -137,6 +139,11 @@ def dexbot_service_running(): def setup_systemd(whiptail, config): + """ Setup systemd unit to auto-start dexbot + + :param whiptail.Whiptail whiptail: instance of Whiptail or NoWhiptail + :param dexbot.config.Config config: dexbot config + """ if not os.path.exists("/etc/systemd"): return # No working systemd @@ -175,6 +182,12 @@ def setup_systemd(whiptail, config): def get_strategy_tag(strategy_class): + """ Obtain tag for a strategy + + :param str strategy_class: strategy class name, example: dexbot.strategies.foo_bar + + It may seems that tags may be common accross strategies, but it is not. Every strategy must use unique tag. + """ for strategy in STRATEGIES: if strategy_class == strategy['class']: return strategy['tag'] @@ -182,6 +195,12 @@ def get_strategy_tag(strategy_class): def configure_worker(whiptail, worker_config, bitshares_instance): + """ Single worker configurator + + :param whiptail.Whiptail whiptail: instance of Whiptail or NoWhiptail + :param collections.OrderedDict worker_config: the config dictionary for this worker + :param bitshares.BitShares bitshares_instance: an instance of BitShares class + """ # By default always editing editing = True @@ -255,6 +274,9 @@ def configure_worker(whiptail, worker_config, bitshares_instance): def configure_dexbot(config, ctx): + """ Main `cli configure` entrypoint + :param dexbot.config.Config config: dexbot config + """ whiptail = get_whiptail('DEXBot configure') workers = config.get('workers', {}) bitshares_instance = ctx.bitshares From ea89c0e36dee78522c79ac73ec86b436f4922d51 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 13 Mar 2019 22:47:43 +0500 Subject: [PATCH 1227/1846] Change variable name to avoid confusion --- dexbot/cli_conf.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index d21c5f62d..33e7a370c 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -73,12 +73,12 @@ def select_choice(current, choices): for tag, text in choices] -def process_config_element(elem, whiptail, config): +def process_config_element(elem, whiptail, worker_config): """ Process an item of configuration metadata, display a widget as appropriate :param base_config.ConfigElement elem: config element :param whiptail.Whiptail whiptail: instance of Whiptail or NoWhiptail - :param collections.OrderedDict config: the config dictionary for this worker + :param collections.OrderedDict worker_config: the config dictionary for this worker """ if elem.description: title = '{} - {}'.format(elem.title, elem.description) @@ -86,19 +86,19 @@ def process_config_element(elem, whiptail, config): title = elem.title if elem.type == "string": - txt = whiptail.prompt(title, config.get(elem.key, elem.default)) + txt = whiptail.prompt(title, worker_config.get(elem.key, elem.default)) if elem.extra: while not re.match(elem.extra, txt): whiptail.alert("The value is not valid") txt = whiptail.prompt( - title, config.get( + title, worker_config.get( elem.key, elem.default)) - config[elem.key] = txt + worker_config[elem.key] = txt if elem.type == "bool": - value = config.get(elem.key, elem.default) + value = worker_config.get(elem.key, elem.default) value = 'yes' if value else 'no' - config[elem.key] = whiptail.confirm(title, value) + worker_config[elem.key] = whiptail.confirm(title, value) if elem.type in ("float", "int"): while True: @@ -106,7 +106,7 @@ def process_config_element(elem, whiptail, config): template = '{}' else: template = '{:.8f}' - txt = whiptail.prompt(title, template.format(config.get(elem.key, elem.default))) + txt = whiptail.prompt(title, template.format(worker_config.get(elem.key, elem.default))) try: if elem.type == "int": val = int(txt) @@ -120,11 +120,11 @@ def process_config_element(elem, whiptail, config): break except ValueError: whiptail.alert("Not a valid value") - config[elem.key] = val + worker_config[elem.key] = val if elem.type == "choice": - config[elem.key] = whiptail.radiolist(title, select_choice( - config.get(elem.key, elem.default), elem.extra)) + worker_config[elem.key] = whiptail.radiolist(title, select_choice( + worker_config.get(elem.key, elem.default), elem.extra)) def dexbot_service_running(): From affa50528547f05f6e42a281136a07ec38d62793 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 13 Mar 2019 22:49:12 +0500 Subject: [PATCH 1228/1846] Rename variable "elem" -> "element" Full variable name looks cleaner. --- dexbot/cli_conf.py | 48 +++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 33e7a370c..1e16d14ab 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -73,58 +73,58 @@ def select_choice(current, choices): for tag, text in choices] -def process_config_element(elem, whiptail, worker_config): +def process_config_element(element, whiptail, worker_config): """ Process an item of configuration metadata, display a widget as appropriate - :param base_config.ConfigElement elem: config element + :param base_config.ConfigElement element: config element :param whiptail.Whiptail whiptail: instance of Whiptail or NoWhiptail :param collections.OrderedDict worker_config: the config dictionary for this worker """ - if elem.description: - title = '{} - {}'.format(elem.title, elem.description) + if element.description: + title = '{} - {}'.format(element.title, element.description) else: - title = elem.title + title = element.title - if elem.type == "string": - txt = whiptail.prompt(title, worker_config.get(elem.key, elem.default)) - if elem.extra: - while not re.match(elem.extra, txt): + if element.type == "string": + txt = whiptail.prompt(title, worker_config.get(element.key, element.default)) + if element.extra: + while not re.match(element.extra, txt): whiptail.alert("The value is not valid") txt = whiptail.prompt( title, worker_config.get( - elem.key, elem.default)) - worker_config[elem.key] = txt + element.key, element.default)) + worker_config[element.key] = txt - if elem.type == "bool": - value = worker_config.get(elem.key, elem.default) + if element.type == "bool": + value = worker_config.get(element.key, element.default) value = 'yes' if value else 'no' - worker_config[elem.key] = whiptail.confirm(title, value) + worker_config[element.key] = whiptail.confirm(title, value) - if elem.type in ("float", "int"): + if element.type in ("float", "int"): while True: - if elem.type == 'int': + if element.type == 'int': template = '{}' else: template = '{:.8f}' - txt = whiptail.prompt(title, template.format(worker_config.get(elem.key, elem.default))) + txt = whiptail.prompt(title, template.format(worker_config.get(element.key, element.default))) try: - if elem.type == "int": + if element.type == "int": val = int(txt) else: val = float(txt) - if val < elem.extra[0]: + if val < element.extra[0]: whiptail.alert("The value is too low") - elif elem.extra[1] and val > elem.extra[1]: + elif element.extra[1] and val > element.extra[1]: whiptail.alert("the value is too high") else: break except ValueError: whiptail.alert("Not a valid value") - worker_config[elem.key] = val + worker_config[element.key] = val - if elem.type == "choice": - worker_config[elem.key] = whiptail.radiolist(title, select_choice( - worker_config.get(elem.key, elem.default), elem.extra)) + if element.type == "choice": + worker_config[element.key] = whiptail.radiolist(title, select_choice( + worker_config.get(element.key, element.default), element.extra)) def dexbot_service_running(): From 4109c19a8bda30ad104516e0892dcb6b56a630ce Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 13 Mar 2019 23:06:44 +0500 Subject: [PATCH 1229/1846] Catch more precise exception General rule is to avoid catching all exceptions, it's better to raise an unexpected exception. --- dexbot/ui.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dexbot/ui.py b/dexbot/ui.py index 4658ef267..46c38cb5a 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -8,6 +8,7 @@ from ruamel import yaml from bitshares import BitShares from bitshares.instance import set_shared_bitshares_instance +from bitshares.exceptions import WrongMasterPasswordException from dexbot.config import Config @@ -105,7 +106,7 @@ def new_func(ctx, *args, **kwargs): "Current Uptick Wallet Passphrase", hide_input=True) try: ctx.bitshares.wallet.unlock(pwd) - except Exception: + except WrongMasterPasswordException: log.critical("Password error, exiting") sys.exit(78) else: From 3b086911d876c6050ddcc24be4d84eace0727679 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 14 Mar 2019 12:14:30 +0500 Subject: [PATCH 1230/1846] Add --logfile arg example --- dexbot/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index 709e99de4..fbf9c2632 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -48,7 +48,7 @@ @click.option( '--logfile', default=None, - help='Override logfile location' + help='Override logfile location (example: ~/dexbot.log)' ) @click.option( '--verbose', From 4eb0d993f08212a6135be881988ca8f578d9b472 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 14 Mar 2019 12:21:45 +0500 Subject: [PATCH 1231/1846] Handle both AttributeError and ValueError Different python versions may raise one or another, so keep both. --- dexbot/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index a8d079ef6..2b50ec800 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -89,12 +89,12 @@ def run(ctx): signal.signal(signal.SIGTERM, kill_workers) signal.signal(signal.SIGINT, kill_workers) try: - # These signals are UNIX-only territory, will ValueError here on Windows + # These signals are UNIX-only territory, will ValueError or AttributeError here on Windows (depending on + # python version) 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 AttributeError: - # except ValueError: + except (ValueError, AttributeError): log.debug("Cannot set all signals -- not available on this platform") if ctx.obj['systemd']: try: From 438e0e19b2f4d6da4c726db1dc240ebed9798307 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 14 Mar 2019 12:31:06 +0500 Subject: [PATCH 1232/1846] Small fix of docstrings formatting --- dexbot/cli_conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 1e16d14ab..66190e159 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -68,7 +68,8 @@ def select_choice(current, choices): - """ For the radiolist, get us a list with the current value selected """ + """ For the radiolist, get us a list with the current value selected + """ return [(tag, text, (current == tag and "ON") or "OFF") for tag, text in choices] @@ -275,6 +276,7 @@ def configure_worker(whiptail, worker_config, bitshares_instance): def configure_dexbot(config, ctx): """ Main `cli configure` entrypoint + :param dexbot.config.Config config: dexbot config """ whiptail = get_whiptail('DEXBot configure') From d632f858a142890f3c650ae21ff4ad8c66239e88 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 14 Mar 2019 12:43:38 +0500 Subject: [PATCH 1233/1846] Rename dexbot/cli_helper.py -> dexbot/config_validator.py --- dexbot/{cli_helper.py => config_validator.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename dexbot/{cli_helper.py => config_validator.py} (100%) diff --git a/dexbot/cli_helper.py b/dexbot/config_validator.py similarity index 100% rename from dexbot/cli_helper.py rename to dexbot/config_validator.py From c2545a0ca2b02cbd612c3a84199e7cafb350e2e0 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 14 Mar 2019 14:18:12 +0500 Subject: [PATCH 1234/1846] Refactor config validation Cli and GUI needs the same validation when configuring a worker, so move all validation code into single place dexbot/config_validator.py. Refactor cli configure and WorkerContoller to use ConfigValidator methods. Cli-specific methods of ConfigValidator moved back to cli_conf.py. --- dexbot/cli_conf.py | 59 ++++++++++-- dexbot/config_validator.py | 115 +++++++++++++++--------- dexbot/controllers/worker_controller.py | 104 +++------------------ 3 files changed, 139 insertions(+), 139 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 66190e159..16fd068dd 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -23,7 +23,7 @@ from dexbot.whiptail import get_whiptail from dexbot.strategies.base import StrategyBase -from dexbot.cli_helper import ConfigValidator +from dexbot.config_validator import ConfigValidator import dexbot.helper @@ -252,14 +252,13 @@ def configure_worker(whiptail, worker_config, bitshares_instance): # Use class metadata for per-worker configuration config_elems = strategy_class.configure() - validator = ConfigValidator(whiptail, bitshares_instance) if config_elems: # Strategy options for elem in config_elems: if not editing and (elem.key == "account"): # only allow WIF addition for new workers - account_name = validator.add_account() + account_name = add_account(whiptail, bitshares_instance) if account_name is False: return # quit configuration if can't get WIF added else: @@ -282,7 +281,6 @@ def configure_dexbot(config, ctx): whiptail = get_whiptail('DEXBot configure') workers = config.get('workers', {}) bitshares_instance = ctx.bitshares - validator = ConfigValidator(whiptail, bitshares_instance) if not workers: while True: @@ -343,9 +341,9 @@ def configure_dexbot(config, ctx): else: config['workers'][txt] = configure_worker(whiptail, {}, bitshares_instance) elif action == 'ADD': - validator.add_account() + add_account(whiptail, bitshares_instance) elif action == 'SHOW': - account_list = validator.list_accounts() + account_list = list_accounts(bitshares_instance) action = whiptail.menu("Bitshares Account List (Name - Type)", account_list) elif action == 'ADD_NODE': txt = whiptail.prompt("Your name for the new node: e.g. wss://dexnode.net/ws") @@ -365,3 +363,52 @@ def configure_dexbot(config, ctx): whiptail.clear() return config + + +def add_account(whiptail, bitshares_instance): + """ "Add account" dialog + + :param whiptail.Whiptail whiptail: instance of Whiptail or NoWhiptail + :param bitshares.BitShares bitshares_instance: an instance of BitShares class + """ + validator = ConfigValidator(bitshares_instance) + + account = whiptail.prompt("Your Account Name") + private_key = whiptail.prompt("Your Private Key", password=True) + + if not validator.validate_account_name(account): + whiptail.alert("Account name does not exist.") + return False + if not validator.validate_private_key(account, private_key): + whiptail.alert("Private key is invalid") + return False + if private_key and not validator.validate_private_key_type(account, private_key): + whiptail.alert("Please use active private key.") + return False + + validator.add_private_key(private_key) + whiptail.alert("Private Key added successfully.") + return account + + +def del_account(self): + # Todo: implement in the cli_conf + # account = self.whiptail.prompt("Account Name") + public_key = self.whiptail.prompt("Public Key", password=True) + wallet = self.bitshares.wallet + try: + wallet.removePrivateKeyFromPublicKey(public_key) + except Exception: + pass + + +def list_accounts(bitshares_instance): + """ Get all accounts installed in local wallet + + :return: list of tuples ('account_name', 'key_type') + """ + accounts = bitshares_instance.wallet.getAccounts() + account_list = [(i['name'], i['type']) for i in accounts] + if len(account_list) == 0: + account_list = [('none', 'none')] + return account_list diff --git a/dexbot/config_validator.py b/dexbot/config_validator.py index 7ff91e1db..f411f30b7 100644 --- a/dexbot/config_validator.py +++ b/dexbot/config_validator.py @@ -1,31 +1,43 @@ -import bitshares +from dexbot.config import Config + from bitshares.instance import shared_bitshares_instance from bitshares.account import Account -from bitshares.exceptions import KeyAlreadyInStoreException +from bitshares.asset import Asset +from bitshares.exceptions import KeyAlreadyInStoreException, AccountDoesNotExistsException, AssetDoesNotExistsException from bitsharesbase.account import PrivateKey class ConfigValidator: - """ validation methods borrowed from gui WorkerController for Cli + """ Config validation methods + + :param bitshares.BitShares: BitShares instance """ - def __init__(self, whiptail, bitshares_instance): + def __init__(self, bitshares_instance): self.bitshares = bitshares_instance or shared_bitshares_instance() - self.whiptail = whiptail def validate_account_name(self, account): + """ Check whether bitshares account exists + + :param str account: bitshares account name + """ if not account: return False try: Account(account, bitshares_instance=self.bitshares) return True - except bitshares.exceptions.AccountDoesNotExistsException: + except AccountDoesNotExistsException: return False def validate_private_key(self, account, private_key): + """ Check whether private key is associated with account + + :param str account: bitshares account name + :param str private_key: private key + """ wallet = self.bitshares.wallet if not private_key: - # Check if the private key is already in the database + # Check if the account is already in the database accounts = wallet.getAccounts() if any(account == d['name'] for d in accounts): return True @@ -36,6 +48,7 @@ def validate_private_key(self, account, private_key): except ValueError: return False + # Load all accounts with corresponding public key from the blockchain accounts = wallet.getAllAccounts(pubkey) account_names = [account['name'] for account in accounts] @@ -45,6 +58,11 @@ def validate_private_key(self, account, private_key): return False def validate_private_key_type(self, account, private_key): + """ Check whether private key type is "active" or "owner" + + :param str account: bitshares account name + :param str private_key: private key + """ account = Account(account) pubkey = format(PrivateKey(private_key).pubkey, self.bitshares.prefix) key_type = self.bitshares.wallet.getKeyType(account, pubkey) @@ -52,46 +70,61 @@ def validate_private_key_type(self, account, private_key): return False return True - def add_private_key(self, private_key): - wallet = self.bitshares.wallet - try: - wallet.addPrivateKey(private_key) - except KeyAlreadyInStoreException: - # Private key already added - pass + @classmethod + def validate_worker_name(cls, worker_name, old_worker_name=None): + """ Check whether worker name is unique or not - def list_accounts(self): - accounts = self.bitshares.wallet.getAccounts() - account_list = [(i['name'], i['type']) for i in accounts] - if len(account_list) == 0: - account_list = [('none', 'none')] - return account_list + :param str worker_name: name of the new worker + :param str old_worker_name: old name of the worker + """ + if old_worker_name != worker_name: + worker_names = Config().workers_data.keys() + # Check that the name is unique + if worker_name in worker_names: + return False + return True + return True - def add_account(self): - # this method modeled off of worker_controller in gui - account = self.whiptail.prompt("Your Account Name") - private_key = self.whiptail.prompt("Your Private Key", password=True) + @classmethod + def validate_account_not_in_use(cls, account): + """ Check whether account is already used for another worker or not - if not self.validate_account_name(account): - self.whiptail.alert("Account name does not exist.") - return False - if not self.validate_private_key(account, private_key): - self.whiptail.alert("Private key is invalid") - return False - if private_key and not self.validate_private_key_type(account, private_key): - self.whiptail.alert("Please use active private key.") + :param str account: bitshares account name + """ + workers = Config().workers_data + for worker_name, worker in workers.items(): + if worker['account'] == account: + return False + return True + + def validate_asset(self, asset): + """ Check whether asset is exists on the network + + :param str asset: asset name + """ + try: + Asset(asset, bitshares_instance=self.bitshares) + return True + except AssetDoesNotExistsException: return False - self.add_private_key(private_key) - self.whiptail.alert("Private Key added successfully.") - return account + @classmethod + def validate_market(cls, base_asset, quote_asset): + """ Check whether market tickers is not the same - def del_account(self): - # Todo: implement in the cli_conf - # account = self.whiptail.prompt("Account Name") - public_key = self.whiptail.prompt("Public Key", password=True) + :param str base_asset: BASE asset ticker + :param str quote_asset: QUOTE asset ticker + """ + return base_asset.lower() != quote_asset.lower() + + def add_private_key(self, private_key): + """ Add private key into local wallet + + :param str private_key: private key + """ wallet = self.bitshares.wallet try: - wallet.removePrivateKeyFromPublicKey(public_key) - except Exception: + wallet.addPrivateKey(private_key) + except KeyAlreadyInStoreException: + # Private key already added pass diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index 0d6913af4..1abd9b009 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -3,17 +3,13 @@ from dexbot.views.errors import gui_error from dexbot.config import Config +from dexbot.config_validator import ConfigValidator from dexbot.helper import find_external_strategies 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 -from bitshares.asset import Asset -from bitshares.account import Account -from bitshares.exceptions import KeyAlreadyInStoreException -from bitsharesbase.account import PrivateKey from PyQt5 import QtGui @@ -21,8 +17,8 @@ class WorkerController: def __init__(self, view, bitshares_instance, mode): self.view = view - self.bitshares = bitshares_instance or shared_bitshares_instance() self.mode = mode + self.validator = ConfigValidator(bitshares_instance or shared_bitshares_instance()) @property def strategies(self): @@ -54,14 +50,6 @@ def get_strategies(cls): """ return cls(None, None, None).strategies - def add_private_key(self, private_key): - wallet = self.bitshares.wallet - try: - wallet.addPrivateKey(private_key) - except KeyAlreadyInStoreException: - # Private key already added - pass - @staticmethod def get_unique_worker_name(): """ Returns unique worker name "Worker %n" @@ -123,74 +111,6 @@ def change_strategy_form(self, worker_data=None): self.view.setMinimumHeight(0) self.view.resize(width, 1) - @classmethod - def validate_worker_name(cls, worker_name, old_worker_name=None): - if old_worker_name != worker_name: - worker_names = Config().workers_data.keys() - # Check that the name is unique - if worker_name in worker_names: - return False - return True - return True - - def validate_asset(self, asset): - try: - Asset(asset, bitshares_instance=self.bitshares) - return True - except bitshares.exceptions.AssetDoesNotExistsException: - return False - - @classmethod - def validate_market(cls, base_asset, quote_asset): - return base_asset.lower() != quote_asset.lower() - - def validate_account_name(self, account): - if not account: - return False - try: - Account(account, bitshares_instance=self.bitshares) - return True - except bitshares.exceptions.AccountDoesNotExistsException: - return False - - def validate_private_key(self, account, private_key): - wallet = self.bitshares.wallet - if not private_key: - # Check if the private key is already in the database - accounts = wallet.getAccounts() - if any(account == d['name'] for d in accounts): - return True - return False - - try: - pubkey = format(PrivateKey(private_key).pubkey, self.bitshares.prefix) - except ValueError: - return False - - accounts = wallet.getAllAccounts(pubkey) - account_names = [account['name'] for account in accounts] - - if account in account_names: - return True - else: - return False - - def validate_private_key_type(self, account, private_key): - account = Account(account) - pubkey = format(PrivateKey(private_key).pubkey, self.bitshares.prefix) - key_type = self.bitshares.wallet.getKeyType(account, pubkey) - if key_type != 'active' and key_type != 'owner': - return False - return True - - @classmethod - def validate_account_not_in_use(cls, account): - workers = Config().workers_data - for worker_name, worker in workers.items(): - if worker['account'] == account: - return False - return True - @gui_error def validate_form(self): error_texts = [] @@ -200,27 +120,27 @@ def validate_form(self): worker_name = self.view.worker_name_input.text() old_worker_name = None if self.mode == 'add' else self.view.worker_name - if not self.validate_worker_name(worker_name, old_worker_name): + if not self.validator.validate_worker_name(worker_name, old_worker_name): error_texts.append( 'Worker name needs to be unique. "{}" is already in use.'.format(worker_name)) - if not self.validate_asset(base_asset): + if not self.validator.validate_asset(base_asset): error_texts.append('Field "Base Asset" does not have a valid asset.') - if not self.validate_asset(quote_asset): + if not self.validator.validate_asset(quote_asset): error_texts.append('Field "Quote Asset" does not have a valid asset.') - if not self.validate_asset(fee_asset): + if not self.validator.validate_asset(fee_asset): error_texts.append('Field "Fee Asset" does not have a valid asset.') - if not self.validate_market(base_asset, quote_asset): + if not self.validator.validate_market(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_account_name(account): + if not self.validator.validate_account_name(account): error_texts.append("Account doesn't exist.") - if not self.validate_account_not_in_use(account): + if not self.validator.validate_account_not_in_use(account): error_texts.append('Use a different account. "{}" is already in use.'.format(account)) - if not self.validate_private_key(account, private_key): + if not self.validator.validate_private_key(account, private_key): error_texts.append('Private key is invalid.') - elif private_key and not self.validate_private_key_type(account, private_key): + elif private_key and not self.validator.validate_private_key_type(account, private_key): error_texts.append('Please use active private key.') error_texts.extend(self.view.strategy_widget.strategy_controller.validation_errors()) @@ -243,7 +163,7 @@ def handle_save(self): private_key = self.view.private_key_input.text() if private_key: - self.add_private_key(private_key) + self.validator.add_private_key(private_key) account = self.view.account_input.text() else: # Edit From c663014459dd462b1b6a621ead84c3c41ebbc110 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 14 Mar 2019 14:29:16 +0500 Subject: [PATCH 1235/1846] Turn ConfigValidator classmethods into staticmethods I don't see a reason why to use classmethods here, seems staticmethods is enough. --- dexbot/config_validator.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dexbot/config_validator.py b/dexbot/config_validator.py index f411f30b7..6d9b438b3 100644 --- a/dexbot/config_validator.py +++ b/dexbot/config_validator.py @@ -70,8 +70,8 @@ def validate_private_key_type(self, account, private_key): return False return True - @classmethod - def validate_worker_name(cls, worker_name, old_worker_name=None): + @staticmethod + def validate_worker_name(worker_name, old_worker_name=None): """ Check whether worker name is unique or not :param str worker_name: name of the new worker @@ -85,8 +85,8 @@ def validate_worker_name(cls, worker_name, old_worker_name=None): return True return True - @classmethod - def validate_account_not_in_use(cls, account): + @staticmethod + def validate_account_not_in_use(account): """ Check whether account is already used for another worker or not :param str account: bitshares account name @@ -108,8 +108,8 @@ def validate_asset(self, asset): except AssetDoesNotExistsException: return False - @classmethod - def validate_market(cls, base_asset, quote_asset): + @staticmethod + def validate_market(base_asset, quote_asset): """ Check whether market tickers is not the same :param str base_asset: BASE asset ticker From 3235f9b0567a7fbffe680ac06183dd6869cfc70f Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 14 Mar 2019 14:39:07 +0500 Subject: [PATCH 1236/1846] Validate worker name uniqueness in cli conf --- dexbot/cli_conf.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 16fd068dd..2ec2e7e7b 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -281,6 +281,7 @@ def configure_dexbot(config, ctx): whiptail = get_whiptail('DEXBot configure') workers = config.get('workers', {}) bitshares_instance = ctx.bitshares + validator = ConfigValidator(bitshares_instance) if not workers: while True: @@ -335,11 +336,13 @@ def configure_dexbot(config, ctx): strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) strategy.clear_all_worker_data() elif action == 'NEW': - txt = whiptail.prompt("Your name for the new worker. ") - if len(txt) == 0: + worker_name = whiptail.prompt("Your name for the new worker. ") + if not worker_name: whiptail.alert("Worker name cannot be blank. ") + elif not validator.validate_worker_name(worker_name): + whiptail.alert('Worker name needs to be unique. "{}" is already in use.'.format(worker_name)) else: - config['workers'][txt] = configure_worker(whiptail, {}, bitshares_instance) + config['workers'][worker_name] = configure_worker(whiptail, {}, bitshares_instance) elif action == 'ADD': add_account(whiptail, bitshares_instance) elif action == 'SHOW': From 10e7ffa4fe7be8b0a0b16e4f1c37d6e01c47b450 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 14 Mar 2019 14:58:13 +0500 Subject: [PATCH 1237/1846] Fix account list output format Before: ``` Bitshares Account List (Name - Type) 1) active 2) active 3) owner ``` After: ``` Bitshares Account List (Name - Type) 1) foo - active 2) bar - active 3) baz - owner ``` --- dexbot/cli_conf.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 2ec2e7e7b..060014058 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -406,12 +406,12 @@ def del_account(self): def list_accounts(bitshares_instance): - """ Get all accounts installed in local wallet + """ Get all accounts installed in local wallet in format suitable for Whiptail.menu() - :return: list of tuples ('account_name', 'key_type') + :return: list of tuples (int, 'account_name - key_type') """ accounts = bitshares_instance.wallet.getAccounts() - account_list = [(i['name'], i['type']) for i in accounts] - if len(account_list) == 0: - account_list = [('none', 'none')] + account_list = [(num, '{} - {}'.format(account['name'], account['type'])) for num, account in enumerate(accounts)] + if not account_list: + account_list = [(0, 'none - none')] return account_list From 3c92c9e82425986c5453055fa88041d9977bf4d3 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 14 Mar 2019 15:15:24 +0500 Subject: [PATCH 1238/1846] Fix cli viewing of worker configuration Before: ``` List of Your Workers. Select to view Configuration. 1) dtest1 Your choice: [1]: 1 dtest1 1) dtest1 2) 1.0 3) False 4) TEST 5) 0.5 ``` After: ``` List of Your Workers. Select to view Configuration. 1) dtest1 Your choice: [1]: 1 account: dtest1 center_price: 1.0 center_price_dynamic: False fee_asset: TEST increment: 0.5 ``` --- dexbot/cli_conf.py | 8 ++++---- dexbot/whiptail.py | 11 +++++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 060014058..236520762 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -318,10 +318,10 @@ def configure_dexbot(config, ctx): # list workers, then provide option to list config of workers worker_name = whiptail.menu("List of Your Workers. Select to view Configuration.", my_workers) content = config['workers'][worker_name] - worker_content = list(content.items()) - worker_list = [[str(i) for i in pairs] for pairs in worker_content] - worker_list = [tuple(i) for i in worker_list] - whiptail.menu(worker_name, worker_list) + text = '\n' + for key, value in content.items(): + text += '{}: {}\n'.format(key, value) + whiptail.view_text(text, pager=False) elif action == 'EDIT': worker_name = whiptail.menu("Select worker to edit", my_workers) diff --git a/dexbot/whiptail.py b/dexbot/whiptail.py index eac2f51ee..d1a079ff7 100644 --- a/dexbot/whiptail.py +++ b/dexbot/whiptail.py @@ -96,7 +96,7 @@ def node_radiolist(self, msg='', items=(), prefix=''): def checklist(self, msg='', items=(), prefix=' - '): return self.showlist('checklist', msg, items, prefix) - def view_text(self, text): + def view_text(self, text, **kwargs): """Whiptail wants a file but we want to provide a text string""" fd, nam = tempfile.mkstemp() f = os.fdopen(fd) @@ -131,9 +131,12 @@ def alert(self, msg): "] " + msg ) - def view_text(self, text): - click.echo_via_pager(text) - + def view_text(self, text, pager=True): + if pager: + click.echo_via_pager(text) + else: + click.echo(text) + def menu(self, msg='', items=(), default=0): click.echo(msg + '\n') if isinstance(items, dict): From 7a1b338c1e83f94ff426bc0017c4c0881664cb2e Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 14 Mar 2019 17:35:31 +0500 Subject: [PATCH 1239/1846] Handle empty private key input in cli configure This fixes bitshares.exceptions.InvalidWifError when user tries to configure a worker with previously added account name and empty key input. --- dexbot/cli_conf.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 236520762..09ebb47de 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -389,8 +389,11 @@ def add_account(whiptail, bitshares_instance): whiptail.alert("Please use active private key.") return False - validator.add_private_key(private_key) - whiptail.alert("Private Key added successfully.") + # User can supply empty private key if it was added earlier + if private_key: + validator.add_private_key(private_key) + whiptail.alert("Private Key added successfully.") + return account From 270fa2b960ea183b83f316a9313201d40b02cb1c Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 14 Mar 2019 17:43:33 +0500 Subject: [PATCH 1240/1846] Improve docstring help --- dexbot/cli_conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 09ebb47de..fd924950a 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -373,6 +373,7 @@ def add_account(whiptail, bitshares_instance): :param whiptail.Whiptail whiptail: instance of Whiptail or NoWhiptail :param bitshares.BitShares bitshares_instance: an instance of BitShares class + :return str: user-supplied account name """ validator = ConfigValidator(bitshares_instance) From 5c877ee27384e5ac437c9c52cf8c30f1745a502a Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 14 Mar 2019 17:44:02 +0500 Subject: [PATCH 1241/1846] Refactor account + key input in cli conf If user supplied wrong account name or key, don't interrupt the configure process, just repeat query instead. Closes: #509 --- dexbot/cli_conf.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index fd924950a..92a1ead32 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -258,11 +258,11 @@ def configure_worker(whiptail, worker_config, bitshares_instance): for elem in config_elems: if not editing and (elem.key == "account"): # only allow WIF addition for new workers - account_name = add_account(whiptail, bitshares_instance) - if account_name is False: - return # quit configuration if can't get WIF added - else: - worker_config[elem.key] = account_name + account_name = None + # Query user until correct account and key provided + while not account_name: + account_name = add_account(whiptail, bitshares_instance) + worker_config[elem.key] = account_name else: # account name only for edit worker process_config_element(elem, whiptail, worker_config) else: From 46f49658a5b6e87356fd092c912c591fe54372f2 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 14 Mar 2019 17:57:54 +0500 Subject: [PATCH 1242/1846] Implement removal of bitshares account in cli configure --- dexbot/cli_conf.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 92a1ead32..1a04aee84 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -300,8 +300,9 @@ def configure_dexbot(config, ctx): [('LIST', 'List your workers'), ('NEW', 'Create a new worker'), ('EDIT', 'Edit a worker'), - ('DEL', 'Delete a worker'), + ('DEL_WORKER', 'Delete a worker'), ('ADD', 'Add a bitshares account'), + ('DEL_ACCOUNT', 'Delete a bitshares account'), ('SHOW', 'Show bitshares accounts'), ('NODES', 'Edit Node Selection'), ('ADD_NODE', 'Add Your Node'), @@ -345,6 +346,8 @@ def configure_dexbot(config, ctx): config['workers'][worker_name] = configure_worker(whiptail, {}, bitshares_instance) elif action == 'ADD': add_account(whiptail, bitshares_instance) + elif action == 'DEL_ACCOUNT': + del_account(whiptail, bitshares_instance) elif action == 'SHOW': account_list = list_accounts(bitshares_instance) action = whiptail.menu("Bitshares Account List (Name - Type)", account_list) @@ -398,15 +401,15 @@ def add_account(whiptail, bitshares_instance): return account -def del_account(self): - # Todo: implement in the cli_conf - # account = self.whiptail.prompt("Account Name") - public_key = self.whiptail.prompt("Public Key", password=True) - wallet = self.bitshares.wallet - try: - wallet.removePrivateKeyFromPublicKey(public_key) - except Exception: - pass +def del_account(whiptail, bitshares_instance): + """ Delete account from the wallet + + :param whiptail.Whiptail whiptail: instance of Whiptail or NoWhiptail + :param bitshares.BitShares bitshares_instance: an instance of BitShares class + """ + account = whiptail.prompt("Account Name") + wallet = bitshares_instance.wallet + wallet.removeAccount(account) def list_accounts(bitshares_instance): From 139120020a93574e99d042c780d55ad503ca1226 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 14 Mar 2019 22:52:58 +0500 Subject: [PATCH 1243/1846] Move strategies config parts into subdir This is @joelva suggestion to not mess strategies and their config elements in the same dir. --- dexbot/strategies/base.py | 2 +- dexbot/strategies/{ => config_parts}/base_config.py | 0 dexbot/strategies/{ => config_parts}/relative_config.py | 2 +- dexbot/strategies/{ => config_parts}/staggered_config.py | 2 +- dexbot/strategies/{ => config_parts}/strategy_config.py | 2 +- dexbot/strategies/relative_orders.py | 4 ++-- dexbot/strategies/staggered_orders.py | 4 ++-- dexbot/strategies/strategy_template.py | 4 ++-- 8 files changed, 10 insertions(+), 10 deletions(-) rename dexbot/strategies/{ => config_parts}/base_config.py (100%) rename dexbot/strategies/{ => config_parts}/relative_config.py (98%) rename dexbot/strategies/{ => config_parts}/staggered_config.py (97%) rename dexbot/strategies/{ => config_parts}/strategy_config.py (95%) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index e29586e67..bc334c540 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -10,7 +10,7 @@ from dexbot.helper import truncate from dexbot.strategies.external_feeds.price_feed import PriceFeed from dexbot.qt_queue.idle_queue import idle_add -from dexbot.strategies.base_config import BaseConfig +from .config_parts.base_config import BaseConfig from events import Events import bitshares.exceptions diff --git a/dexbot/strategies/base_config.py b/dexbot/strategies/config_parts/base_config.py similarity index 100% rename from dexbot/strategies/base_config.py rename to dexbot/strategies/config_parts/base_config.py diff --git a/dexbot/strategies/relative_config.py b/dexbot/strategies/config_parts/relative_config.py similarity index 98% rename from dexbot/strategies/relative_config.py rename to dexbot/strategies/config_parts/relative_config.py index 33cd72fb7..87a79e2fc 100644 --- a/dexbot/strategies/relative_config.py +++ b/dexbot/strategies/config_parts/relative_config.py @@ -1,4 +1,4 @@ -from dexbot.strategies.base_config import BaseConfig, ConfigElement +from .base_config import BaseConfig, ConfigElement class RelativeConfig(BaseConfig): diff --git a/dexbot/strategies/staggered_config.py b/dexbot/strategies/config_parts/staggered_config.py similarity index 97% rename from dexbot/strategies/staggered_config.py rename to dexbot/strategies/config_parts/staggered_config.py index d9d4bca70..4e50eec08 100644 --- a/dexbot/strategies/staggered_config.py +++ b/dexbot/strategies/config_parts/staggered_config.py @@ -1,4 +1,4 @@ -from dexbot.strategies.base_config import BaseConfig, ConfigElement +from .base_config import BaseConfig, ConfigElement class StaggeredConfig(BaseConfig): diff --git a/dexbot/strategies/strategy_config.py b/dexbot/strategies/config_parts/strategy_config.py similarity index 95% rename from dexbot/strategies/strategy_config.py rename to dexbot/strategies/config_parts/strategy_config.py index ae3ae1bdc..a429f397a 100644 --- a/dexbot/strategies/strategy_config.py +++ b/dexbot/strategies/config_parts/strategy_config.py @@ -1,4 +1,4 @@ -from dexbot.strategies.base_config import BaseConfig, ConfigElement, DetailElement +from .base_config import BaseConfig, ConfigElement, DetailElement class StrategyConfig(BaseConfig): diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 947530e24..e2bcb2610 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -1,8 +1,8 @@ import math from datetime import datetime, timedelta -from dexbot.strategies.base import StrategyBase -from dexbot.strategies.relative_config import RelativeConfig +from .base import StrategyBase +from .config_parts.relative_config import RelativeConfig class Strategy(StrategyBase): diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 3a8e17388..9bf95531e 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -8,8 +8,8 @@ from bitshares.dex import Dex from bitshares.amount import Amount -from dexbot.strategies.base import StrategyBase -from dexbot.strategies.staggered_config import StaggeredConfig +from .base import StrategyBase +from .config_parts.staggered_config import StaggeredConfig class Strategy(StrategyBase): diff --git a/dexbot/strategies/strategy_template.py b/dexbot/strategies/strategy_template.py index be3f49ba7..c5ac7f9ac 100644 --- a/dexbot/strategies/strategy_template.py +++ b/dexbot/strategies/strategy_template.py @@ -2,8 +2,8 @@ # import math # Project imports -from dexbot.strategies.base import StrategyBase -from dexbot.strategies.strategy_config import StrategyConfig +from .base import StrategyBase +from .config_parts.strategy_config import StrategyConfig from dexbot.qt_queue.idle_queue import idle_add # Third party imports From 39fc9e64459480e1ec98d767a44efc9370b377bf Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 15 Mar 2019 12:14:34 +0500 Subject: [PATCH 1244/1846] Change ask message --- dexbot/cli_conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 1a04aee84..d6f7fc797 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -357,7 +357,7 @@ def configure_dexbot(config, ctx): # overrides the top position elif action == 'NODES': choice = whiptail.node_radiolist( - msg="Choose node", + msg="Choose your preferred node", items=select_choice(config['node'][0], [(index, index) for index in config['node']])) # Move selected node as first item in the config file's node list From d3a781592070745c22a39313fefdeb361c97b3c0 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 15 Mar 2019 12:15:49 +0500 Subject: [PATCH 1245/1846] Add NoWhiptail.node_radiolist() stub This is for compatibility with Whiptail to avoid exception: AttributeError: 'NoWhiptail' object has no attribute 'node_radiolist' --- dexbot/whiptail.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dexbot/whiptail.py b/dexbot/whiptail.py index d1a079ff7..5124f9a61 100644 --- a/dexbot/whiptail.py +++ b/dexbot/whiptail.py @@ -159,6 +159,11 @@ def radiolist(self, msg='', items=()): d += 1 return self.menu(msg, [(k, v) for k, v, s in items], default=default) + def node_radiolist(self, *args, **kwargs): + """ Proxy stub to maintain compatibility with Whiptail class + """ + return self.radiolist(*args, **kwargs) + def clear(self): pass # Don't tidy the screen From 6890f5e5399ad0fc8ae3b95f82cd75a18c547c57 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 15 Mar 2019 12:21:54 +0500 Subject: [PATCH 1246/1846] Change adding node logic in cli conf When adding a new node, insert it on top of the list instead of overriding the first entry --- dexbot/cli_conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index d6f7fc797..6b9920342 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -353,8 +353,8 @@ def configure_dexbot(config, ctx): action = whiptail.menu("Bitshares Account List (Name - Type)", account_list) elif action == 'ADD_NODE': txt = whiptail.prompt("Your name for the new node: e.g. wss://dexnode.net/ws") - config['node'][0] = txt - # overrides the top position + # Insert new node on top of the list + config['node'].insert(0, txt) elif action == 'NODES': choice = whiptail.node_radiolist( msg="Choose your preferred node", From 0182e219456a0c29f6d53f94bb486f763a51be20 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 15 Mar 2019 12:22:59 +0500 Subject: [PATCH 1247/1846] Set default to "no" for "run via systemd" question --- dexbot/cli_conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 6b9920342..19f2a481c 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -149,7 +149,7 @@ def setup_systemd(whiptail, config): return # No working systemd if not whiptail.confirm( - "Do you want to run dexbot as a background (daemon) process?"): + "Do you want to run dexbot as a background (daemon) process?", default="no"): config['systemd_status'] = 'disabled' return From 5d1f926c76e830e74483b5582369985f7b80dfa9 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 15 Mar 2019 13:37:17 +0500 Subject: [PATCH 1248/1846] Fix file opening mode in whiptail --- dexbot/whiptail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/whiptail.py b/dexbot/whiptail.py index 5124f9a61..51d16eae7 100644 --- a/dexbot/whiptail.py +++ b/dexbot/whiptail.py @@ -99,7 +99,7 @@ def checklist(self, msg='', items=(), prefix=' - '): def view_text(self, text, **kwargs): """Whiptail wants a file but we want to provide a text string""" fd, nam = tempfile.mkstemp() - f = os.fdopen(fd) + f = os.fdopen(fd, 'w') f.write(text) f.close() self.view_file(nam) From 6fa3aa9f6f477472c91c594c3c644f722a542aed Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 15 Mar 2019 13:43:20 +0500 Subject: [PATCH 1249/1846] Restore Whiptail compatibility in list_accounts() --- dexbot/cli_conf.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 19f2a481c..934a65322 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -415,10 +415,14 @@ def del_account(whiptail, bitshares_instance): def list_accounts(bitshares_instance): """ Get all accounts installed in local wallet in format suitable for Whiptail.menu() + Returning format is compatible both with Whiptail and NoWhiptail. + :return: list of tuples (int, 'account_name - key_type') """ accounts = bitshares_instance.wallet.getAccounts() - account_list = [(num, '{} - {}'.format(account['name'], account['type'])) for num, account in enumerate(accounts)] + account_list = [ + (str(num), '{} - {}'.format(account['name'], account['type'])) for num, account in enumerate(accounts) + ] if not account_list: - account_list = [(0, 'none - none')] + account_list = [('0', 'none - none')] return account_list From c4b06c6208cf1b8cf8af8be81b74879c35fc03ca Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 15 Mar 2019 12:24:48 +0200 Subject: [PATCH 1250/1846] Change dexbot version number to 0.9.20 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index a3d8ed568..a92edbcc1 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.19' +VERSION = '0.9.20' AUTHOR = 'Codaone Oy' __version__ = VERSION From 9318751509596a060134b7dd5a7af32a9c105498 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 15 Mar 2019 12:46:57 +0200 Subject: [PATCH 1251/1846] Change dexbot version number to 0.9.21 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index a92edbcc1..9c6d16557 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.20' +VERSION = '0.9.21' AUTHOR = 'Codaone Oy' __version__ = VERSION From 9cbfe516c94141db4f35f091bb52c7e5505eeb37 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 15 Mar 2019 12:55:07 +0200 Subject: [PATCH 1252/1846] Change dexbot version number to 0.9.22 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 9c6d16557..a59e97b22 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.21' +VERSION = '0.9.22' AUTHOR = 'Codaone Oy' __version__ = VERSION From 10e1cc2875d5ffb310eb7fed8360f3a013cd06b5 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 15 Mar 2019 12:59:28 +0200 Subject: [PATCH 1253/1846] Change dexbot version number to 0.9.23 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index a59e97b22..c4fc2e038 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.22' +VERSION = '0.9.23' AUTHOR = 'Codaone Oy' __version__ = VERSION From 476d258f350bc45e99ca9326e3ac8d51c080f366 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 15 Mar 2019 14:21:39 +0200 Subject: [PATCH 1254/1846] Change dexbot version number to 0.9.24 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index c4fc2e038..18e0b8930 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.23' +VERSION = '0.9.24' AUTHOR = 'Codaone Oy' __version__ = VERSION From b93033da027795136dd839e8e3423f9974437b53 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 15 Mar 2019 15:13:16 +0200 Subject: [PATCH 1255/1846] Change dexbot version number to 0.9.25 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 18e0b8930..876ed252b 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.24' +VERSION = '0.9.25' AUTHOR = 'Codaone Oy' __version__ = VERSION From 6d07db73ad39efd2126016b8fb9b75db1a53f97b Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 15 Mar 2019 15:44:24 +0200 Subject: [PATCH 1256/1846] Change dexbot version number to 0.9.26 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 876ed252b..ad9afdd6c 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.25' +VERSION = '0.9.26' AUTHOR = 'Codaone Oy' __version__ = VERSION From e9deee195bff0924a75eb14c675e6cacd3546eeb Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 15 Mar 2019 19:11:16 +0500 Subject: [PATCH 1257/1846] Add comment --- dexbot/ui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dexbot/ui.py b/dexbot/ui.py index b541ea9d4..bd1a44fdb 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -46,6 +46,7 @@ def new_func(ctx, *args, **kwargs): # Logging to a file filename = ctx.obj.get('logfile') if not filename: + # By default, log to a file located where the script is filename = os.path.join(os.path.dirname(sys.argv[0]), 'dexbot.log') fh = logging.FileHandler(filename) fh.setFormatter(formatter2) From fcd3c4a59787818e958adf4fd86c49d43a19ffba Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 15 Mar 2019 19:14:39 +0500 Subject: [PATCH 1258/1846] Ensure --logfile and --pidfile are writeable files --- dexbot/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index fbf9c2632..e5e2c9c4e 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -48,6 +48,7 @@ @click.option( '--logfile', default=None, + type=click.Path(dir_okay=False, writable=True), help='Override logfile location (example: ~/dexbot.log)' ) @click.option( @@ -64,7 +65,7 @@ @click.option( '--pidfile', '-p', - type=str, + type=click.Path(dir_okay=False, writable=True), default='', help='File to write PID') @click.pass_context From 5ce0ddc2780b57707584e191bcb99d9d2f2f3722 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 15 Mar 2019 19:53:38 +0500 Subject: [PATCH 1259/1846] Add operational balance settings into GUI --- dexbot/views/ui/create_worker_window.ui | 284 ++++++++++++++++++++++-- dexbot/views/ui/edit_worker_window.ui | 254 ++++++++++++++++++++- 2 files changed, 521 insertions(+), 17 deletions(-) diff --git a/dexbot/views/ui/create_worker_window.ui b/dexbot/views/ui/create_worker_window.ui index 669eb9eed..fc4a3b94b 100644 --- a/dexbot/views/ui/create_worker_window.ui +++ b/dexbot/views/ui/create_worker_window.ui @@ -130,22 +130,6 @@ - - - - - 0 - 0 - - - - QLineEdit::Password - - - false - - - @@ -238,6 +222,22 @@ + + + + + 0 + 0 + + + + QLineEdit::Password + + + false + + + @@ -634,6 +634,258 @@ + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Operational QUOTE + + + quote_asset_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Max % of QUOTE asset available to this worker, 0 - auto + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Operational BASE + + + quote_asset_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Max % of BASE asset available to this worker, 0 - auto + + + ? + + + 5 + + + + + + + + + + + 145 + 0 + + + + + 80 + 16777215 + + + + ArrowCursor + + + true + + + Qt::WheelFocus + + + + + + % + + + 100.000000000000000 + + + 1.000000000000000 + + + + + + + + 145 + 0 + + + + + 80 + 16777215 + + + + ArrowCursor + + + true + + + Qt::WheelFocus + + + + + + % + + + 100.000000000000000 + + + 1.000000000000000 + + + diff --git a/dexbot/views/ui/edit_worker_window.ui b/dexbot/views/ui/edit_worker_window.ui index af790dd41..09f4b5467 100644 --- a/dexbot/views/ui/edit_worker_window.ui +++ b/dexbot/views/ui/edit_worker_window.ui @@ -401,6 +401,258 @@ + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Operational QUOTE + + + quote_asset_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Max % of QUOTE asset available to this worker, 0 - auto + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Operational BASE + + + quote_asset_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Max % of BASE asset available to this worker, 0 - auto + + + ? + + + 5 + + + + + + + + + + + 145 + 0 + + + + + 80 + 16777215 + + + + ArrowCursor + + + true + + + Qt::WheelFocus + + + + + + % + + + 100.000000000000000 + + + 1.000000000000000 + + + + + + + + 145 + 0 + + + + + 80 + 16777215 + + + + ArrowCursor + + + true + + + Qt::WheelFocus + + + + + + % + + + 100.000000000000000 + + + 1.000000000000000 + + + @@ -503,7 +755,7 @@ min-height: 23px; color: #fff; } - + QPushButton:hover { background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #ed9f9c, stop: 1 #db4240); From 7c95a3e0081fa051ab4a679bd4e2f5365eba370d Mon Sep 17 00:00:00 2001 From: joelvai Date: Fri, 15 Mar 2019 19:06:12 +0200 Subject: [PATCH 1260/1846] Change dexbot version number to 0.9.27 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index ad9afdd6c..81be5baa0 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.26' +VERSION = '0.9.27' AUTHOR = 'Codaone Oy' __version__ = VERSION From 10e931ff543aee760dcaf49b44f3f1d9743d4823 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 18 Mar 2019 12:33:31 +0500 Subject: [PATCH 1261/1846] Remove unused import --- dexbot/strategies/staggered_orders.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 9da65c6b2..2210ce1a0 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1,4 +1,3 @@ -import sys import time import math import bitsharesapi.exceptions From 91c0cbb6c84ed4c3b1c1d61356bcab4fbeb3a4a1 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 19 Mar 2019 08:37:55 +0200 Subject: [PATCH 1262/1846] Fix option to delete worker DEL_WORKER didn't do anything since the check was using old key `DEL` instead. --- dexbot/cli_conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 934a65322..87f41b43d 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -331,7 +331,7 @@ def configure_dexbot(config, ctx): strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) strategy.clear_all_worker_data() - elif action == 'DEL': + elif action == 'DEL_WORKER': worker_name = whiptail.menu("Select worker to delete", my_workers) del config['workers'][worker_name] strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) From 80cd040cf5069f8da3946963c48ede027f0f8652 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 19 Mar 2019 08:44:55 +0200 Subject: [PATCH 1263/1846] Fix typo --- dexbot/cli_conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 87f41b43d..9136b8647 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -187,7 +187,7 @@ def get_strategy_tag(strategy_class): :param str strategy_class: strategy class name, example: dexbot.strategies.foo_bar - It may seems that tags may be common accross strategies, but it is not. Every strategy must use unique tag. + It may seems that tags may be common across strategies, but it is not. Every strategy must use unique tag. """ for strategy in STRATEGIES: if strategy_class == strategy['class']: From 87eea8002c6641b6bdab61ed39a2dd85b52a2438 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 19 Mar 2019 08:45:19 +0200 Subject: [PATCH 1264/1846] Fix crashing on menus that list workers when no workers available --- dexbot/cli_conf.py | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 9136b8647..c41555456 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -312,30 +312,37 @@ def configure_dexbot(config, ctx): my_workers = [(index, index) for index in workers] if action == 'EXIT': - # cancel will also exit the application. but this is a clearer label + # Cancel will also exit the application. but this is a clearer label # Todo: modify cancel to be "Quit" or "Exit" for the whiptail menu item. break elif action == 'LIST': - # list workers, then provide option to list config of workers - worker_name = whiptail.menu("List of Your Workers. Select to view Configuration.", my_workers) - content = config['workers'][worker_name] - text = '\n' - for key, value in content.items(): - text += '{}: {}\n'.format(key, value) - whiptail.view_text(text, pager=False) - + if len(my_workers): + # List workers, then provide option to list config of workers + worker_name = whiptail.menu("List of Your Workers. Select to view Configuration.", my_workers) + content = config['workers'][worker_name] + text = '\n' + for key, value in content.items(): + text += '{}: {}\n'.format(key, value) + whiptail.view_text(text, pager=False) + else: + whiptail.view_text('No workers to view.', pager=False) elif action == 'EDIT': - worker_name = whiptail.menu("Select worker to edit", my_workers) - config['workers'][worker_name] = configure_worker(whiptail, config['workers'][worker_name], - bitshares_instance) - strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) - strategy.clear_all_worker_data() - + if len(my_workers): + worker_name = whiptail.menu("Select worker to edit", my_workers) + config['workers'][worker_name] = configure_worker(whiptail, config['workers'][worker_name], + bitshares_instance) + strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) + strategy.clear_all_worker_data() + else: + whiptail.view_text('No workers to edit.', pager=False) elif action == 'DEL_WORKER': - worker_name = whiptail.menu("Select worker to delete", my_workers) - del config['workers'][worker_name] - strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) - strategy.clear_all_worker_data() + if len(my_workers): + worker_name = whiptail.menu("Select worker to delete", my_workers) + del config['workers'][worker_name] + strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) + strategy.clear_all_worker_data() + else: + whiptail.view_text('No workers to delete.', pager=False) elif action == 'NEW': worker_name = whiptail.prompt("Your name for the new worker. ") if not worker_name: From f3d0897b8e5ea52ee9c419cb059769b5e78458c8 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 19 Mar 2019 13:18:17 +0500 Subject: [PATCH 1265/1846] Preserve --configfile option when editing or deleting worker Config() instance is empty dict, and to query for something we call config_instance['key'], so it's being called via __getitem__(). In "DEL_WORKER": ``` strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) ``` config passed is empty dict too, so inside StrategyBase() it instantiates a new config from default config path: ``` self.config = config = Config.get_worker_config_file(name) ``` To reproduce, you need to run cli configure with custom config like this --configfile new_conf.yml. So cli configure shows you list of workers in this custom config, but when you're want to delete it tries to delete from the default config. To allow StrategyBase use proper config, just pass `ctx.config` which is loaded config (). --- dexbot/cli_conf.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index c41555456..3e01041ff 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -331,7 +331,7 @@ def configure_dexbot(config, ctx): worker_name = whiptail.menu("Select worker to edit", my_workers) config['workers'][worker_name] = configure_worker(whiptail, config['workers'][worker_name], bitshares_instance) - strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) + strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=ctx.config) strategy.clear_all_worker_data() else: whiptail.view_text('No workers to edit.', pager=False) @@ -339,7 +339,11 @@ def configure_dexbot(config, ctx): if len(my_workers): worker_name = whiptail.menu("Select worker to delete", my_workers) del config['workers'][worker_name] - strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) + # Pass ctx.config which is a loaded config (see ui.py configfile()), while `config` in a Config() + # instance, which is empty dict, but capable of returning keys via __getitem__(). We need to pass + # loaded config into StrategyBase to avoid loading a default config and preserve `--configfile` + # option + strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=ctx.config) strategy.clear_all_worker_data() else: whiptail.view_text('No workers to delete.', pager=False) From 7576e1432cbdc4a991d2b3b951a346e55bd9b1d8 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 19 Mar 2019 13:24:50 +0500 Subject: [PATCH 1266/1846] More clean indication when no bitshares accounts --- dexbot/cli_conf.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 3e01041ff..780297791 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -361,7 +361,10 @@ def configure_dexbot(config, ctx): del_account(whiptail, bitshares_instance) elif action == 'SHOW': account_list = list_accounts(bitshares_instance) - action = whiptail.menu("Bitshares Account List (Name - Type)", account_list) + if account_list: + action = whiptail.menu("Bitshares Account List (Name - Type)", account_list) + else: + whiptail.alert('You do not have any bitshares accounts in the wallet') elif action == 'ADD_NODE': txt = whiptail.prompt("Your name for the new node: e.g. wss://dexnode.net/ws") # Insert new node on top of the list @@ -434,6 +437,4 @@ def list_accounts(bitshares_instance): account_list = [ (str(num), '{} - {}'.format(account['name'], account['type'])) for num, account in enumerate(accounts) ] - if not account_list: - account_list = [('0', 'none - none')] return account_list From 1787e0c942652cfe974527bcdf8b24fcb2ddae19 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 19 Mar 2019 10:38:01 +0200 Subject: [PATCH 1267/1846] Change whiptail.view_text to whiptail.alert --- dexbot/cli_conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 780297791..683fe45de 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -325,7 +325,7 @@ def configure_dexbot(config, ctx): text += '{}: {}\n'.format(key, value) whiptail.view_text(text, pager=False) else: - whiptail.view_text('No workers to view.', pager=False) + whiptail.alert('No workers to view.') elif action == 'EDIT': if len(my_workers): worker_name = whiptail.menu("Select worker to edit", my_workers) @@ -334,7 +334,7 @@ def configure_dexbot(config, ctx): strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=ctx.config) strategy.clear_all_worker_data() else: - whiptail.view_text('No workers to edit.', pager=False) + whiptail.alert('No workers to edit.') elif action == 'DEL_WORKER': if len(my_workers): worker_name = whiptail.menu("Select worker to delete", my_workers) @@ -346,7 +346,7 @@ def configure_dexbot(config, ctx): strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=ctx.config) strategy.clear_all_worker_data() else: - whiptail.view_text('No workers to delete.', pager=False) + whiptail.alert('No workers to delete.') elif action == 'NEW': worker_name = whiptail.prompt("Your name for the new worker. ") if not worker_name: From 630ed9c007d1f4f33ba16cc5e0481c1b0c301302 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 19 Mar 2019 13:55:46 +0500 Subject: [PATCH 1268/1846] Do not purge worker data after EDIT in cli We don't want to purge worker data on each reconfiguration. The similar problem was already solved for the GUI by only pausing a running worker after reconfiguration. See dexbot.views.worker_item.WorkerItemWidget.handle_edit_worker() --- dexbot/cli_conf.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 683fe45de..4f677d291 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -331,8 +331,6 @@ def configure_dexbot(config, ctx): worker_name = whiptail.menu("Select worker to edit", my_workers) config['workers'][worker_name] = configure_worker(whiptail, config['workers'][worker_name], bitshares_instance) - strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=ctx.config) - strategy.clear_all_worker_data() else: whiptail.alert('No workers to edit.') elif action == 'DEL_WORKER': From a0786ae62fb7128fda70f5379a39199699367038 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 19 Mar 2019 14:53:46 +0500 Subject: [PATCH 1269/1846] Add comment about return_base_config usage --- dexbot/views/strategy_form.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dexbot/views/strategy_form.py b/dexbot/views/strategy_form.py index 4826107e2..9a0a5bfce 100644 --- a/dexbot/views/strategy_form.py +++ b/dexbot/views/strategy_form.py @@ -16,7 +16,8 @@ def __init__(self, controller, strategy_module, worker_config=None): importlib.import_module(strategy_module), 'Strategy' ) - configure = strategy_class.configure(False) + # For strategies uses autogeneration, we need the strategy configs without the defaults + configure = strategy_class.configure(return_base_config=False) form_module = controller.strategies[strategy_module].get('form_module') try: widget = getattr( From 55b405cc66b92955ebe48f1e155492b42fc7d0d2 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 19 Mar 2019 14:55:52 +0500 Subject: [PATCH 1270/1846] Restore proper handling of return_base_config Instead of handling return_base_config kwarg at stategy level just pass it into BaseConfig.configure() as it was before. This fixes a `KeyError: 'manual_offset'` while editing RO worker in the GUI. --- dexbot/strategies/config_parts/relative_config.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dexbot/strategies/config_parts/relative_config.py b/dexbot/strategies/config_parts/relative_config.py index 3c834ed41..d450fdca7 100644 --- a/dexbot/strategies/config_parts/relative_config.py +++ b/dexbot/strategies/config_parts/relative_config.py @@ -75,9 +75,7 @@ def configure(cls, return_base_config=True): (30, 157680000, '')) ] - if return_base_config: - return BaseConfig.configure(return_base_config) + relative_orders_config - return [] + return BaseConfig.configure(return_base_config) + relative_orders_config @classmethod def configure_details(cls, include_default_tabs=True): From 37b14940e7ec34eec2a56e8d878ce4e5589780bc Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 19 Mar 2019 12:19:20 +0200 Subject: [PATCH 1271/1846] Change dexbot version number to 0.10.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 81be5baa0..4167d29d5 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.9.27' +VERSION = '0.10.0' AUTHOR = 'Codaone Oy' __version__ = VERSION From e58685d373e4e39dc3adf53db0fb348a52677e8d Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 19 Mar 2019 12:51:04 +0200 Subject: [PATCH 1272/1846] Update pybitshares dependency to version 0.3.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e81f40f7c..1bbb6e007 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ requests>=2.20.0 yarl==1.1.0 ccxt==1.17.434 pywaves==0.8.20 -bitshares==0.2.1 +bitshares==0.3.0 uptick==0.2.1 ruamel.yaml>=0.15.37 appdirs>=1.4.3 From 450b1fe6e0a73039815a710fc3e5d4d8c4d72428 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 19 Mar 2019 13:08:29 +0200 Subject: [PATCH 1273/1846] Change dexbot version number to 0.10.1 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 4167d29d5..62fae13fd 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.10.0' +VERSION = '0.10.1' AUTHOR = 'Codaone Oy' __version__ = VERSION From 262b7e2524dc7237fd174d2b9cd54a41ec683ae9 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 20 Mar 2019 09:06:32 +0200 Subject: [PATCH 1274/1846] Revert "Change dexbot version number to 0.10.1" This reverts commit 450b1fe6e0a73039815a710fc3e5d4d8c4d72428. --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 62fae13fd..4167d29d5 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.10.1' +VERSION = '0.10.0' AUTHOR = 'Codaone Oy' __version__ = VERSION From 1ec240ea730ee9504a3a4deeac638b1eae4c8f56 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 20 Mar 2019 09:07:33 +0200 Subject: [PATCH 1275/1846] Revert "Update pybitshares dependency to version 0.3.0" This reverts commit e58685d373e4e39dc3adf53db0fb348a52677e8d. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1bbb6e007..e81f40f7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ requests>=2.20.0 yarl==1.1.0 ccxt==1.17.434 pywaves==0.8.20 -bitshares==0.3.0 +bitshares==0.2.1 uptick==0.2.1 ruamel.yaml>=0.15.37 appdirs>=1.4.3 From 54cd5ec8834ac57a705c0f06d4d72276a587cce6 Mon Sep 17 00:00:00 2001 From: Juhani Haapala Date: Wed, 20 Mar 2019 13:34:22 +0200 Subject: [PATCH 1276/1846] Add connected node to gui footer --- dexbot/views/worker_list.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 29a4a5fa0..ad1a83701 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -149,13 +149,13 @@ def get_statusbar_message(self): node = self.config['node'] try: start = time.time() - BitSharesNodeRPC(node, num_retries=1) + rpc = BitSharesNodeRPC(node, num_retries=1) latency = (time.time() - start) * 1000 except BaseException: latency = -1 if latency != -1: - return "ver {} - Node delay: {:.2f}ms".format(__version__, latency) + return "ver {} - Node delay: {:.2f}ms - node: {}".format(__version__, latency, rpc.url) else: return "ver {} - Node disconnected".format(__version__) From d6aacc72c77b5997ee56eb05670b50bd3e8a6bac Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Mon, 25 Mar 2019 08:46:42 +0200 Subject: [PATCH 1277/1846] Add __init__.py to config_parts --- dexbot/strategies/config_parts/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 dexbot/strategies/config_parts/__init__.py diff --git a/dexbot/strategies/config_parts/__init__.py b/dexbot/strategies/config_parts/__init__.py new file mode 100644 index 000000000..e69de29bb From a69652521108aac555eb2b047d470e55bc8cf7cc Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Mon, 25 Mar 2019 08:47:21 +0200 Subject: [PATCH 1278/1846] Remove redundant () --- dexbot/strategies/config_parts/base_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/config_parts/base_config.py b/dexbot/strategies/config_parts/base_config.py index 4d16a85be..6ebceac11 100644 --- a/dexbot/strategies/config_parts/base_config.py +++ b/dexbot/strategies/config_parts/base_config.py @@ -46,7 +46,7 @@ DetailElement = collections.namedtuple('DetailTab', 'type name title file') -class BaseConfig(): +class BaseConfig: @classmethod def configure(cls, return_base_config=True): From 185886b21c36772a7320c8f223b660af3ac18122 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Mon, 25 Mar 2019 08:48:29 +0200 Subject: [PATCH 1279/1846] Change relative import paths to absolute --- dexbot/strategies/config_parts/base_config.py | 2 +- dexbot/strategies/config_parts/relative_config.py | 2 +- dexbot/strategies/config_parts/staggered_config.py | 2 +- dexbot/strategies/config_parts/strategy_config.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/config_parts/base_config.py b/dexbot/strategies/config_parts/base_config.py index 6ebceac11..535536507 100644 --- a/dexbot/strategies/config_parts/base_config.py +++ b/dexbot/strategies/config_parts/base_config.py @@ -3,7 +3,7 @@ """ Strategies need to specify their own configuration values, so each strategy can have a class method 'configure' which returns a list of ConfigElement named tuples. - Tuple fields as follows: + Tuple fields as fgitollows: - Key: The key in the bot config dictionary that gets saved back to config.yml - Type: "int", "float", "bool", "string" or "choice" - Default: The default value, must be same type as the Type defined diff --git a/dexbot/strategies/config_parts/relative_config.py b/dexbot/strategies/config_parts/relative_config.py index d450fdca7..480dab533 100644 --- a/dexbot/strategies/config_parts/relative_config.py +++ b/dexbot/strategies/config_parts/relative_config.py @@ -1,4 +1,4 @@ -from .base_config import BaseConfig, ConfigElement +from dexbot.strategies.config_parts.base_config import BaseConfig, ConfigElement class RelativeConfig(BaseConfig): diff --git a/dexbot/strategies/config_parts/staggered_config.py b/dexbot/strategies/config_parts/staggered_config.py index 4e50eec08..3b59166a6 100644 --- a/dexbot/strategies/config_parts/staggered_config.py +++ b/dexbot/strategies/config_parts/staggered_config.py @@ -1,4 +1,4 @@ -from .base_config import BaseConfig, ConfigElement +from dexbot.strategies.config_parts.base_config import BaseConfig, ConfigElement class StaggeredConfig(BaseConfig): diff --git a/dexbot/strategies/config_parts/strategy_config.py b/dexbot/strategies/config_parts/strategy_config.py index a429f397a..5c961d3f7 100644 --- a/dexbot/strategies/config_parts/strategy_config.py +++ b/dexbot/strategies/config_parts/strategy_config.py @@ -1,4 +1,4 @@ -from .base_config import BaseConfig, ConfigElement, DetailElement +from dexbot.strategies.config_parts.base_config import BaseConfig, ConfigElement, DetailElement class StrategyConfig(BaseConfig): From ddeb1a1465723e875fe391302c6101df17f21b46 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Mon, 25 Mar 2019 08:56:11 +0200 Subject: [PATCH 1280/1846] Fix typo Accidentally typed git inside documentation --- dexbot/strategies/config_parts/base_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/config_parts/base_config.py b/dexbot/strategies/config_parts/base_config.py index 535536507..6ebceac11 100644 --- a/dexbot/strategies/config_parts/base_config.py +++ b/dexbot/strategies/config_parts/base_config.py @@ -3,7 +3,7 @@ """ Strategies need to specify their own configuration values, so each strategy can have a class method 'configure' which returns a list of ConfigElement named tuples. - Tuple fields as fgitollows: + Tuple fields as follows: - Key: The key in the bot config dictionary that gets saved back to config.yml - Type: "int", "float", "bool", "string" or "choice" - Default: The default value, must be same type as the Type defined From 4d0b4da732d150e086694d7c37beaa71204f8ac3 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Mon, 25 Mar 2019 09:04:50 +0200 Subject: [PATCH 1281/1846] Change dexbot version number to 0.10.1 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 4167d29d5..62fae13fd 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.10.0' +VERSION = '0.10.1' AUTHOR = 'Codaone Oy' __version__ = VERSION From 61f81f1691830b6feffda7d43af48262cb1fdfca Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 31 Mar 2019 14:33:15 +0500 Subject: [PATCH 1282/1846] Add new pytest testsuite New testsuite uses local bitshares testnet spawned via docker container. Closes: #515 --- requirements-dev.txt | 2 + tests/README.md | 61 ++++++ tests/conftest.py | 201 ++++++++++++++++++ tests/node_config/config.ini | 79 +++++++ tests/node_config/genesis.json | 364 +++++++++++++++++++++++++++++++++ tests/test_prepared_testnet.py | 33 +++ 6 files changed, 740 insertions(+) create mode 100644 requirements-dev.txt create mode 100644 tests/README.md create mode 100644 tests/conftest.py create mode 100644 tests/node_config/config.ini create mode 100644 tests/node_config/genesis.json create mode 100644 tests/test_prepared_testnet.py diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 000000000..edd9a9b55 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +pytest +docker diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 000000000..37905366d --- /dev/null +++ b/tests/README.md @@ -0,0 +1,61 @@ +Overview +======== + +This testsuite is based on [pytest](https://docs.pytest.org/en/latest/contents.html) + +Testsuite wants to run local bitshares-core testnet via docker containter. This implies the following requirements are +satisfied: + +* Your platform supports docker +* docker daemon is installed and configured +* current user is able to interact with docker daemon and have sufficient permissions + +Running testsuite +----------------- + +``` +pip install -r requirements-dev.txt +``` + +Run all tests: + +``` +pytest +``` + +or + +``` +python -m pytest +``` + +Run single test: + +``` +pytest tests/test_prepared_testnet.py +``` + +How to prepare genesis.json +=========================== + +genesis.json contains initial accounts including witnesses and committee members. Every account has it's public key. +For the sake of simplicity, pick any keypair and use it's public key for every account. + + +Balances +-------- + +At the beginning, all balances are stored in `initial_balances` object. To access these balances, users must claim them +via `balance_claim` operation. This step is automated. + +`initial_balances` object has `owner` field which is graphene Address. To generate an address from public key, use the +following code: + +```python +from graphenebase import PublicKey, Address + +w = '5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3' +k = PublicKey.from_privkey(w) +a = Address.from_pubkey(k) +str(a) +``` diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..01b5a3974 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,201 @@ +import uuid +import docker +import os.path +import pytest +import socket +import random + +from bitshares import BitShares +from bitshares.genesisbalance import GenesisBalance +from bitshares.account import Account +from bitshares.asset import Asset +from bitshares.exceptions import AssetDoesNotExistsException, AccountDoesNotExistsException + +from bitsharesbase.account import PublicKey +from bitsharesbase.chains import known_chains + +# Note: chain_id is generated from genesis.json, every time it's changes you need to get new chain_id from +# `bitshares.rpc.get_chain_properties()` +known_chains["TEST"]["chain_id"] = "5f0c72a2637f4938507f06d87e07be5d8015c32d720dce468d6d9a30db79947b" + +PRIVATE_KEYS = ['5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3'] +DEFAULT_ACCOUNT = 'init0' + +# Example how to split conftest.py into multiple files +# pytest_plugins = ['fixture_a.py', 'fixture_b.py'] + + +@pytest.fixture(scope='session') +def session_id(): + """ Generate unique session id. This is needed in case testsuite may run in parallel on the same server, for example + if CI/CD is being used. CI/CD infrastructure may run tests for each commit, so these tests should not influence + each other. + """ + return str(uuid.uuid4()) + + +@pytest.fixture(scope='session') +def unused_port(): + """ Obtain unused port to bind some service + """ + + def _unused_port(): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(('127.0.0.1', 0)) + return s.getsockname()[1] + + return _unused_port + + +@pytest.fixture(scope='session') +def docker_manager(): + """ Initialize docker management client + """ + return docker.from_env(version='auto') + + +@pytest.fixture(scope='session') +def bitshares_testnet(session_id, unused_port, docker_manager): + """ Run bitshares-core inside local docker container + + Manual run example: + $ docker run --name bitshares -p 0.0.0.0:8091:8091 -v `pwd`/cfg:/etc/bitshares/ bitshares/bitshares-core:testnet + """ + port = unused_port() + container = docker_manager.containers.run( + image='bitshares/bitshares-core:testnet', + name='bitshares-testnet-{}'.format(session_id), + ports={'8091': port}, + volumes={'{}/tests/node_config'.format(os.path.abspath('.')): {'bind': '/etc/bitshares/', 'mode': 'ro'}}, + detach=True, + ) + container.service_port = port + yield container + container.remove(v=True, force=True) + + +@pytest.fixture(scope='session') +def bitshares_instance(bitshares_testnet): + """ Initialize BitShares instance connected to a local testnet + """ + bitshares = BitShares( + node='ws://127.0.0.1:{}'.format(bitshares_testnet.service_port), keys=PRIVATE_KEYS, num_retries=-1 + ) + # Todo: show chain params when connectiong to unknown network + # https://github.com/bitshares/python-bitshares/issues/221 + + return bitshares + + +@pytest.fixture(scope='session') +def claim_balance(bitshares_instance): + """ Transfer balance from genesis into actual account + """ + genesis_balance = GenesisBalance('1.15.0', bitshares_instance=bitshares_instance) + genesis_balance.claim(account=DEFAULT_ACCOUNT) + + +@pytest.fixture(scope='session') +def bitshares(bitshares_instance, claim_balance): + """ Prepare the testnet and return BitShares instance + """ + return bitshares_instance + + +@pytest.fixture(scope='session') +def create_asset(bitshares): + """ Create a new asset + """ + + def _create_asset(asset, precision): + max_supply = 1000000000000000 / 10 ** precision if precision > 0 else 1000000000000000 + bitshares.create_asset(asset, precision, max_supply, account=DEFAULT_ACCOUNT) + + return _create_asset + + +@pytest.fixture(scope='session') +def issue_asset(bitshares): + """ Issue asset shares to specified account + """ + + def _issue_asset(asset, amount, to): + asset = Asset(asset, bitshares_instance=bitshares) + asset.issue(amount, to) + + return _issue_asset + + +@pytest.fixture(scope='session') +def create_account(bitshares): + """ Create new account + """ + + def _create_account(account): + parent_account = Account(DEFAULT_ACCOUNT, bitshares_instance=bitshares) + public_key = PublicKey.from_privkey(PRIVATE_KEYS[0], prefix=bitshares.prefix) + bitshares.create_account( + account, + registrar=DEFAULT_ACCOUNT, + referrer=parent_account['id'], + referrer_percent=0, + owner_key=public_key, + active_key=public_key, + memo_key=public_key, + storekeys=False, + ) + + return _create_account + + +@pytest.fixture(scope='session') +def unused_account(bitshares): + """ Find unexistent account + """ + + def _unused_account(): + range = 100000 + while True: + account = 'worker-{}'.format(random.randint(1, range)) + try: + Account(account, bitshares_instance=bitshares) + except AccountDoesNotExistsException: + return account + + return _unused_account + + +@pytest.fixture(scope='session') +def prepare_account(bitshares, unused_account, create_account, create_asset, issue_asset): + """ Ensure an account with specified amounts of assets. Account must not exist! + + :param dict assets: assets to credit account balance with + :param str account: (optional) account name to prepare (default: generate random account name) + :return: account name + :rtype: str + + Example assets: {'FOO': 1000, 'BAR': 5000} + """ + + def _prepare_account(assets, account=None): + # Account name is optional, take unused account name if not specified + if not account: + account = unused_account() + + create_account(account) + + for asset, amount in assets.items(): + # If asset does not exists, create it + try: + Asset(asset, bitshares_instance=bitshares) + except AssetDoesNotExistsException: + create_asset(asset, 5) + + if asset == 'TEST': + bitshares.transfer(account, amount, 'TEST', memo='prepare account', account=DEFAULT_ACCOUNT) + else: + issue_asset(asset, amount, account) + + return account + + return _prepare_account diff --git a/tests/node_config/config.ini b/tests/node_config/config.ini new file mode 100644 index 000000000..75aac9a1d --- /dev/null +++ b/tests/node_config/config.ini @@ -0,0 +1,79 @@ +# Endpoint for P2P node to listen on +p2p-endpoint = 0.0.0.0:9091 + +# P2P nodes to connect to on startup (may specify multiple times) +# seed-node = + +# JSON array of P2P nodes to connect to on startup +seed-nodes = [] + +# Pairs of [BLOCK_NUM,BLOCK_ID] that should be enforced as checkpoints. +# checkpoint = + +# Endpoint for websocket RPC to listen on +rpc-endpoint = 0.0.0.0:8091 + +# Endpoint for TLS websocket RPC to listen on +# rpc-tls-endpoint = + +# The TLS certificate file for this server +# server-pem = + +# Password for this certificate +# server-pem-password = + +# File to read Genesis State from +genesis-json = /etc/bitshares/genesis.json + +# Block signing key to use for init witnesses, overrides genesis file +# dbg-init-key = + +# JSON file specifying API permissions +# api-access = + +# Enable block production, even if the chain is stale. +enable-stale-production = true + +# Percent of witnesses (0-99) that must be participating in order to produce blocks +required-participation = false + +# ID of witness controlled by this node (e.g. "1.6.5", quotes are required, may specify multiple times) +# witness-id = +witness-id = "1.6.1" +witness-id = "1.6.2" +witness-id = "1.6.3" +witness-id = "1.6.4" +witness-id = "1.6.5" +witness-id = "1.6.6" +witness-id = "1.6.7" +witness-id = "1.6.8" +witness-id = "1.6.9" +witness-id = "1.6.10" +witness-id = "1.6.11" + +# Tuple of [PublicKey, WIF private key] (may specify multiple times) +private-key = ["TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3"] + +# Account ID to track history for (may specify multiple times) +# track-account = "1.2.0" + +# Keep only those operations in memory that are related to account history tracking +# partial-operations = true + +# Maximum number of operations per account will be kept in memory +# max-ops-per-account = 10 + +# Track market history by grouping orders into buckets of equal size measured in seconds specified as a JSON array of numbers +# bucket-size = [15,60,300,3600,86400] +bucket-size = [60,300,900,1800,3600,14400,86400] +# for 1 min, 5 mins, 30 mins, 1h, 4 hs and 1 day. i think this should be the default. +# https://github.com/bitshares/bitshares-core/issues/465 + +# How far back in time to track history for each bucket size, measured in the number of buckets (default: 1000) +history-per-size = 1000 + +# Max amount of operations to store in the database, per account (drastically reduces RAM requirements) +max-ops-per-account = 1000 + +# Remove old operation history # objects from RAM +partial-operations = true diff --git a/tests/node_config/genesis.json b/tests/node_config/genesis.json new file mode 100644 index 000000000..5d4a3341c --- /dev/null +++ b/tests/node_config/genesis.json @@ -0,0 +1,364 @@ +{ + "initial_timestamp": "2016-01-18T09:18:25", + "max_core_supply": "1000000000000000", + "initial_parameters": { + "current_fees": { + "parameters": [[ + 0,{ + "fee": 2000000, + "price_per_kbyte": 1000000 + } + ],[ + 1,{ + "fee": 500000 + } + ],[ + 2,{ + "fee": 0 + } + ],[ + 3,{ + "fee": 2000000 + } + ],[ + 4,{} + ],[ + 5,{ + "basic_fee": 500000, + "premium_fee": 200000000, + "price_per_kbyte": 100000 + } + ],[ + 6,{ + "fee": 2000000, + "price_per_kbyte": 100000 + } + ],[ + 7,{ + "fee": 300000 + } + ],[ + 8,{ + "membership_annual_fee": 200000000, + "membership_lifetime_fee": 1000000000 + } + ],[ + 9,{ + "fee": 50000000 + } + ],[ + 10,{ + "symbol3": "50000000000", + "symbol4": "30000000000", + "long_symbol": 500000000, + "price_per_kbyte": 10 + } + ],[ + 11,{ + "fee": 50000000, + "price_per_kbyte": 10 + } + ],[ + 12,{ + "fee": 50000000 + } + ],[ + 13,{ + "fee": 50000000 + } + ],[ + 14,{ + "fee": 2000000, + "price_per_kbyte": 100000 + } + ],[ + 15,{ + "fee": 2000000 + } + ],[ + 16,{ + "fee": 100000 + } + ],[ + 17,{ + "fee": 10000000 + } + ],[ + 18,{ + "fee": 50000000 + } + ],[ + 19,{ + "fee": 100000 + } + ],[ + 20,{ + "fee": 500000000 + } + ],[ + 21,{ + "fee": 2000000 + } + ],[ + 22,{ + "fee": 2000000, + "price_per_kbyte": 10 + } + ],[ + 23,{ + "fee": 2000000, + "price_per_kbyte": 10 + } + ],[ + 24,{ + "fee": 100000 + } + ],[ + 25,{ + "fee": 100000 + } + ],[ + 26,{ + "fee": 100000 + } + ],[ + 27,{ + "fee": 2000000, + "price_per_kbyte": 10 + } + ],[ + 28,{ + "fee": 0 + } + ],[ + 29,{ + "fee": 500000000 + } + ],[ + 30,{ + "fee": 2000000 + } + ],[ + 31,{ + "fee": 100000 + } + ],[ + 32,{ + "fee": 100000 + } + ],[ + 33,{ + "fee": 2000000 + } + ],[ + 34,{ + "fee": 500000000 + } + ],[ + 35,{ + "fee": 100000, + "price_per_kbyte": 10 + } + ],[ + 36,{ + "fee": 100000 + } + ],[ + 37,{} + ],[ + 38,{ + "fee": 2000000, + "price_per_kbyte": 10 + } + ],[ + 39,{ + "fee": 500000, + "price_per_output": 500000 + } + ],[ + 40,{ + "fee": 500000, + "price_per_output": 500000 + } + ],[ + 41,{ + "fee": 500000 + } + ],[ + 42,{} + ],[ + 43,{ + "fee": 2000000 + } + ] + ], + "scale": 10000 + }, + "block_interval": 5, + "maintenance_interval": 86400, + "maintenance_skip_slots": 3, + "committee_proposal_review_period": 1209600, + "maximum_transaction_size": 2048, + "maximum_block_size": 2048000000, + "maximum_time_until_expiration": 86400, + "maximum_proposal_lifetime": 2419200, + "maximum_asset_whitelist_authorities": 10, + "maximum_asset_feed_publishers": 10, + "maximum_witness_count": 1001, + "maximum_committee_count": 1001, + "maximum_authority_membership": 10, + "reserve_percent_of_fee": 2000, + "network_percent_of_fee": 2000, + "lifetime_referrer_percent_of_fee": 3000, + "cashback_vesting_period_seconds": 31536000, + "cashback_vesting_threshold": 10000000, + "count_non_member_votes": true, + "allow_non_member_whitelists": false, + "witness_pay_per_block": 1000000, + "worker_budget_per_day": "50000000000", + "max_predicate_opcode": 1, + "fee_liquidation_threshold": 10000000, + "accounts_per_fee_scale": 1000, + "account_fee_scale_bitshifts": 4, + "max_authority_depth": 2, + "extensions": [] + }, + "initial_accounts": [{ + "name": "init0", + "owner_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "active_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "is_lifetime_member": true + },{ + "name": "init1", + "owner_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "active_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "is_lifetime_member": true + },{ + "name": "init2", + "owner_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "active_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "is_lifetime_member": true + },{ + "name": "init3", + "owner_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "active_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "is_lifetime_member": true + },{ + "name": "init4", + "owner_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "active_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "is_lifetime_member": true + },{ + "name": "init5", + "owner_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "active_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "is_lifetime_member": true + },{ + "name": "init6", + "owner_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "active_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "is_lifetime_member": true + },{ + "name": "init7", + "owner_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "active_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "is_lifetime_member": true + },{ + "name": "init8", + "owner_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "active_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "is_lifetime_member": true + },{ + "name": "init9", + "owner_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "active_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "is_lifetime_member": true + },{ + "name": "init10", + "owner_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "active_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "is_lifetime_member": true + },{ + "name": "faucet", + "owner_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "active_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + "is_lifetime_member": false + } + ], + "initial_assets": [], + "initial_balances": [{ + "owner": "TEST7CwdioNL9my53mj31UYGdcPxhxHfPTBvx", + "asset_symbol": "TEST", + "amount": "1000000000000000" + } + ], + "initial_vesting_balances": [], + "initial_active_witnesses": 11, + "initial_witness_candidates": [{ + "owner_name": "init0", + "block_signing_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" + },{ + "owner_name": "init1", + "block_signing_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" + },{ + "owner_name": "init2", + "block_signing_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" + },{ + "owner_name": "init3", + "block_signing_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" + },{ + "owner_name": "init4", + "block_signing_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" + },{ + "owner_name": "init5", + "block_signing_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" + },{ + "owner_name": "init6", + "block_signing_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" + },{ + "owner_name": "init7", + "block_signing_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" + },{ + "owner_name": "init8", + "block_signing_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" + },{ + "owner_name": "init9", + "block_signing_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" + },{ + "owner_name": "init10", + "block_signing_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" + } + ], + "initial_committee_candidates": [{ + "owner_name": "init0" + },{ + "owner_name": "init1" + },{ + "owner_name": "init2" + },{ + "owner_name": "init3" + },{ + "owner_name": "init4" + },{ + "owner_name": "init5" + },{ + "owner_name": "init6" + },{ + "owner_name": "init7" + },{ + "owner_name": "init8" + },{ + "owner_name": "init9" + },{ + "owner_name": "init10" + } + ], + "initial_worker_candidates": [], + "initial_chain_id": "aa34045518f1469a28fa4578240d5f039afa9959c0b95ce3b39674efa691fb21", + "immutable_parameters": { + "min_committee_member_count": 11, + "min_witness_count": 11, + "num_special_accounts": 0, + "num_special_assets": 0 + } +} diff --git a/tests/test_prepared_testnet.py b/tests/test_prepared_testnet.py new file mode 100644 index 000000000..b27bf2adf --- /dev/null +++ b/tests/test_prepared_testnet.py @@ -0,0 +1,33 @@ +import pytest + +from bitshares.account import Account +from bitshares.asset import Asset + + +@pytest.fixture(scope='module') +def assets(create_asset): + create_asset('MYBASE', 0) + create_asset('MYQUOTE', 5) + + +@pytest.fixture(scope='module') +def accounts(assets, prepare_account): + prepare_account({'MYBASE': 10000, 'MYQUOTE': 2000}, account='worker1') + prepare_account({'MYBASE': 20000, 'MYQUOTE': 5000, 'TEST': 10000}, account='worker2') + + +def test_worker_balance(bitshares, accounts): + a = Account('worker2', bitshares_instance=bitshares) + assert a.balance('MYBASE') == 20000 + assert a.balance('MYQUOTE') == 5000 + assert a.balance('TEST') == 10000 + + +def test_asset_base(bitshares, assets): + a = Asset('MYBASE', full=True, bitshares_instance=bitshares) + assert a['dynamic_asset_data']['current_supply'] > 1000 + + +def test_asset_quote(bitshares, assets): + a = Asset('MYQUOTE', full=True, bitshares_instance=bitshares) + assert a['dynamic_asset_data']['current_supply'] > 1000 From 97812942912ff4ce48b0fd0ccdfb25227645aacc Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 31 Mar 2019 14:35:04 +0500 Subject: [PATCH 1283/1846] Rename tests/test.py -> tests/test_worker_infrastructure.py --- tests/{test.py => test_worker_infrastructure.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test.py => test_worker_infrastructure.py} (100%) diff --git a/tests/test.py b/tests/test_worker_infrastructure.py similarity index 100% rename from tests/test.py rename to tests/test_worker_infrastructure.py From e1774db8f59db327986b8c8b13988bc25504b4c6 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 31 Mar 2019 14:37:49 +0500 Subject: [PATCH 1284/1846] Refactor test_worker_infrastructure.py --- tests/test_worker_infrastructure.py | 64 ++++++++++++----------------- 1 file changed, 26 insertions(+), 38 deletions(-) mode change 100755 => 100644 tests/test_worker_infrastructure.py diff --git a/tests/test_worker_infrastructure.py b/tests/test_worker_infrastructure.py old mode 100755 new mode 100644 index a4c0a88e9..786e27bf6 --- a/tests/test_worker_infrastructure.py +++ b/tests/test_worker_infrastructure.py @@ -1,52 +1,40 @@ -#!/usr/bin/python3 import threading -import unittest import logging import time -import os +import pytest from dexbot.worker import WorkerInfrastructure -from bitshares.bitshares import BitShares +logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s') -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' - } - } -} - -# User needs to put a key in -KEYS = [os.environ['DEXBOT_TEST_WIF']] +@pytest.fixture(scope='module') +def account(prepare_account): + account = prepare_account({'MYBASE': 10000, 'MYQUOTE': 2000}) + return account -class TestDexbot(unittest.TestCase): - - def test_dexbot(self): - bitshares_instance = BitShares(node=TEST_CONFIG['node'], keys=KEYS) - worker_infrastructure = WorkerInfrastructure(config=TEST_CONFIG, - bitshares_instance=bitshares_instance) +@pytest.fixture(scope='module') +def config(bitshares, account): + config = { + 'node': '{}'.format(bitshares.rpc.url), + 'workers': { + 'echo': {'account': '{}'.format(account), 'market': 'MYQUOTE/MYBASE', 'module': 'dexbot.strategies.echo'} + }, + } + return config - def wait_then_stop(): - time.sleep(20) - worker_infrastructure.do_next_tick(worker_infrastructure.stop) - stopper = threading.Thread(target=wait_then_stop) - stopper.start() - worker_infrastructure.run() - stopper.join() +def test_worker_infrastructure(bitshares, config): + """ Test whether dexbot core is able to work + """ + worker_infrastructure = WorkerInfrastructure(config=config, bitshares_instance=bitshares) + def wait_then_stop(): + time.sleep(1) + worker_infrastructure.do_next_tick(worker_infrastructure.stop(pause=True)) -if __name__ == '__main__': - unittest.main() + stopper = threading.Thread(target=wait_then_stop) + stopper.start() + worker_infrastructure.run() + stopper.join() From ce6aa64130d0d4c9cf135114ba00dd7456035cdb Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 1 Apr 2019 23:03:13 +0500 Subject: [PATCH 1285/1846] Remove unused import --- dexbot/worker.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dexbot/worker.py b/dexbot/worker.py index ccdaf126c..a79cfc1e0 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -8,7 +8,6 @@ import dexbot.errors as errors from dexbot.strategies.base import StrategyBase -from bitshares import BitShares from bitshares.notify import Notify from bitshares.instance import shared_bitshares_instance From 152e74afda2fb724bc011b0508b03a23b6f225ed Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 1 Apr 2019 23:03:58 +0500 Subject: [PATCH 1286/1846] Emit "worker is disabled" only once No need to flood the logs, single error should be enough. Closes: #474 --- dexbot/worker.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/dexbot/worker.py b/dexbot/worker.py index a79cfc1e0..d691b4485 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -115,7 +115,11 @@ def on_block(self, data): 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: + if worker_name not in self.workers: + continue + elif self.workers[worker_name].disabled: + self.workers[worker_name].log.error('Worker "{}" is disabled'.format(worker_name)) + self.config["workers"].pop(worker_name) continue try: self.workers[worker_name].ontick(data) @@ -134,7 +138,8 @@ def on_market(self, data): 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)) + self.workers[worker_name].log.error('Worker "{}" is disabled'.format(worker_name)) + self.config["workers"].pop(worker_name) continue if worker["market"] == data.market: try: @@ -152,7 +157,8 @@ 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)) + self.workers[worker_name].log.error('Worker "{}" is disabled'.format(worker_name)) + self.config["workers"].pop(worker_name) continue if worker["account"] == account["name"]: try: From ba710f8c4f5cecab4e9fed6e6ce0ae840b50e551 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 2 Apr 2019 23:32:09 +0500 Subject: [PATCH 1287/1846] Remove unused import --- docs/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 2c45b43b5..bd7c950aa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,7 +15,6 @@ import sys import os -import shlex # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the From cd8c91b39eab53ff491507f3a1efada434c6c08c Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 2 Apr 2019 23:34:04 +0500 Subject: [PATCH 1288/1846] Update Author / Copyright info --- docs/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index bd7c950aa..f2dc059dc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,8 +48,8 @@ # General information about the project. project = 'DEXBot' -copyright = '2017, ChainSquad GmbH' -author = 'Fabian Schuh' +copyright = '2017-2019, DEXBot Team and contributors' +author = 'DEXBot Team' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -224,7 +224,7 @@ # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'DEXBot.tex', 'DEXBot Documentation', - 'Fabian Schuh', 'manual'), + 'DEXBot Team', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of From bee1e0c50b452b547a5a48810a34d2d40493959c Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 2 Apr 2019 23:29:49 +0500 Subject: [PATCH 1289/1846] Enable automatic building of API reference --- .gitignore | 5 ++++- docs/conf.py | 8 +++++++- docs/requirements.txt | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) mode change 100644 => 100755 docs/conf.py create mode 100644 docs/requirements.txt diff --git a/.gitignore b/.gitignore index 7c4f705e7..9b078fcb9 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,9 @@ coverage.xml docs/_build/ docs/html +# Autogenerated reference +docs/reference + # PyBuilder target/ @@ -78,4 +81,4 @@ venv/ dexbot/views/ui/**/*_ui.py dexbot/resources/*_rc.py archive -*~ \ No newline at end of file +*~ diff --git a/docs/conf.py b/docs/conf.py old mode 100644 new mode 100755 index f2dc059dc..785aa2f57 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,7 +30,13 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ["sphinx.ext.autodoc"] +extensions = ['sphinx.ext.autodoc', 'sphinxcontrib.apidoc'] + +# apidoc settings +apidoc_module_dir = '../dexbot' +apidoc_output_dir = 'reference' +apidoc_excluded_paths = ['tests'] +apidoc_separate_modules = True # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..b0f844724 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +sphinxcontrib-apidoc From 8c5b46d345f4bee65be247c60dec5f84ad97fd30 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 3 Apr 2019 00:07:33 +0500 Subject: [PATCH 1290/1846] Remove outdated docs --- docs/echo.rst | 14 ----------- docs/index.rst | 7 ++---- docs/wall.rst | 63 -------------------------------------------------- 3 files changed, 2 insertions(+), 82 deletions(-) delete mode 100644 docs/echo.rst delete mode 100644 docs/wall.rst diff --git a/docs/echo.rst b/docs/echo.rst deleted file mode 100644 index f3f06e93f..000000000 --- a/docs/echo.rst +++ /dev/null @@ -1,14 +0,0 @@ -******************** -Simple Echo Strategy -******************** - -API ---- -.. autoclass:: dexbot.strategies.echo.Echo - :members: - -Full Source Code ----------------- -.. literalinclude:: ../dexbot/strategies/echo.py - :language: python - :linenos: diff --git a/docs/index.rst b/docs/index.rst index 8c5f34fc1..45c285a94 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,6 +9,7 @@ Basics setup configuration + manual Strategies ---------- @@ -16,20 +17,16 @@ Strategies .. toctree:: :maxdepth: 1 - wall - Developing own Strategies ------------------------- .. toctree:: :maxdepth: 1 - manual - basestrategy storage statemachine + strategybase events - echo Indices and tables ================== diff --git a/docs/wall.rst b/docs/wall.rst deleted file mode 100644 index 669e1c28c..000000000 --- a/docs/wall.rst +++ /dev/null @@ -1,63 +0,0 @@ -************* -Wall Strategy -************* - -This strategy simply places a buy and a sell wall into a specific market -using a specified account. - -Example Configuration ---------------------- -.. code-block:: yaml - - # BitShares end point - node: "wss://node.bitshares.eu" - - # List of Bots - bots: - - # Only a single Walls Bot - Walls: - - # The Walls strategy module and class - module: dexbot.strategies.walls - bot: Walls - - # The market to serve - market: HERO:BTS - - # The account to sue - account: hero-market-maker - - # We shall bundle operations into a single transaction - bundle: True - - # Test your conditions every x blocks - test: - blocks: 10 - - # Where the walls should be - target: - - # They relate to the price feed - reference: feed - - # There should be an offset - offsets: - buy: 2.5 - sell: 2.5 - - # We'd like to use x amount of quote (here: HERO) - # in the walls - amount: - buy: 5.0 - sell: 5.0 - - # When the price moves by more than 2%, update the walls - threshold: 2 - - -Source Code ------------ -.. literalinclude:: ../dexbot/strategies/walls.py - :language: python - :linenos: From eac959b3d5d820b69cb3eb22896b0fb82a34b5ad Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 3 Apr 2019 00:08:03 +0500 Subject: [PATCH 1291/1846] Fix references --- docs/strategybase.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/strategybase.rst b/docs/strategybase.rst index 910239d64..7dd8816a1 100644 --- a/docs/strategybase.rst +++ b/docs/strategybase.rst @@ -3,11 +3,12 @@ Strategy Base ************* All strategies should inherit -:class:`dexbot.strategies.StrategyBase` which simplifies and +:class:`dexbot.strategies.base.StrategyBase` which simplifies and unifies the development of new strategies. API --- -.. autoclass:: dexbot.strategies.StrategyBase +.. autoclass:: dexbot.strategies.base.StrategyBase :members: + :noindex: From 7c0f8ecc719ece7ba23695c64abc2c83b02dd8fb Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 3 Apr 2019 00:08:30 +0500 Subject: [PATCH 1292/1846] Fix sphinx docstrings warnings --- dexbot/strategies/base.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index efbe902e6..44029acef 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -33,8 +33,8 @@ class StrategyBase(Storage, StateMachine, Events): All prices are passed and returned as BASE/QUOTE. (In the BREAD:USD market that would be USD/BREAD, 2.5 USD / 1 BREAD). - - Buy orders reserve BASE - - Sell orders reserve QUOTE + - Buy orders reserve BASE + - Sell orders reserve QUOTE Strategy inherits: * :class:`dexbot.storage.Storage` : Stores data to sqlite database @@ -354,10 +354,11 @@ def count_asset(self, order_ids=None, return_asset=False): """ Returns the combined amount of the given order ids and the account balance The amounts are returned in quote and base assets of the market + Todo: When would we want the sum of a subset of orders? Why order_ids? Maybe just specify asset? + :param list | order_ids: list of order ids to be added to the balance :param bool | return_asset: true if returned values should be Amount instances :return: dict with keys quote and base - Todo: When would we want the sum of a subset of orders? Why order_ids? Maybe just specify asset? """ quote = 0 base = 0 @@ -899,7 +900,7 @@ def is_buy_order(self, order): """ Check whether an order is buy order :param dict | order: dict or Order object - :return bool + :return: bool """ # Check if the order is buy order, by comparing asset symbol of the order and the market if order['base']['symbol'] == self.market['base']['symbol']: @@ -929,7 +930,7 @@ def is_sell_order(self, order): """ Check whether an order is sell order :param dict | order: dict or Order object - :return bool + :return: bool """ # Check if the order is sell order, by comparing asset symbol of the order and the market if order['base']['symbol'] == self.market['quote']['symbol']: From ff23cedf2a017ac7299559296aad92920fe4cc5f Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 3 Apr 2019 10:32:08 +0500 Subject: [PATCH 1293/1846] Add readthedocs config --- .gitignore | 3 +++ .readthedocs.yml | 6 ++++++ 2 files changed, 9 insertions(+) create mode 100644 .readthedocs.yml diff --git a/.gitignore b/.gitignore index 9b078fcb9..fc318b9bb 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,6 @@ dexbot/views/ui/**/*_ui.py dexbot/resources/*_rc.py archive *~ + +# Keep readthedocs config +!.readthedocs.yml diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..31b97a4d4 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,6 @@ +version: 2 +python: + version: 3.6 + install: + - requirements: docs/requirements.txt + - requirements: requirements.txt From cd31ff016e507ab09fe3401a27eea116d91ed98f Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 3 Apr 2019 11:33:24 +0500 Subject: [PATCH 1294/1846] Improve docs building on readthedocs --- .readthedocs.yml | 1 - docs/conf.py | 5 +++++ docs/requirements.txt | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 31b97a4d4..884df65fe 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -3,4 +3,3 @@ python: version: 3.6 install: - requirements: docs/requirements.txt - - requirements: requirements.txt diff --git a/docs/conf.py b/docs/conf.py index 785aa2f57..57908ba21 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -38,6 +38,11 @@ apidoc_excluded_paths = ['tests'] apidoc_separate_modules = True +# Mock some modules to fix building on readthedocs.io as we cannot just install +# everything from requirements.txt as readthedocs doesn't allow cpython modules +# building +autodoc_mock_imports = ['PyQt5', 'bitshares', 'bitsharesapi', 'bitsharesbase'] + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/requirements.txt b/docs/requirements.txt index b0f844724..58de52f7c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1,7 @@ sphinxcontrib-apidoc +ccxt +appdirs +click +ruamel.yaml +sqlalchemy +events From 59ea13b34696491862573cafa574c3762c1aa61f Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 3 Apr 2019 11:33:48 +0500 Subject: [PATCH 1295/1846] Fix references --- docs/statemachine.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/statemachine.rst b/docs/statemachine.rst index 4c1ebc230..c5e09f591 100644 --- a/docs/statemachine.rst +++ b/docs/statemachine.rst @@ -7,10 +7,11 @@ strategy. Similar to :doc:`storage`, the methods of this class can be used in your strategy directly, e.g., via ``self.get_state()``, since the class is -inherited by :doc:`basestrategy`. +inherited by :doc:`strategybase`. API --- .. autoclass:: dexbot.statemachine.StateMachine :members: + :noindex: From 54c303b0160528bc7335b0a2855a0a3ba9e80f30 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 3 Apr 2019 11:34:29 +0500 Subject: [PATCH 1296/1846] Add storagedemo example from previously removed strategy --- docs/storage.rst | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/storage.rst b/docs/storage.rst index d9c02c6d1..4f813ab21 100644 --- a/docs/storage.rst +++ b/docs/storage.rst @@ -40,16 +40,31 @@ Where ```` is ``dexbot`` and ```` is Simple example -------------- +.. code-block:: python -.. literalinclude:: ../dexbot/strategies/storagedemo.py - :language: python - :linenos: + from dexbot.basestrategy import BaseStrategy + + + class Strategy(BaseStrategy): + """ + Storage demo strategy + Strategy that prints all new blocks in the blockchain + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ontick += self.tick + + def tick(self, i): + print("previous block: %s" % self["block"]) + print("new block: %s" % i) + self["block"] = i **Example Output:** :: - Current Wallet Passphrase: + Current Wallet Passphrase: previous block: None new block: 008c4c2424e6394ad4bf5a9756ae2ee883b0e049 previous block: 008c4c2424e6394ad4bf5a9756ae2ee883b0e049 From b6fd0e76715268cd8f9e1870a91770250cfc4ee3 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 9 Apr 2019 09:09:41 +0300 Subject: [PATCH 1297/1846] Change dexbot version number to 0.10.2 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 62fae13fd..e5881e430 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.10.1' +VERSION = '0.10.2' AUTHOR = 'Codaone Oy' __version__ = VERSION From bf9825bad21c5d9d87f8dc6f51e6d3e9bdaf78e9 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 9 Apr 2019 09:14:22 +0300 Subject: [PATCH 1298/1846] Change dexbot version number to 0.10.3 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index e5881e430..a8dc8d08c 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.10.2' +VERSION = '0.10.3' AUTHOR = 'Codaone Oy' __version__ = VERSION From 7928186ab6cfd7e198271f78bdc1ae64ffe63c67 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 9 Apr 2019 14:15:07 +0500 Subject: [PATCH 1299/1846] Pin versions in requirements-dev.txt --- requirements-dev.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index edd9a9b55..ada39d9da 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,6 @@ -pytest -docker +# Include base requirements +-r requirements.txt + +# Packages needed for running testsuite +docker==3.7.2 +pytest==4.4.0 From c590ce0398f6f70fc008abc1e960a5c536cd9eaa Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 9 Apr 2019 14:19:36 +0300 Subject: [PATCH 1300/1846] Add pep-test to MakeFile Added PEP8 testing to the MakeFile which does the following: 1. Installs Flake8 3.7.7 with dependencies 2. Runs flake8 test for dexbot folder 3. Uninstalls Flake8 with dependencies so that the project's build does not grow too much in size --- Makefile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Makefile b/Makefile index 8ff073d01..6fc3d8191 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,14 @@ pip-user: lint: flake8 dexbot/ +pep-test: + pip install flake8==3.7.7 + flake8 dexbot/ + pip uninstall flake8 + pip uninstall pyflakes + pip uninstall pycodestyle + pip uninstall mccabe + build: pip python3 setup.py build From 64bea5a55ad06fd08e50c9129886a214fa949ddb Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 9 Apr 2019 14:21:18 +0300 Subject: [PATCH 1301/1846] Modify .travis.yml Runs pep-test before building --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index f075117c5..0ef3afa85 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,9 @@ install: - pip install pyinstaller - pip install --upgrade setuptools - make install +before_script: + # PEP8 linting + - make pep-test script: - echo "@TODO - Running tests..." - pyinstaller --distpath dist/$TRAVIS_OS_NAME gui.spec From ecd180bc11471b847201f0bae1e7e50427b5a772 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 9 Apr 2019 14:21:50 +0300 Subject: [PATCH 1302/1846] Add Flake8 configuration to setup.cfg Includes the max line length of 120 and excludes some auto generated python files --- setup.cfg | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/setup.cfg b/setup.cfg index 10e6d8ae2..d1aa8d461 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,3 +2,10 @@ description-file=README.md [aliases] test=pytest +[flake8] +max-line-length = 120 +count = true +statistics = true +exclude = + dexbot/views/ui/, + dexbot/resources/ From d7f9140e21d4317f99738f9d688e50f62d979354 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 10 Apr 2019 07:16:07 +0300 Subject: [PATCH 1303/1846] Fix MakeFile pep-test Moved all the pip install packages on one line and fixed the package installation --- Makefile | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 6fc3d8191..ad48bc6ba 100644 --- a/Makefile +++ b/Makefile @@ -27,10 +27,7 @@ lint: pep-test: pip install flake8==3.7.7 flake8 dexbot/ - pip uninstall flake8 - pip uninstall pyflakes - pip uninstall pycodestyle - pip uninstall mccabe + pip uninstall -y flake8 pyflakes pycodestyle mccabe build: pip python3 setup.py build From c13949f3ccea6623d324397f2df4ad82228c900e Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 10 Apr 2019 07:25:37 +0300 Subject: [PATCH 1304/1846] Remove unused import from worker.py --- dexbot/worker.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dexbot/worker.py b/dexbot/worker.py index ccdaf126c..a79cfc1e0 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -8,7 +8,6 @@ import dexbot.errors as errors from dexbot.strategies.base import StrategyBase -from bitshares import BitShares from bitshares.notify import Notify from bitshares.instance import shared_bitshares_instance From 1b862e154b3b5f0fca725cb32fcca1cbb9df8e12 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 10 Apr 2019 07:26:54 +0300 Subject: [PATCH 1305/1846] Fix PEP8 error 'variable is assigned but never used' --- 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 ad1a83701..927750287 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -98,7 +98,7 @@ def handle_add_worker(self): @gui_error def handle_open_settings(self): settings_dialog = SettingsView() - return_value = settings_dialog.exec_() + settings_dialog.exec_() @staticmethod def handle_open_documentation(): From b2cb3e85086375d4fec2ff6c647d23f80d29ca58 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 10 Apr 2019 07:28:09 +0300 Subject: [PATCH 1306/1846] Change imports in worker_details.py - Removed unused imports - Changed 'dexbot.helper' import to be more specific - Fixed 'os' import, since it doesn't come from 'dexbot.helper' anymore --- dexbot/views/worker_details.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dexbot/views/worker_details.py b/dexbot/views/worker_details.py index 5bb7c97ad..138fe0e3f 100644 --- a/dexbot/views/worker_details.py +++ b/dexbot/views/worker_details.py @@ -1,11 +1,13 @@ +import os + from dexbot.controllers.worker_details_controller import WorkerDetailsController -from dexbot.helper import * +from dexbot.helper import get_user_data_directory from dexbot.views.ui.worker_details_window_ui import Ui_details_dialog from dexbot.views.ui.tabs.graph_tab_ui import Ui_Graph_Tab from dexbot.views.ui.tabs.table_tab_ui import Ui_Table_Tab from dexbot.views.ui.tabs.text_tab_ui import Ui_Text_Tab -from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5 import QtWidgets from PyQt5.QtWidgets import QWidget import importlib From 8d98f6e296ee324ecf96a8c70e039919b0048735 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 10 Apr 2019 07:30:15 +0300 Subject: [PATCH 1307/1846] Remove unused imports from strategy_controller.py --- dexbot/controllers/strategy_controller.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 7ea6e614a..2bd2d1689 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -1,7 +1,3 @@ -import collections - -from dexbot.views.errors import gui_error - from PyQt5 import QtWidgets From 68621697b1101334a80a91d452692ce1ba0ddf56 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 10 Apr 2019 07:31:47 +0300 Subject: [PATCH 1308/1846] Fix PEP8 error trailing whitespace --- dexbot/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/helper.py b/dexbot/helper.py index 4f778b4b7..352616aa4 100644 --- a/dexbot/helper.py +++ b/dexbot/helper.py @@ -86,7 +86,7 @@ def initialize_orders_log(): def find_external_strategies(): """Use setuptools introspection to find third-party strategies the user may have installed. Packages that provide a strategy should export a setuptools "entry point" (see setuptools docs) - with group "dexbot.strategy", "name" is the display name of the strategy. + with group "dexbot.strategy", "name" is the display name of the strategy. Only set the module not any attribute (because it would always be a class called "Strategy") If you want a handwritten graphical UI, define "Ui_Form" and "StrategyController" in the same module From d7989592953b2f7568a4abbf34e28cb9cc028bea Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 10 Apr 2019 07:37:07 +0300 Subject: [PATCH 1309/1846] Fix spacing PEP8 errors Added some missing new lines from end of the files and removed one extra --- dexbot/qt_queue/queue_dispatcher.py | 2 +- dexbot/storage.py | 1 + dexbot/views/edit_worker.py | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/qt_queue/queue_dispatcher.py b/dexbot/qt_queue/queue_dispatcher.py index 4fce8b82f..ef7903f4e 100644 --- a/dexbot/qt_queue/queue_dispatcher.py +++ b/dexbot/qt_queue/queue_dispatcher.py @@ -27,4 +27,4 @@ class _Event(QEvent): def __init__(self, callback): # Thread-safe QEvent.__init__(self, _Event.EVENT_TYPE) - self.callback = callback \ No newline at end of file + self.callback = callback diff --git a/dexbot/storage.py b/dexbot/storage.py index 96203c9a8..115419051 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -358,6 +358,7 @@ def _get_recent_balance_entry(self, account, worker, base_asset, quote_asset, to self._set_result(token, result) + # Derive sqlite file directory data_dir = user_data_dir(APP_NAME, AUTHOR) sqlDataBaseFile = os.path.join(data_dir, storageDatabase) diff --git a/dexbot/views/edit_worker.py b/dexbot/views/edit_worker.py index 0e0573444..8e35faba6 100644 --- a/dexbot/views/edit_worker.py +++ b/dexbot/views/edit_worker.py @@ -50,4 +50,3 @@ def __init__(self, parent_widget, bitshares_instance, worker_name, config): def handle_remove(self): self.parent_widget.remove_widget_dialog() self.reject() - From 38c2bf9c8b0c3fbc2394b2d188afef63dbe47e32 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 10 Apr 2019 07:42:31 +0300 Subject: [PATCH 1310/1846] Remove unused variables from external_feeds/gecko_feed.py --- dexbot/strategies/external_feeds/gecko_feed.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/dexbot/strategies/external_feeds/gecko_feed.py b/dexbot/strategies/external_feeds/gecko_feed.py index ce4efc701..21b9722ef 100644 --- a/dexbot/strategies/external_feeds/gecko_feed.py +++ b/dexbot/strategies/external_feeds/gecko_feed.py @@ -32,9 +32,6 @@ def _get_market_price(base, quote): current_price = None for entry in ticker: current_price = entry['current_price'] - high_24h = entry['high_24h'] - low_24h = entry['low_24h'] - total_volume = entry['total_volume'] return current_price except TypeError: return None From 3df745aa0a8620ed1e29f77a718636feefe9ab1a Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 10 Apr 2019 07:43:39 +0300 Subject: [PATCH 1311/1846] Fix invalid escape sequence PEP8 error --- dexbot/strategies/external_feeds/process_pair.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/external_feeds/process_pair.py b/dexbot/strategies/external_feeds/process_pair.py index 2c053f301..397dd362e 100644 --- a/dexbot/strategies/external_feeds/process_pair.py +++ b/dexbot/strategies/external_feeds/process_pair.py @@ -16,7 +16,7 @@ def filter_prefix_symbol(symbol): # Example open.USD or bridge.USD, remove leading bit up to . base = '' if re.match(r'^[a-zA-Z](.*)\.(.*)', symbol): - base = re.sub('(.*)\.', '', symbol) + base = re.sub(r'(.*)\.', '', symbol) else: base = symbol return base From 14a1be0a363e014c8fbc661730dea88cefb0f290 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 10 Apr 2019 07:43:58 +0300 Subject: [PATCH 1312/1846] Fix invalid spacing on staggered_orders.py --- dexbot/strategies/staggered_orders.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 2210ce1a0..74e9402e2 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -688,10 +688,10 @@ def allocate_asset(self, asset, asset_balance): elif (self.mode == 'valley' or (self.mode == 'buy_slope' and asset == 'quote') or (self.mode == 'sell_slope' and asset == 'base')): - opposite_asset_limit = closest_opposite_order['base']['amount'] - own_asset_limit = None - self.log.debug('Limiting {} order by opposite order: {:.{prec}f} {}'.format( - order_type, opposite_asset_limit, opposite_symbol, prec=opposite_precision)) + opposite_asset_limit = closest_opposite_order['base']['amount'] + own_asset_limit = None + self.log.debug('Limiting {} order by opposite order: {:.{prec}f} {}'.format( + order_type, opposite_asset_limit, opposite_symbol, prec=opposite_precision)) allow_partial = True if asset == 'quote' else False self.place_closer_order(asset, closest_own_order, own_asset_limit=own_asset_limit, opposite_asset_limit=opposite_asset_limit, allow_partial=allow_partial) From d244cb348468878edc4b24f6bee6d80cc7cb202f Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 10 Apr 2019 07:44:26 +0300 Subject: [PATCH 1313/1846] Remove unused imports --- dexbot/views/errors.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dexbot/views/errors.py b/dexbot/views/errors.py index 14529fbbf..bb56ce3f0 100644 --- a/dexbot/views/errors.py +++ b/dexbot/views/errors.py @@ -6,7 +6,6 @@ from dexbot.qt_queue.idle_queue import idle_add from PyQt5 import QtWidgets, QtCore -from PyQt5.QtGui import QIcon, QPixmap class PyQtHandler(logging.Handler): From 6f2be3666b00c55661452e313a5b894d8037af35 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 10 Apr 2019 07:44:44 +0300 Subject: [PATCH 1314/1846] Remove unused variable from external_feeds/waves_feed.py --- dexbot/strategies/external_feeds/waves_feed.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/external_feeds/waves_feed.py b/dexbot/strategies/external_feeds/waves_feed.py index 72fb4b2a8..62bf0b9ed 100644 --- a/dexbot/strategies/external_feeds/waves_feed.py +++ b/dexbot/strategies/external_feeds/waves_feed.py @@ -20,8 +20,8 @@ def get_last_price(base, quote): asyncio.set_event_loop(async_loop) ticker = asyncio.get_event_loop().run_until_complete(get_json(WAVES_URL + market_bq)) current_price = ticker['24h_close'] - except Exception as exeption: - pass # No pair found on waves dex for external price. + except Exception: + pass # No pair found on waves dex for external price. return current_price From 3673d2f3f7effdb88df8fb574502e8ba2a608369 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 10 Apr 2019 09:34:32 +0300 Subject: [PATCH 1315/1846] Change MakeFile to use Python 3 --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index ad48bc6ba..7feb2ce9f 100644 --- a/Makefile +++ b/Makefile @@ -25,9 +25,9 @@ lint: flake8 dexbot/ pep-test: - pip install flake8==3.7.7 + python3 -m pip install flake8==3.7.7 flake8 dexbot/ - pip uninstall -y flake8 pyflakes pycodestyle mccabe + python3 -m pip uninstall -y flake8 pyflakes pycodestyle mccabe build: pip python3 setup.py build From 4fdfaa726c4ff32037db11d798f561a78de00686 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 10 Apr 2019 09:59:09 +0300 Subject: [PATCH 1316/1846] Change MakeFile Added tests/ folder to lint and pep-test, since this was missing --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 7feb2ce9f..cd95a9fc0 100644 --- a/Makefile +++ b/Makefile @@ -22,11 +22,11 @@ pip-user: python3 -m pip install --user -r requirements.txt lint: - flake8 dexbot/ + flake8 dexbot/ tests/ pep-test: python3 -m pip install flake8==3.7.7 - flake8 dexbot/ + flake8 dexbot/ tests/ python3 -m pip uninstall -y flake8 pyflakes pycodestyle mccabe build: pip From 1f723f3e45f5a513a34cac3e9fa69618d7c2b8e7 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 10 Apr 2019 09:59:55 +0300 Subject: [PATCH 1317/1846] Fix PEP8 errors on test files --- tests/process_pair_test.py | 2 +- tests/styles_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/process_pair_test.py b/tests/process_pair_test.py index 59189de84..ae8f09897 100644 --- a/tests/process_pair_test.py +++ b/tests/process_pair_test.py @@ -19,7 +19,7 @@ def test_split_symbol(): group = ['BTC:USD', 'STEEM/USD'] pair = [split_pair(symbol) for symbol in group] print('original:', group, 'result:', pair, sep=' ') - except Exception as e: + except Exception: pass diff --git a/tests/styles_test.py b/tests/styles_test.py index e9dac1143..c3b7f5b7e 100644 --- a/tests/styles_test.py +++ b/tests/styles_test.py @@ -9,4 +9,4 @@ print(red("red style test")) print(pink("pink style test")) print(bold("bold style test")) - print(underline("underline test")) \ No newline at end of file + print(underline("underline test")) From 465c2f3339d5c5c5745d92911349b5877a318b47 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 10 Apr 2019 11:29:00 +0300 Subject: [PATCH 1318/1846] Change dexbot version number to 0.10.4 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index a8dc8d08c..afc83f4f4 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.10.3' +VERSION = '0.10.4' AUTHOR = 'Codaone Oy' __version__ = VERSION From 23ec76939184e72dd1a50bbc8a1ad51cc9778865 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Wed, 10 Apr 2019 22:30:25 -0700 Subject: [PATCH 1319/1846] remove state machine ; not used --- dexbot/statemachine.py | 27 --------------------------- dexbot/strategies/base.py | 7 +------ 2 files changed, 1 insertion(+), 33 deletions(-) delete mode 100644 dexbot/statemachine.py diff --git a/dexbot/statemachine.py b/dexbot/statemachine.py deleted file mode 100644 index 8a2dd26c0..000000000 --- a/dexbot/statemachine.py +++ /dev/null @@ -1,27 +0,0 @@ -class StateMachine: - """ Generic state machine - """ - - def __init__(self, *args, **kwargs): - self.states = set() - self.state = None - - def add_state(self, state): - """ Add a new state to the state machine - - :param str state: Name of the state - """ - self.states.add(state) - - def set_state(self, state): - """ Change state of the state machine - - :param str state: Name of the new state - """ - assert state in self.states, "Unknown State" - self.state = state - - def get_state(self): - """ Return state of state machine - """ - return self.state diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 8d9fb6f28..fbc37d7a3 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -6,7 +6,6 @@ from dexbot.config import Config from dexbot.storage import Storage -from dexbot.statemachine import StateMachine from dexbot.helper import truncate from dexbot.strategies.external_feeds.price_feed import PriceFeed from dexbot.qt_queue.idle_queue import idle_add @@ -28,7 +27,7 @@ MAX_TRIES = 3 -class StrategyBase(Storage, StateMachine, Events): +class StrategyBase(Storage, Events): """ A strategy based on this class is intended to work in one market. This class contains most common methods needed by the strategy. @@ -39,7 +38,6 @@ class StrategyBase(Storage, StateMachine, Events): Strategy inherits: * :class:`dexbot.storage.Storage` : Stores data to sqlite database - * :class:`dexbot.statemachine.StateMachine` * ``Events`` Available attributes: @@ -109,9 +107,6 @@ def __init__(self, # Storage Storage.__init__(self, name) - # Statemachine - StateMachine.__init__(self, name) - # Events Events.__init__(self) From dc1b79dd93625ff2e32e3cb52031d1b8fae99080 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Thu, 11 Apr 2019 08:43:28 +0300 Subject: [PATCH 1320/1846] Change dexbot version number to 0.10.5 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index afc83f4f4..f443ece59 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.10.4' +VERSION = '0.10.5' AUTHOR = 'Codaone Oy' __version__ = VERSION From 72f65a4193788a78d9f268ac8ccad97ff76574b2 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 11 Apr 2019 01:32:57 -0700 Subject: [PATCH 1321/1846] modified class name to fit strategy name --- dexbot/strategies/echo.py | 2 +- dexbot/strategies/relative_orders.py | 2 +- dexbot/strategies/staggered_orders.py | 2 +- dexbot/strategies/strategy_template.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/echo.py b/dexbot/strategies/echo.py index 264f1027c..225eb08bb 100644 --- a/dexbot/strategies/echo.py +++ b/dexbot/strategies/echo.py @@ -1,7 +1,7 @@ from dexbot.strategies.base import StrategyBase -class Strategy(StrategyBase): +class EchoStrategy(StrategyBase): """ Echo strategy Strategy that logs all events within the blockchain """ diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 3ce0f7b14..46017c789 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -5,7 +5,7 @@ from .config_parts.relative_config import RelativeConfig -class Strategy(StrategyBase): +class RelativeStrategy(StrategyBase): """ Relative Orders strategy """ diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 74e9402e2..c16a67b2c 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -10,7 +10,7 @@ from .config_parts.staggered_config import StaggeredConfig -class Strategy(StrategyBase): +class StaggeredStrategy(StrategyBase): """ Staggered Orders strategy """ @classmethod diff --git a/dexbot/strategies/strategy_template.py b/dexbot/strategies/strategy_template.py index c5ac7f9ac..2be281b0f 100644 --- a/dexbot/strategies/strategy_template.py +++ b/dexbot/strategies/strategy_template.py @@ -12,7 +12,7 @@ STRATEGY_NAME = 'Strategy Template' -class Strategy(StrategyBase): +class TemplateStrategy(StrategyBase): """ Replace with the name of the strategy. From cb30c934fc4aef8732d06ae255f8aa0f300db0a8 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Fri, 12 Apr 2019 18:31:01 +0200 Subject: [PATCH 1322/1846] Import QApplication from QtWidgets --- dexbot/gui.py | 2 +- dexbot/qt_queue/queue_dispatcher.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/gui.py b/dexbot/gui.py index d1543f70b..46670d82b 100644 --- a/dexbot/gui.py +++ b/dexbot/gui.py @@ -7,7 +7,7 @@ from dexbot.views.unlock_wallet import UnlockWalletView from dexbot.views.create_wallet import CreateWalletView -from PyQt5.Qt import QApplication +from PyQt5.QtWidgets import QApplication from bitshares import BitShares diff --git a/dexbot/qt_queue/queue_dispatcher.py b/dexbot/qt_queue/queue_dispatcher.py index 4fce8b82f..803644988 100644 --- a/dexbot/qt_queue/queue_dispatcher.py +++ b/dexbot/qt_queue/queue_dispatcher.py @@ -1,4 +1,4 @@ -from PyQt5.Qt import QApplication +from PyQt5.QtWidgets import QApplication from PyQt5.QtCore import QThread, QEvent from dexbot.qt_queue.idle_queue import idle_loop From 95da83437b1bc8596a035b6b2cd20f8f071f4703 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Fri, 12 Apr 2019 18:56:39 +0200 Subject: [PATCH 1323/1846] Import Qt from QtCore --- dexbot/views/layouts/flow_layout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/views/layouts/flow_layout.py b/dexbot/views/layouts/flow_layout.py index 393ad2c2b..307d6b11d 100644 --- a/dexbot/views/layouts/flow_layout.py +++ b/dexbot/views/layouts/flow_layout.py @@ -1,5 +1,5 @@ -from PyQt5 import Qt, QtCore, QtWidgets - +from PyQt5 import QtCore, QtWidgets +from PyQt5.QtCore import Qt class FlowLayout(QtWidgets.QLayout): From dfd37b0111ac18407b7c0731584136269bbe0682 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Fri, 12 Apr 2019 16:01:13 -0700 Subject: [PATCH 1324/1846] initial package structure; moved external price method to RO --- dexbot/orderengines/BitsharesOrderEngine.py | 132 ++++++++++++++++++ dexbot/pricefeeds/BitsharesFeed.py | 13 ++ dexbot/strategies/base.py | 144 +------------------- dexbot/strategies/relative_orders.py | 26 +++- 4 files changed, 171 insertions(+), 144 deletions(-) create mode 100644 dexbot/orderengines/BitsharesOrderEngine.py create mode 100644 dexbot/pricefeeds/BitsharesFeed.py diff --git a/dexbot/orderengines/BitsharesOrderEngine.py b/dexbot/orderengines/BitsharesOrderEngine.py new file mode 100644 index 000000000..c00db07cf --- /dev/null +++ b/dexbot/orderengines/BitsharesOrderEngine.py @@ -0,0 +1,132 @@ +from bitshares.instance import shared_bitshares_instance + + +class BitsharesOrderEngine: + + def __init__(self, + bitshares_instance=None): + + # BitShares instance + self.bitshares = bitshares_instance or shared_bitshares_instance() + + # Dex instance used to get different fees for the market + self.dex = Dex(self.bitshares) + + + + def get_market_buy_orders(self, depth=10): + """ Fetches most recent data and returns list of buy orders. + + :param int | depth: Amount of buy orders returned, Default=10 + :return: List of market sell orders + """ + orders = self.get_market_orders(depth=depth) + buy_orders = self.filter_buy_orders(orders) + return buy_orders + + + def get_market_sell_orders(self, depth=10): + """ Fetches most recent data and returns list of sell orders. + + :param int | depth: Amount of sell orders returned, Default=10 + :return: List of market sell orders + """ + orders = self.get_market_orders(depth=depth) + sell_orders = self.filter_sell_orders(orders) + return sell_orders + + def get_highest_market_buy_order(self, orders=None): + """ Returns the highest buy order that is not own, regardless of order size. + + :param list | orders: Optional list of orders, if none given fetch newest from market + :return: Highest market buy order or None + """ + if not orders: + orders = self.get_market_buy_orders(1) + + try: + order = orders[0] + except IndexError: + self.log.info('Market has no buy orders.') + return None + + return order + + def get_highest_own_buy_order(self, orders=None): + """ Returns highest own buy order. + + :param list | orders: + :return: Highest own buy order by price at the market or None + """ + if not orders: + orders = self.get_own_buy_orders() + + try: + return orders[0] + except IndexError: + return None + + def get_lowest_market_sell_order(self, orders=None): + """ Returns the lowest sell order that is not own, regardless of order size. + + :param list | orders: Optional list of orders, if none given fetch newest from market + :return: Lowest market sell order or None + """ + if not orders: + orders = self.get_market_sell_orders(1) + + try: + order = orders[0] + except IndexError: + self.log.info('Market has no sell orders.') + return None + + return order + + def get_lowest_own_sell_order(self, orders=None): + """ Returns lowest own sell order. + + :param list | orders: + :return: Lowest own sell order by price at the market + """ + if not orders: + orders = self.get_own_sell_orders() + + try: + return orders[0] + except IndexError: + return None + + def cancel_all_orders(self): + """ Cancel all orders of the worker's account + """ + self.log.info('Canceling all orders') + + if self.all_own_orders: + self.cancel_orders(self.all_own_orders) + + self.log.info("Orders canceled") + + def cancel_orders(self, orders, batch_only=False): + """ Cancel specific order(s) + + :param list | orders: List of orders to cancel + :param bool | batch_only: Try cancel orders only in batch mode without one-by-one fallback + :return: + """ + if not isinstance(orders, (list, set, tuple)): + orders = [orders] + + orders = [order['id'] for order in orders if 'id' in order] + + success = self._cancel_orders(orders) + if not success and batch_only: + return False + if not success and len(orders) > 1 and not batch_only: + # One of the order cancels failed, cancel the orders one by one + for order in orders: + success = self._cancel_orders(order) + if not success: + return False + return success + diff --git a/dexbot/pricefeeds/BitsharesFeed.py b/dexbot/pricefeeds/BitsharesFeed.py new file mode 100644 index 000000000..9e53a97c0 --- /dev/null +++ b/dexbot/pricefeeds/BitsharesFeed.py @@ -0,0 +1,13 @@ + +import bitshares.exceptions +import bitsharesapi +import bitsharesapi.exceptions +from bitshares.market import Market + +class BitsharesFeed: + + def __init__(self, market, account): + self.market = market + self.account = account + + diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index fbc37d7a3..b08d0be92 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -7,7 +7,6 @@ from dexbot.config import Config from dexbot.storage import Storage from dexbot.helper import truncate -from dexbot.strategies.external_feeds.price_feed import PriceFeed from dexbot.qt_queue.idle_queue import idle_add from .config_parts.base_config import BaseConfig @@ -15,6 +14,7 @@ import bitshares.exceptions import bitsharesapi import bitsharesapi.exceptions + from bitshares.account import Account from bitshares.amount import Amount, Asset from bitshares.dex import Dex @@ -42,9 +42,6 @@ class StrategyBase(Storage, Events): Available attributes: * ``worker.bitshares``: instance of ´`bitshares.BitShares()`` - * ``worker.add_state``: Add a specific state - * ``worker.set_state``: Set finite state machine - * ``worker.get_state``: Change state of state machine * ``worker.account``: The Account object of this worker * ``worker.market``: The market used by this worker * ``worker.orders``: List of open orders of the worker's account in the worker's market @@ -101,9 +98,6 @@ def __init__(self, # BitShares instance self.bitshares = bitshares_instance or shared_bitshares_instance() - # Dex instance used to get different fees for the market - self.dex = Dex(self.bitshares) - # Storage Storage.__init__(self, name) @@ -313,39 +307,6 @@ def calculate_worker_value(self, unit_of_measure): # Fixme: Make sure that decimal precision is correct. return base_total + quote_total - def cancel_all_orders(self): - """ Cancel all orders of the worker's account - """ - self.log.info('Canceling all orders') - - if self.all_own_orders: - self.cancel_orders(self.all_own_orders) - - self.log.info("Orders canceled") - - def cancel_orders(self, orders, batch_only=False): - """ Cancel specific order(s) - - :param list | orders: List of orders to cancel - :param bool | batch_only: Try cancel orders only in batch mode without one-by-one fallback - :return: - """ - if not isinstance(orders, (list, set, tuple)): - orders = [orders] - - orders = [order['id'] for order in orders if 'id' in order] - - success = self._cancel_orders(orders) - if not success and batch_only: - return False - if not success and len(orders) > 1 and not batch_only: - # One of the order cancels failed, cancel the orders one by one - for order in orders: - success = self._cancel_orders(order) - if not success: - return False - return success - def count_asset(self, order_ids=None, return_asset=False): """ Returns the combined amount of the given order ids and the account balance The amounts are returned in quote and base assets of the market @@ -415,109 +376,6 @@ def get_allocated_assets(self, order_ids=None, return_asset=False): return {'quote': quote, 'base': base} - def get_market_buy_orders(self, depth=10): - """ Fetches most recent data and returns list of buy orders. - - :param int | depth: Amount of buy orders returned, Default=10 - :return: List of market sell orders - """ - orders = self.get_market_orders(depth=depth) - buy_orders = self.filter_buy_orders(orders) - return buy_orders - - def get_market_sell_orders(self, depth=10): - """ Fetches most recent data and returns list of sell orders. - - :param int | depth: Amount of sell orders returned, Default=10 - :return: List of market sell orders - """ - orders = self.get_market_orders(depth=depth) - sell_orders = self.filter_sell_orders(orders) - return sell_orders - - def get_highest_market_buy_order(self, orders=None): - """ Returns the highest buy order that is not own, regardless of order size. - - :param list | orders: Optional list of orders, if none given fetch newest from market - :return: Highest market buy order or None - """ - if not orders: - orders = self.get_market_buy_orders(1) - - try: - order = orders[0] - except IndexError: - self.log.info('Market has no buy orders.') - return None - - return order - - def get_highest_own_buy_order(self, orders=None): - """ Returns highest own buy order. - - :param list | orders: - :return: Highest own buy order by price at the market or None - """ - if not orders: - orders = self.get_own_buy_orders() - - try: - return orders[0] - except IndexError: - return None - - def get_lowest_market_sell_order(self, orders=None): - """ Returns the lowest sell order that is not own, regardless of order size. - - :param list | orders: Optional list of orders, if none given fetch newest from market - :return: Lowest market sell order or None - """ - if not orders: - orders = self.get_market_sell_orders(1) - - try: - order = orders[0] - except IndexError: - self.log.info('Market has no sell orders.') - return None - - return order - - def get_lowest_own_sell_order(self, orders=None): - """ Returns lowest own sell order. - - :param list | orders: - :return: Lowest own sell order by price at the market - """ - if not orders: - orders = self.get_own_sell_orders() - - try: - return orders[0] - except IndexError: - return None - - def get_external_market_center_price(self, external_price_source): - """ Get center price from an external market for current market pair - - :param external_price_source: External market name - :return: Center price as float - """ - self.log.debug('inside get_external_mcp, exchange: {} '.format(external_price_source)) - market = self.market.get_string('/') - self.log.debug('market: {} '.format(market)) - price_feed = PriceFeed(external_price_source, market) - price_feed.filter_symbols() - center_price = price_feed.get_center_price(None) - self.log.debug('PriceFeed: {}'.format(center_price)) - - if center_price is None: # Try USDT - center_price = price_feed.get_center_price("USDT") - self.log.debug('Substitute USD/USDT center price: {}'.format(center_price)) - if center_price is None: # Try consolidated - center_price = price_feed.get_consolidated_price() - self.log.debug('Consolidated center price: {}'.format(center_price)) - return center_price def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors=False): """ Returns the center price of market including own orders. diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 46017c789..ccac914ed 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -3,7 +3,7 @@ from .base import StrategyBase from .config_parts.relative_config import RelativeConfig - +from dexbot.strategies.external_feeds.price_feed import PriceFeed class RelativeStrategy(StrategyBase): """ Relative Orders strategy @@ -235,6 +235,30 @@ def update_orders(self): if len(order_ids) < expected_num_orders and not self.disabled: self.update_orders() + + def get_external_market_center_price(self, external_price_source): + """ Get center price from an external market for current market pair + + :param external_price_source: External market name + :return: Center price as float + """ + self.log.debug('inside get_external_mcp, exchange: {} '.format(external_price_source)) + market = self.market.get_string('/') + self.log.debug('market: {} '.format(market)) + price_feed = PriceFeed(external_price_source, market) + price_feed.filter_symbols() + center_price = price_feed.get_center_price(None) + self.log.debug('PriceFeed: {}'.format(center_price)) + + if center_price is None: # Try USDT + center_price = price_feed.get_center_price("USDT") + self.log.debug('Substitute USD/USDT center price: {}'.format(center_price)) + if center_price is None: # Try consolidated + center_price = price_feed.get_consolidated_price() + self.log.debug('Consolidated center price: {}'.format(center_price)) + return center_price + + def _calculate_center_price(self, suppress_errors=False): highest_bid = float(self.ticker().get('highestBid')) lowest_ask = float(self.ticker().get('lowestAsk')) From 8d0f769256ca5a1fb5a0f798b289451b74a6cf46 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Fri, 12 Apr 2019 19:05:10 -0700 Subject: [PATCH 1325/1846] move get_external_market_center_price from base to relative orders strategy --- dexbot/strategies/base.py | 23 ----------------------- dexbot/strategies/relative_orders.py | 24 +++++++++++++++++++++++- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index fbc37d7a3..413ec6f1a 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -7,7 +7,6 @@ from dexbot.config import Config from dexbot.storage import Storage from dexbot.helper import truncate -from dexbot.strategies.external_feeds.price_feed import PriceFeed from dexbot.qt_queue.idle_queue import idle_add from .config_parts.base_config import BaseConfig @@ -497,28 +496,6 @@ def get_lowest_own_sell_order(self, orders=None): except IndexError: return None - def get_external_market_center_price(self, external_price_source): - """ Get center price from an external market for current market pair - - :param external_price_source: External market name - :return: Center price as float - """ - self.log.debug('inside get_external_mcp, exchange: {} '.format(external_price_source)) - market = self.market.get_string('/') - self.log.debug('market: {} '.format(market)) - price_feed = PriceFeed(external_price_source, market) - price_feed.filter_symbols() - center_price = price_feed.get_center_price(None) - self.log.debug('PriceFeed: {}'.format(center_price)) - - if center_price is None: # Try USDT - center_price = price_feed.get_center_price("USDT") - self.log.debug('Substitute USD/USDT center price: {}'.format(center_price)) - if center_price is None: # Try consolidated - center_price = price_feed.get_consolidated_price() - self.log.debug('Consolidated center price: {}'.format(center_price)) - return center_price - def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors=False): """ Returns the center price of market including own orders. diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 3ce0f7b14..2c6b943e3 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -3,7 +3,7 @@ from .base import StrategyBase from .config_parts.relative_config import RelativeConfig - +from dexbot.strategies.external_feeds.price_feed import PriceFeed class Strategy(StrategyBase): """ Relative Orders strategy @@ -155,6 +155,28 @@ def amount_to_buy(self): amount = 0 return amount + def get_external_market_center_price(self, external_price_source): + """ Get center price from an external market for current market pair + + :param external_price_source: External market name + :return: Center price as float + """ + self.log.debug('inside get_external_mcp, exchange: {} '.format(external_price_source)) + market = self.market.get_string('/') + self.log.debug('market: {} '.format(market)) + price_feed = PriceFeed(external_price_source, market) + price_feed.filter_symbols() + center_price = price_feed.get_center_price(None) + self.log.debug('PriceFeed: {}'.format(center_price)) + + if center_price is None: # Try USDT + center_price = price_feed.get_center_price("USDT") + self.log.debug('Substitute USD/USDT center price: {}'.format(center_price)) + if center_price is None: # Try consolidated + center_price = price_feed.get_consolidated_price() + self.log.debug('Consolidated center price: {}'.format(center_price)) + return center_price + def calculate_order_prices(self): # Set center price as None, in case dynamic has not amount given, center price is calculated from market orders center_price = None From b1a217f2411f4abd131214cb9ecadf41b6cc59bc Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Sun, 14 Apr 2019 21:33:31 -0700 Subject: [PATCH 1326/1846] fix formatting error on if statement --- 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 2c6b943e3..a45c46c0a 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -5,6 +5,7 @@ from .config_parts.relative_config import RelativeConfig from dexbot.strategies.external_feeds.price_feed import PriceFeed + class Strategy(StrategyBase): """ Relative Orders strategy """ @@ -118,8 +119,7 @@ def tick(self, d): """ Ticks come in on every block. We need to periodically check orders because cancelled orders do not triggers a market_update event """ - if (self.is_reset_on_price_change and not - self.counter % 8): + if (self.is_reset_on_price_change and not self.counter % 8): self.log.debug('Checking orders by tick threshold') self.check_orders() self.counter += 1 From 243aedcf54e15632684f76755f14be2f637b12c9 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Sun, 14 Apr 2019 23:56:26 -0700 Subject: [PATCH 1327/1846] renaming classes --- .../orderengines/{BitsharesOrderEngine.py => Bitshares.py} | 2 +- dexbot/pricefeeds/{BitsharesFeed.py => Bitshares.py} | 6 ++---- dexbot/strategies/echo.py | 2 +- dexbot/strategies/relative_orders.py | 2 +- dexbot/strategies/staggered_orders.py | 2 +- dexbot/strategies/strategy_template.py | 2 +- 6 files changed, 7 insertions(+), 9 deletions(-) rename dexbot/orderengines/{BitsharesOrderEngine.py => Bitshares.py} (99%) rename dexbot/pricefeeds/{BitsharesFeed.py => Bitshares.py} (75%) diff --git a/dexbot/orderengines/BitsharesOrderEngine.py b/dexbot/orderengines/Bitshares.py similarity index 99% rename from dexbot/orderengines/BitsharesOrderEngine.py rename to dexbot/orderengines/Bitshares.py index c00db07cf..22dfb20fd 100644 --- a/dexbot/orderengines/BitsharesOrderEngine.py +++ b/dexbot/orderengines/Bitshares.py @@ -1,7 +1,7 @@ from bitshares.instance import shared_bitshares_instance -class BitsharesOrderEngine: +class OrderEngine: def __init__(self, bitshares_instance=None): diff --git a/dexbot/pricefeeds/BitsharesFeed.py b/dexbot/pricefeeds/Bitshares.py similarity index 75% rename from dexbot/pricefeeds/BitsharesFeed.py rename to dexbot/pricefeeds/Bitshares.py index 9e53a97c0..98827674a 100644 --- a/dexbot/pricefeeds/BitsharesFeed.py +++ b/dexbot/pricefeeds/Bitshares.py @@ -4,10 +4,8 @@ import bitsharesapi.exceptions from bitshares.market import Market -class BitsharesFeed: +class PriceFeed: def __init__(self, market, account): self.market = market - self.account = account - - + self.account = account \ No newline at end of file diff --git a/dexbot/strategies/echo.py b/dexbot/strategies/echo.py index 225eb08bb..264f1027c 100644 --- a/dexbot/strategies/echo.py +++ b/dexbot/strategies/echo.py @@ -1,7 +1,7 @@ from dexbot.strategies.base import StrategyBase -class EchoStrategy(StrategyBase): +class Strategy(StrategyBase): """ Echo strategy Strategy that logs all events within the blockchain """ diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index ccac914ed..2b1e6d348 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -5,7 +5,7 @@ from .config_parts.relative_config import RelativeConfig from dexbot.strategies.external_feeds.price_feed import PriceFeed -class RelativeStrategy(StrategyBase): +class Strategy(StrategyBase): """ Relative Orders strategy """ diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c16a67b2c..74e9402e2 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -10,7 +10,7 @@ from .config_parts.staggered_config import StaggeredConfig -class StaggeredStrategy(StrategyBase): +class Strategy(StrategyBase): """ Staggered Orders strategy """ @classmethod diff --git a/dexbot/strategies/strategy_template.py b/dexbot/strategies/strategy_template.py index 2be281b0f..c5ac7f9ac 100644 --- a/dexbot/strategies/strategy_template.py +++ b/dexbot/strategies/strategy_template.py @@ -12,7 +12,7 @@ STRATEGY_NAME = 'Strategy Template' -class TemplateStrategy(StrategyBase): +class Strategy(StrategyBase): """ Replace with the name of the strategy. From 542741d0f53101dafee46dc8378e43316dca105f Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 15 Apr 2019 12:35:03 +0500 Subject: [PATCH 1328/1846] Handle disabling worker a bit cleaner Previous fix caused "RuntimeError: OrderedDict mutated during iteration" error, this change prevents this. --- dexbot/worker.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/dexbot/worker.py b/dexbot/worker.py index d691b4485..a7df9c1d2 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -119,7 +119,7 @@ def on_block(self, data): continue elif self.workers[worker_name].disabled: self.workers[worker_name].log.error('Worker "{}" is disabled'.format(worker_name)) - self.config["workers"].pop(worker_name) + self.workers.pop(worker_name) continue try: self.workers[worker_name].ontick(data) @@ -137,9 +137,11 @@ def on_market(self, data): self.config_lock.acquire() for worker_name, worker in self.config["workers"].items(): - if self.workers[worker_name].disabled: + if worker_name not in self.workers: + continue + elif self.workers[worker_name].disabled: self.workers[worker_name].log.error('Worker "{}" is disabled'.format(worker_name)) - self.config["workers"].pop(worker_name) + self.workers.pop(worker_name) continue if worker["market"] == data.market: try: @@ -156,9 +158,11 @@ 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: + if worker_name not in self.workers: + continue + elif self.workers[worker_name].disabled: self.workers[worker_name].log.error('Worker "{}" is disabled'.format(worker_name)) - self.config["workers"].pop(worker_name) + self.workers.pop(worker_name) continue if worker["account"] == account["name"]: try: From f9922c720b5cc7d332af66b2feeb8d6d61724617 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 14 Apr 2019 14:29:41 +0500 Subject: [PATCH 1329/1846] Pass bitshares_instance while instantiating Asset Closes: #556 --- dexbot/strategies/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index fbc37d7a3..acdffb6bf 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -150,12 +150,12 @@ def __init__(self, if fee_asset_symbol: try: - self.fee_asset = Asset(fee_asset_symbol) + self.fee_asset = Asset(fee_asset_symbol, bitshares_instance=self.bitshares) except bitshares.exceptions.AssetDoesNotExistsException: - self.fee_asset = Asset('1.3.0') + self.fee_asset = Asset('1.3.0', bitshares_instance=self.bitshares) else: # If there is no fee asset, use BTS - self.fee_asset = Asset('1.3.0') + self.fee_asset = Asset('1.3.0', bitshares_instance=self.bitshares) # CER cache self.core_exchange_rate = None @@ -1302,7 +1302,7 @@ def convert_fee(self, fee_amount, fee_asset): :return: float | amount of fee_asset to pay fee """ if isinstance(fee_asset, str): - fee_asset = Asset(fee_asset) + fee_asset = Asset(fee_asset, bitshares_instance=self.bitshares) if fee_asset['id'] == '1.3.0': # Fee asset is BTS, so no further calculations are needed From 5d40c5fcdf34ceca7312d7c5cd99e3010485bf97 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 14 Apr 2019 15:09:15 +0500 Subject: [PATCH 1330/1846] Instantiate Amounts passing bitshares_instance Closes: #557 --- dexbot/strategies/staggered_orders.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 74e9402e2..3c82de877 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1773,10 +1773,10 @@ def place_virtual_buy_order(self, amount, price): order = VirtualOrder() order['price'] = price - quote_asset = Amount(amount, self.market['quote']['symbol']) + quote_asset = Amount(amount, self.market['quote']['symbol'], bitshares_instance=self.bitshares) order['quote'] = quote_asset - base_asset = Amount(amount * price, self.market['base']['symbol']) + base_asset = Amount(amount * price, self.market['base']['symbol'], bitshares_instance=self.bitshares) order['base'] = base_asset order['for_sale'] = base_asset @@ -1802,10 +1802,10 @@ def place_virtual_sell_order(self, amount, price): order = VirtualOrder() order['price'] = price ** -1 - quote_asset = Amount(amount * price, self.market['base']['symbol']) + quote_asset = Amount(amount * price, self.market['base']['symbol'], bitshares_instance=self.bitshares) order['quote'] = quote_asset - base_asset = Amount(amount, self.market['quote']['symbol']) + base_asset = Amount(amount, self.market['quote']['symbol'], bitshares_instance=self.bitshares) order['base'] = base_asset order['for_sale'] = base_asset From 840250cf43f2df90b1f7d5f773f052530cdeec59 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 15 Apr 2019 23:31:45 +0500 Subject: [PATCH 1331/1846] Pass bitshares_instance on Amount initialization --- dexbot/strategies/base.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index acdffb6bf..2f6b84c31 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -270,10 +270,10 @@ def balance(self, asset, fee_reservation=0): return balance def calculate_order_data(self, order, amount, price): - quote_asset = Amount(amount, self.market['quote']['symbol']) + quote_asset = Amount(amount, self.market['quote']['symbol'], bitshares_instance=self.bitshares) order['quote'] = quote_asset order['price'] = price - base_asset = Amount(amount * price, self.market['base']['symbol']) + base_asset = Amount(amount * price, self.market['base']['symbol'], bitshares_instance=self.bitshares) order['base'] = base_asset return order @@ -376,8 +376,8 @@ def count_asset(self, order_ids=None, return_asset=False): base += orders_balance['base'] if return_asset: - quote = Amount(quote, quote_asset) - base = Amount(base, base_asset) + quote = Amount(quote, quote_asset, bitshares_instance=self.bitshares) + base = Amount(base, base_asset, bitshares_instance=self.bitshares) return {'quote': quote, 'base': base} @@ -410,8 +410,8 @@ def get_allocated_assets(self, order_ids=None, return_asset=False): # Return as Amount objects instead of only float values if return_asset: - quote = Amount(quote, quote_asset) - base = Amount(base, base_asset) + quote = Amount(quote, quote_asset, bitshares_instance=self.bitshares) + base = Amount(base, base_asset, bitshares_instance=self.bitshares) return {'quote': quote, 'base': base} @@ -990,7 +990,7 @@ def place_market_buy_order(self, amount, price, return_none=False, *args, **kwar buy_transaction = self.retry_action( self.market.buy, price, - Amount(amount=amount, asset=self.market["quote"]), + Amount(amount=amount, asset=self.market["quote"], bitshares_instance=self.bitshares), account=self.account.name, expiration=self.expiration, returnOrderId=return_order_id, @@ -1046,7 +1046,7 @@ def place_market_sell_order(self, amount, price, return_none=False, invert=False sell_transaction = self.retry_action( self.market.sell, price, - Amount(amount=amount, asset=self.market["quote"]), + Amount(amount=amount, asset=self.market["quote"], bitshares_instance=self.bitshares), account=self.account.name, expiration=self.expiration, returnOrderId=return_order_id, From 792dab1f2b63787edc4492fd1fb2cf2d1a35a9f6 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 16 Apr 2019 12:03:32 -0700 Subject: [PATCH 1332/1846] restore base.py, remove statemachine comment; move external feeds; refactor pricefeed methods; decouple methods from account --- dexbot/orderengines/__init__.py | 0 dexbot/pricefeeds/Bitshares.py | 4 - dexbot/pricefeeds/__init__.py | 0 dexbot/strategies/base.py | 140 +++++++++++++++++++++++++++++++- 4 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 dexbot/orderengines/__init__.py create mode 100644 dexbot/pricefeeds/__init__.py diff --git a/dexbot/orderengines/__init__.py b/dexbot/orderengines/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dexbot/pricefeeds/Bitshares.py b/dexbot/pricefeeds/Bitshares.py index 98827674a..a8555330c 100644 --- a/dexbot/pricefeeds/Bitshares.py +++ b/dexbot/pricefeeds/Bitshares.py @@ -1,8 +1,4 @@ -import bitshares.exceptions -import bitsharesapi -import bitsharesapi.exceptions -from bitshares.market import Market class PriceFeed: diff --git a/dexbot/pricefeeds/__init__.py b/dexbot/pricefeeds/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index b08d0be92..d467aaadb 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -14,7 +14,6 @@ import bitshares.exceptions import bitsharesapi import bitsharesapi.exceptions - from bitshares.account import Account from bitshares.amount import Amount, Asset from bitshares.dex import Dex @@ -98,6 +97,9 @@ def __init__(self, # BitShares instance self.bitshares = bitshares_instance or shared_bitshares_instance() + # Dex instance used to get different fees for the market + self.dex = Dex(self.bitshares) + # Storage Storage.__init__(self, name) @@ -307,6 +309,39 @@ def calculate_worker_value(self, unit_of_measure): # Fixme: Make sure that decimal precision is correct. return base_total + quote_total + def cancel_all_orders(self): + """ Cancel all orders of the worker's account + """ + self.log.info('Canceling all orders') + + if self.all_own_orders: + self.cancel_orders(self.all_own_orders) + + self.log.info("Orders canceled") + + def cancel_orders(self, orders, batch_only=False): + """ Cancel specific order(s) + + :param list | orders: List of orders to cancel + :param bool | batch_only: Try cancel orders only in batch mode without one-by-one fallback + :return: + """ + if not isinstance(orders, (list, set, tuple)): + orders = [orders] + + orders = [order['id'] for order in orders if 'id' in order] + + success = self._cancel_orders(orders) + if not success and batch_only: + return False + if not success and len(orders) > 1 and not batch_only: + # One of the order cancels failed, cancel the orders one by one + for order in orders: + success = self._cancel_orders(order) + if not success: + return False + return success + def count_asset(self, order_ids=None, return_asset=False): """ Returns the combined amount of the given order ids and the account balance The amounts are returned in quote and base assets of the market @@ -376,6 +411,109 @@ def get_allocated_assets(self, order_ids=None, return_asset=False): return {'quote': quote, 'base': base} + def get_market_buy_orders(self, depth=10): + """ Fetches most recent data and returns list of buy orders. + + :param int | depth: Amount of buy orders returned, Default=10 + :return: List of market sell orders + """ + orders = self.get_market_orders(depth=depth) + buy_orders = self.filter_buy_orders(orders) + return buy_orders + + def get_market_sell_orders(self, depth=10): + """ Fetches most recent data and returns list of sell orders. + + :param int | depth: Amount of sell orders returned, Default=10 + :return: List of market sell orders + """ + orders = self.get_market_orders(depth=depth) + sell_orders = self.filter_sell_orders(orders) + return sell_orders + + def get_highest_market_buy_order(self, orders=None): + """ Returns the highest buy order that is not own, regardless of order size. + + :param list | orders: Optional list of orders, if none given fetch newest from market + :return: Highest market buy order or None + """ + if not orders: + orders = self.get_market_buy_orders(1) + + try: + order = orders[0] + except IndexError: + self.log.info('Market has no buy orders.') + return None + + return order + + def get_highest_own_buy_order(self, orders=None): + """ Returns highest own buy order. + + :param list | orders: + :return: Highest own buy order by price at the market or None + """ + if not orders: + orders = self.get_own_buy_orders() + + try: + return orders[0] + except IndexError: + return None + + def get_lowest_market_sell_order(self, orders=None): + """ Returns the lowest sell order that is not own, regardless of order size. + + :param list | orders: Optional list of orders, if none given fetch newest from market + :return: Lowest market sell order or None + """ + if not orders: + orders = self.get_market_sell_orders(1) + + try: + order = orders[0] + except IndexError: + self.log.info('Market has no sell orders.') + return None + + return order + + def get_lowest_own_sell_order(self, orders=None): + """ Returns lowest own sell order. + + :param list | orders: + :return: Lowest own sell order by price at the market + """ + if not orders: + orders = self.get_own_sell_orders() + + try: + return orders[0] + except IndexError: + return None + + def get_external_market_center_price(self, external_price_source): + """ Get center price from an external market for current market pair + + :param external_price_source: External market name + :return: Center price as float + """ + self.log.debug('inside get_external_mcp, exchange: {} '.format(external_price_source)) + market = self.market.get_string('/') + self.log.debug('market: {} '.format(market)) + price_feed = PriceFeed(external_price_source, market) + price_feed.filter_symbols() + center_price = price_feed.get_center_price(None) + self.log.debug('PriceFeed: {}'.format(center_price)) + + if center_price is None: # Try USDT + center_price = price_feed.get_center_price("USDT") + self.log.debug('Substitute USD/USDT center price: {}'.format(center_price)) + if center_price is None: # Try consolidated + center_price = price_feed.get_consolidated_price() + self.log.debug('Consolidated center price: {}'.format(center_price)) + return center_price def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors=False): """ Returns the center price of market including own orders. From 8063cb6315e2af0657938e442843d8ecf42c8aca Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 16 Apr 2019 12:04:49 -0700 Subject: [PATCH 1333/1846] removed capital name version of file Bitshares --- dexbot/pricefeeds/Bitshares.py | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 dexbot/pricefeeds/Bitshares.py diff --git a/dexbot/pricefeeds/Bitshares.py b/dexbot/pricefeeds/Bitshares.py deleted file mode 100644 index a8555330c..000000000 --- a/dexbot/pricefeeds/Bitshares.py +++ /dev/null @@ -1,7 +0,0 @@ - - -class PriceFeed: - - def __init__(self, market, account): - self.market = market - self.account = account \ No newline at end of file From 4b243ea80550bd4fb7b38736b51d73df57b46bb3 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 16 Apr 2019 12:13:18 -0700 Subject: [PATCH 1334/1846] remove external feeds as it has been moved to relative orders --- dexbot/strategies/base.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index d467aaadb..2e84a02f3 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -493,28 +493,6 @@ def get_lowest_own_sell_order(self, orders=None): except IndexError: return None - def get_external_market_center_price(self, external_price_source): - """ Get center price from an external market for current market pair - - :param external_price_source: External market name - :return: Center price as float - """ - self.log.debug('inside get_external_mcp, exchange: {} '.format(external_price_source)) - market = self.market.get_string('/') - self.log.debug('market: {} '.format(market)) - price_feed = PriceFeed(external_price_source, market) - price_feed.filter_symbols() - center_price = price_feed.get_center_price(None) - self.log.debug('PriceFeed: {}'.format(center_price)) - - if center_price is None: # Try USDT - center_price = price_feed.get_center_price("USDT") - self.log.debug('Substitute USD/USDT center price: {}'.format(center_price)) - if center_price is None: # Try consolidated - center_price = price_feed.get_consolidated_price() - self.log.debug('Consolidated center price: {}'.format(center_price)) - return center_price - def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors=False): """ Returns the center price of market including own orders. From 96e9f59e1f22ec8c159ed608815203b20e739ece Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 16 Apr 2019 12:14:44 -0700 Subject: [PATCH 1335/1846] add price feeds in renamed file --- dexbot/pricefeeds/bitshares.py | 318 +++++++++++++++++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 dexbot/pricefeeds/bitshares.py diff --git a/dexbot/pricefeeds/bitshares.py b/dexbot/pricefeeds/bitshares.py new file mode 100644 index 000000000..91158d60d --- /dev/null +++ b/dexbot/pricefeeds/bitshares.py @@ -0,0 +1,318 @@ +import logging +from bitshares.instance import shared_bitshares_instance + + +class PriceFeed: + + def __init__(self, market): + super().__init__(*args, **kwargs) + self._market = market + self.ticker = self.market.ticker + self.disabled = False # temporary while we figure out + + # Count of orders to be fetched from the API + self.fetch_depth = 8 + # BitShares instance + self.bitshares = bitshares_instance or shared_bitshares_instance() + + self.log = logging.LoggerAdapter( + logging.getLogger('dexbot.pricefeed_log'), {} + ) + + def get_market_orders(self, depth=1, updated=True): + """ Returns orders from the current market. Orders are sorted by price. + + get_market_orders() call does not have any depth limit. + + :param int | depth: Amount of orders per side will be fetched, default=1 + :param bool | updated: Return updated orders. "Updated" means partially filled orders will represent + remainders and not just initial amounts + :return: Returns a list of orders or None + """ + orders = self.bitshares.rpc.get_limit_orders(self.market['base']['id'], self.market['quote']['id'], depth) + if updated: + orders = [self.get_updated_limit_order(o) for o in orders] + orders = [Order(o, bitshares_instance=self.bitshares) for o in orders] + return orders + + def filter_buy_orders(self, orders, sort=None): + """ Return own buy orders from list of orders. Can be used to pick buy orders from a list + that is not up to date with the blockchain data. + + :param list | orders: List of orders + :param string | sort: DESC or ASC will sort the orders accordingly, default None + :return list | buy_orders: List of buy orders only + """ + buy_orders = [] + + # Filter buy orders + for order in orders: + # Check if the order is buy order, by comparing asset symbol of the order and the market + if order['base']['symbol'] == self.market['base']['symbol']: + buy_orders.append(order) + + if sort: + buy_orders = self.sort_orders_by_price(buy_orders, sort) + + return buy_orders + + def filter_sell_orders(self, orders, sort=None, invert=True): + """ Return sell orders from list of orders. Can be used to pick sell orders from a list + that is not up to date with the blockchain data. + + :param list | orders: List of orders + :param string | sort: DESC or ASC will sort the orders accordingly, default None + :param bool | invert: return inverted orders or not + :return list | sell_orders: List of sell orders only + """ + sell_orders = [] + + # Filter sell orders + for order in orders: + # Check if the order is buy order, by comparing asset symbol of the order and the market + if order['base']['symbol'] != self.market['base']['symbol']: + # Invert order before appending to the list, this gives easier comparison in strategy logic + if invert: + order = order.invert() + sell_orders.append(order) + + if sort: + sell_orders = self.sort_orders_by_price(sell_orders, sort) + + return sell_orders + + + def get_market_buy_orders(self, depth=10): + """ Fetches most recent data and returns list of buy orders. + + :param int | depth: Amount of buy orders returned, Default=10 + :return: List of market sell orders + """ + orders = self.get_market_orders(depth=depth) + buy_orders = self.filter_buy_orders(orders) + return buy_orders + + def get_market_sell_orders(self, depth=10): + """ Fetches most recent data and returns list of sell orders. + + :param int | depth: Amount of sell orders returned, Default=10 + :return: List of market sell orders + """ + orders = self.get_market_orders(depth=depth) + sell_orders = self.filter_sell_orders(orders) + return sell_orders + + def get_market_buy_price(self, quote_amount=0, base_amount=0): + """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with + moving average or weighted moving average + + :param float | quote_amount: + :param float | base_amount: + :return: price as float + """ + market_buy_orders = [] + + # In case amount is not given, return price of the highest buy order on the market + if quote_amount == 0 and base_amount == 0: + return float(self.ticker().get('highestBid')) + + # Like get_market_sell_price(), but defaulting to base_amount if both base and quote are specified. + asset_amount = base_amount + + """ Since the purpose is never get both quote and base amounts, favor base amount if both given because + this function is looking for buy price. + """ + if base_amount > quote_amount: + base = True + else: + asset_amount = quote_amount + base = False + + if not market_buy_orders: + market_buy_orders = self.get_market_buy_orders(depth=self.fetch_depth) + market_fee = self.market['base'].market_fee_percent + + target_amount = asset_amount * (1 + market_fee) + + quote_amount = 0 + base_amount = 0 + missing_amount = target_amount + + for order in market_buy_orders: + if base: + # BASE amount was given + if order['base']['amount'] <= missing_amount: + quote_amount += order['quote']['amount'] + base_amount += order['base']['amount'] + missing_amount -= order['base']['amount'] + else: + base_amount += missing_amount + quote_amount += missing_amount / order['price'] + break + elif not base: + # QUOTE amount was given + if order['quote']['amount'] <= missing_amount: + quote_amount += order['quote']['amount'] + base_amount += order['base']['amount'] + missing_amount -= order['quote']['amount'] + else: + base_amount += missing_amount * order['price'] + quote_amount += missing_amount + break + + # Prevent division by zero + if not quote_amount: + return 0.0 + + return base_amount / quote_amount + + def get_market_sell_price(self, quote_amount=0, base_amount=0): + """ Returns the BASE/QUOTE price for which [quote_amount] worth of QUOTE could be bought, + enhanced with moving average or weighted moving average. + + [quote/base]_amount = 0 means lowest regardless of size + + :param float | quote_amount: + :param float | base_amount: + :return: + """ + market_sell_orders = [] + + # In case amount is not given, return price of the lowest sell order on the market + if quote_amount == 0 and base_amount == 0: + return float(self.ticker().get('lowestAsk')) + + asset_amount = quote_amount + + """ Since the purpose is never get both quote and base amounts, favor quote amount if both given because + this function is looking for sell price. + """ + if quote_amount > base_amount: + quote = True + else: + asset_amount = base_amount + quote = False + + if not market_sell_orders: + market_sell_orders = self.get_market_sell_orders(depth=self.fetch_depth) + market_fee = self.market['quote'].market_fee_percent + + target_amount = asset_amount * (1 + market_fee) + + quote_amount = 0 + base_amount = 0 + missing_amount = target_amount + + for order in market_sell_orders: + if quote: + # QUOTE amount was given + if order['quote']['amount'] <= missing_amount: + quote_amount += order['quote']['amount'] + base_amount += order['base']['amount'] + missing_amount -= order['quote']['amount'] + else: + base_amount += missing_amount * order['price'] + quote_amount += missing_amount + break + elif not quote: + # BASE amount was given + if order['base']['amount'] <= missing_amount: + quote_amount += order['quote']['amount'] + base_amount += order['base']['amount'] + missing_amount -= order['base']['amount'] + else: + base_amount += missing_amount + quote_amount += missing_amount / order['price'] + break + + # Prevent division by zero + if not quote_amount: + return 0.0 + + return base_amount / quote_amount + + def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors=False): + """ Returns the center price of market including own orders. + + :param float | base_amount: + :param float | quote_amount: + :param bool | suppress_errors: + :return: Market center price as float + """ + center_price = None + buy_price = self.get_market_buy_price(quote_amount=quote_amount, + base_amount=base_amount) + sell_price = self.get_market_sell_price(quote_amount=quote_amount, + base_amount=base_amount) + if buy_price is None or buy_price == 0.0: + if not suppress_errors: + self.log.critical("Cannot estimate center price, there is no highest bid.") + self.disabled = True + return None + + if sell_price is None or sell_price == 0.0: + if not suppress_errors: + self.log.critical("Cannot estimate center price, there is no lowest ask.") + self.disabled = True + return None + # Calculate and return market center price. make sure buy_price has value + if buy_price: + center_price = buy_price * math.sqrt(sell_price / buy_price) + self.log.debug('Center price in get_market_center_price: {:.8f} '.format(center_price)) + return center_price + + def get_market_spread(self, quote_amount=0, base_amount=0): + """ Returns the market spread %, including own orders, from specified depth. + + :param float | quote_amount: + :param float | base_amount: + :return: Market spread as float or None + """ + ask = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount) + bid = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount) + + # Calculate market spread + if ask == 0 or bid == 0: + return None + + return ask / bid - 1 + + @property + def market(self): + """ Return the market object as :class:`bitshares.market.Market` + """ + return self._market + + @staticmethod + def get_updated_limit_order(limit_order): + """ Returns a modified limit_order so that when passed to Order class, + will return an Order object with updated amount values + + :param limit_order: an item of Account['limit_orders'] or bitshares.rpc.get_limit_orders() + :return: Order + """ + order = copy.deepcopy(limit_order) + price = float(order['sell_price']['base']['amount']) / float(order['sell_price']['quote']['amount']) + base_amount = float(order['for_sale']) + quote_amount = base_amount / price + order['sell_price']['base']['amount'] = base_amount + order['sell_price']['quote']['amount'] = quote_amount + return order + + @staticmethod + def sort_orders_by_price(orders, sort='DESC'): + """ Return list of orders sorted ascending or descending by price + + :param list | orders: list of orders to be sorted + :param string | sort: ASC or DESC. Default DESC + :return list: Sorted list of orders + """ + if sort.upper() == 'ASC': + reverse = False + elif sort.upper() == 'DESC': + reverse = True + else: + return None + + # Sort orders by price + return sorted(orders, key=lambda order: order['price'], reverse=reverse) From f1083581f6e14455240e04597b54643adc0fd47a Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 16 Apr 2019 13:27:31 -0700 Subject: [PATCH 1336/1846] revert to init orderengine --- dexbot/orderengines/{Bitshares.py => bitshares.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename dexbot/orderengines/{Bitshares.py => bitshares.py} (100%) diff --git a/dexbot/orderengines/Bitshares.py b/dexbot/orderengines/bitshares.py similarity index 100% rename from dexbot/orderengines/Bitshares.py rename to dexbot/orderengines/bitshares.py From bafd04584722a3497f395b89314fbe88775aec2d Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 16 Apr 2019 13:28:20 -0700 Subject: [PATCH 1337/1846] inital order engine --- dexbot/orderengines/bitshares.py | 118 ------------------------------- 1 file changed, 118 deletions(-) diff --git a/dexbot/orderengines/bitshares.py b/dexbot/orderengines/bitshares.py index 22dfb20fd..039a217d2 100644 --- a/dexbot/orderengines/bitshares.py +++ b/dexbot/orderengines/bitshares.py @@ -11,122 +11,4 @@ def __init__(self, # Dex instance used to get different fees for the market self.dex = Dex(self.bitshares) - - - - def get_market_buy_orders(self, depth=10): - """ Fetches most recent data and returns list of buy orders. - - :param int | depth: Amount of buy orders returned, Default=10 - :return: List of market sell orders - """ - orders = self.get_market_orders(depth=depth) - buy_orders = self.filter_buy_orders(orders) - return buy_orders - - - def get_market_sell_orders(self, depth=10): - """ Fetches most recent data and returns list of sell orders. - - :param int | depth: Amount of sell orders returned, Default=10 - :return: List of market sell orders - """ - orders = self.get_market_orders(depth=depth) - sell_orders = self.filter_sell_orders(orders) - return sell_orders - - def get_highest_market_buy_order(self, orders=None): - """ Returns the highest buy order that is not own, regardless of order size. - - :param list | orders: Optional list of orders, if none given fetch newest from market - :return: Highest market buy order or None - """ - if not orders: - orders = self.get_market_buy_orders(1) - - try: - order = orders[0] - except IndexError: - self.log.info('Market has no buy orders.') - return None - - return order - - def get_highest_own_buy_order(self, orders=None): - """ Returns highest own buy order. - - :param list | orders: - :return: Highest own buy order by price at the market or None - """ - if not orders: - orders = self.get_own_buy_orders() - - try: - return orders[0] - except IndexError: - return None - - def get_lowest_market_sell_order(self, orders=None): - """ Returns the lowest sell order that is not own, regardless of order size. - - :param list | orders: Optional list of orders, if none given fetch newest from market - :return: Lowest market sell order or None - """ - if not orders: - orders = self.get_market_sell_orders(1) - - try: - order = orders[0] - except IndexError: - self.log.info('Market has no sell orders.') - return None - - return order - - def get_lowest_own_sell_order(self, orders=None): - """ Returns lowest own sell order. - - :param list | orders: - :return: Lowest own sell order by price at the market - """ - if not orders: - orders = self.get_own_sell_orders() - - try: - return orders[0] - except IndexError: - return None - - def cancel_all_orders(self): - """ Cancel all orders of the worker's account - """ - self.log.info('Canceling all orders') - - if self.all_own_orders: - self.cancel_orders(self.all_own_orders) - - self.log.info("Orders canceled") - - def cancel_orders(self, orders, batch_only=False): - """ Cancel specific order(s) - - :param list | orders: List of orders to cancel - :param bool | batch_only: Try cancel orders only in batch mode without one-by-one fallback - :return: - """ - if not isinstance(orders, (list, set, tuple)): - orders = [orders] - - orders = [order['id'] for order in orders if 'id' in order] - - success = self._cancel_orders(orders) - if not success and batch_only: - return False - if not success and len(orders) > 1 and not batch_only: - # One of the order cancels failed, cancel the orders one by one - for order in orders: - success = self._cancel_orders(order) - if not success: - return False - return success From 6f678aa54fdf13b9e962d7ba2db1266598fe9ab3 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 16 Apr 2019 14:17:50 -0700 Subject: [PATCH 1338/1846] initial test --- dexbot/pricefeeds/bitshares-test.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 dexbot/pricefeeds/bitshares-test.py diff --git a/dexbot/pricefeeds/bitshares-test.py b/dexbot/pricefeeds/bitshares-test.py new file mode 100644 index 000000000..dbbd95191 --- /dev/null +++ b/dexbot/pricefeeds/bitshares-test.py @@ -0,0 +1,22 @@ +from bitshares.bitshares import BitShares +from bitshares.market import Market +#from dexbot.pricefeeds.bitshares import PriceFeed + +node_url = "wss://api.fr.bitsharesdex.com/ws" + +TEST_CONFIG = { + 'node': node_url +} + +bitshares = BitShares(node=TEST_CONFIG['node']) + +market = Market("USD:BTS") +print(market.ticker()) + +#pf = PriceFeed(market=market, bitshares_instance=bitshares) + +#orders = pf.get_market_buy_orders(depth=10) +#print(orders) + +#center_price = pf.get_market_center_price(base_amount=0, quote_amount=0, suppress_errors=False) +#print(center_price) \ No newline at end of file From e6889a6a8fb13bc02e373e6613ca8c3901f17f8a Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 16 Apr 2019 14:21:47 -0700 Subject: [PATCH 1339/1846] add tests --- dexbot/pricefeeds/bitshares-test.py | 14 ++++++++------ dexbot/pricefeeds/bitshares.py | 9 +++++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/dexbot/pricefeeds/bitshares-test.py b/dexbot/pricefeeds/bitshares-test.py index dbbd95191..3c81a4a70 100644 --- a/dexbot/pricefeeds/bitshares-test.py +++ b/dexbot/pricefeeds/bitshares-test.py @@ -1,6 +1,6 @@ -from bitshares.bitshares import BitShares +#from bitshares.bitshares import BitShares from bitshares.market import Market -#from dexbot.pricefeeds.bitshares import PriceFeed +from dexbot.pricefeeds.bitshares import PriceFeed node_url = "wss://api.fr.bitsharesdex.com/ws" @@ -8,15 +8,17 @@ 'node': node_url } -bitshares = BitShares(node=TEST_CONFIG['node']) +#bitshares = BitShares(node=TEST_CONFIG['node']) + +print("this is a test") market = Market("USD:BTS") print(market.ticker()) #pf = PriceFeed(market=market, bitshares_instance=bitshares) +#center_price = pf.get_market_center_price(base_amount=0, quote_amount=0, suppress_errors=False) +#print(center_price) + #orders = pf.get_market_buy_orders(depth=10) #print(orders) - -#center_price = pf.get_market_center_price(base_amount=0, quote_amount=0, suppress_errors=False) -#print(center_price) \ No newline at end of file diff --git a/dexbot/pricefeeds/bitshares.py b/dexbot/pricefeeds/bitshares.py index 91158d60d..82fbdf637 100644 --- a/dexbot/pricefeeds/bitshares.py +++ b/dexbot/pricefeeds/bitshares.py @@ -1,14 +1,15 @@ import logging from bitshares.instance import shared_bitshares_instance - class PriceFeed: - def __init__(self, market): - super().__init__(*args, **kwargs) + def __init__(self, + market, + bitshares_instance=None): + self._market = market self.ticker = self.market.ticker - self.disabled = False # temporary while we figure out + self.disabled = False # temporary while I figure out what this does # Count of orders to be fetched from the API self.fetch_depth = 8 From e3874978f6b50c4a5f848b833461f138480d498e Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 16 Apr 2019 14:37:12 -0700 Subject: [PATCH 1340/1846] rename files --- dexbot/orderengines/{bitshares.py => bts_engine.py} | 0 dexbot/pricefeeds/{bitshares.py => bts_feed.py} | 0 dexbot/pricefeeds/{bitshares-test.py => bts_feed_test.py} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename dexbot/orderengines/{bitshares.py => bts_engine.py} (100%) rename dexbot/pricefeeds/{bitshares.py => bts_feed.py} (100%) rename dexbot/pricefeeds/{bitshares-test.py => bts_feed_test.py} (100%) diff --git a/dexbot/orderengines/bitshares.py b/dexbot/orderengines/bts_engine.py similarity index 100% rename from dexbot/orderengines/bitshares.py rename to dexbot/orderengines/bts_engine.py diff --git a/dexbot/pricefeeds/bitshares.py b/dexbot/pricefeeds/bts_feed.py similarity index 100% rename from dexbot/pricefeeds/bitshares.py rename to dexbot/pricefeeds/bts_feed.py diff --git a/dexbot/pricefeeds/bitshares-test.py b/dexbot/pricefeeds/bts_feed_test.py similarity index 100% rename from dexbot/pricefeeds/bitshares-test.py rename to dexbot/pricefeeds/bts_feed_test.py From b0b9a457477d9c8aee87455872e105bfe5e93368 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 16 Apr 2019 15:00:34 -0700 Subject: [PATCH 1341/1846] clean up requirements --- dexbot/pricefeeds/bts_feed.py | 8 ++++++++ dexbot/pricefeeds/bts_feed_test.py | 20 ++++++++++---------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/dexbot/pricefeeds/bts_feed.py b/dexbot/pricefeeds/bts_feed.py index 82fbdf637..0d846ec51 100644 --- a/dexbot/pricefeeds/bts_feed.py +++ b/dexbot/pricefeeds/bts_feed.py @@ -1,5 +1,13 @@ +import datetime +import copy import logging +import math +import time + from bitshares.instance import shared_bitshares_instance +from bitshares.market import Market +from bitshares.price import FilledOrder, Order, UpdateCallOrder +from bitshares.utils import formatTime class PriceFeed: diff --git a/dexbot/pricefeeds/bts_feed_test.py b/dexbot/pricefeeds/bts_feed_test.py index 3c81a4a70..792e82fc2 100644 --- a/dexbot/pricefeeds/bts_feed_test.py +++ b/dexbot/pricefeeds/bts_feed_test.py @@ -1,6 +1,6 @@ -#from bitshares.bitshares import BitShares +from bitshares.bitshares import BitShares from bitshares.market import Market -from dexbot.pricefeeds.bitshares import PriceFeed +from dexbot.pricefeeds.bts_feed import PriceFeed node_url = "wss://api.fr.bitsharesdex.com/ws" @@ -8,17 +8,17 @@ 'node': node_url } -#bitshares = BitShares(node=TEST_CONFIG['node']) +bts = BitShares(node=TEST_CONFIG['node']) -print("this is a test") +print("Bitshares Price Feed Test") market = Market("USD:BTS") -print(market.ticker()) +#print(market.ticker()) -#pf = PriceFeed(market=market, bitshares_instance=bitshares) +pf = PriceFeed(market=market) -#center_price = pf.get_market_center_price(base_amount=0, quote_amount=0, suppress_errors=False) -#print(center_price) +center_price = pf.get_market_center_price(base_amount=0, quote_amount=0, suppress_errors=False) +print(center_price) -#orders = pf.get_market_buy_orders(depth=10) -#print(orders) +orders = pf.get_market_buy_orders(depth=10) +print(orders) From 3331f08e2242201b42fcafdb48f5e6941ea44d8b Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 17 Apr 2019 15:07:46 +0500 Subject: [PATCH 1342/1846] Fix testnet transaction doubling See https://github.com/bitshares/bitshares-core/issues/1722 --- tests/node_config/config.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/node_config/config.ini b/tests/node_config/config.ini index 75aac9a1d..99e5725fd 100644 --- a/tests/node_config/config.ini +++ b/tests/node_config/config.ini @@ -8,7 +8,7 @@ p2p-endpoint = 0.0.0.0:9091 seed-nodes = [] # Pairs of [BLOCK_NUM,BLOCK_ID] that should be enforced as checkpoints. -# checkpoint = +checkpoint = [] # Endpoint for websocket RPC to listen on rpc-endpoint = 0.0.0.0:8091 From 7750377449e2cab8fa256607e6c7b60ac47218fa Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 17 Apr 2019 15:08:46 +0500 Subject: [PATCH 1343/1846] Lower block interval to 1s Faster blocks means faster tests --- tests/conftest.py | 2 +- tests/node_config/genesis.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 01b5a3974..5f9a4a998 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,7 +16,7 @@ # Note: chain_id is generated from genesis.json, every time it's changes you need to get new chain_id from # `bitshares.rpc.get_chain_properties()` -known_chains["TEST"]["chain_id"] = "5f0c72a2637f4938507f06d87e07be5d8015c32d720dce468d6d9a30db79947b" +known_chains["TEST"]["chain_id"] = "c74ddb39b3a233445dd95d7b6fc2d0fa4ba666698db26b53855d94fffcc460af" PRIVATE_KEYS = ['5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3'] DEFAULT_ACCOUNT = 'init0' diff --git a/tests/node_config/genesis.json b/tests/node_config/genesis.json index 5d4a3341c..a8f32e7db 100644 --- a/tests/node_config/genesis.json +++ b/tests/node_config/genesis.json @@ -194,7 +194,7 @@ ], "scale": 10000 }, - "block_interval": 5, + "block_interval": 1, "maintenance_interval": 86400, "maintenance_skip_slots": 3, "committee_proposal_review_period": 1209600, From f24fd24c50265e075fb6005cbf658089d6cef61a Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Wed, 17 Apr 2019 12:08:28 -0700 Subject: [PATCH 1344/1846] Cleaned up methods, add tests for bitshares feed. --- dexbot/pricefeeds/bts_feed.py | 8 ++---- dexbot/pricefeeds/bts_feed_test.py | 46 ++++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/dexbot/pricefeeds/bts_feed.py b/dexbot/pricefeeds/bts_feed.py index 0d846ec51..655051513 100644 --- a/dexbot/pricefeeds/bts_feed.py +++ b/dexbot/pricefeeds/bts_feed.py @@ -1,13 +1,9 @@ -import datetime import copy import logging import math -import time from bitshares.instance import shared_bitshares_instance -from bitshares.market import Market -from bitshares.price import FilledOrder, Order, UpdateCallOrder -from bitshares.utils import formatTime +from bitshares.price import Order class PriceFeed: @@ -17,7 +13,7 @@ def __init__(self, self._market = market self.ticker = self.market.ticker - self.disabled = False # temporary while I figure out what this does + self.disabled = False # flag for suppress errors # Count of orders to be fetched from the API self.fetch_depth = 8 diff --git a/dexbot/pricefeeds/bts_feed_test.py b/dexbot/pricefeeds/bts_feed_test.py index 792e82fc2..4817b2de9 100644 --- a/dexbot/pricefeeds/bts_feed_test.py +++ b/dexbot/pricefeeds/bts_feed_test.py @@ -1,5 +1,7 @@ from bitshares.bitshares import BitShares from bitshares.market import Market +#from bitshares.price import Order + from dexbot.pricefeeds.bts_feed import PriceFeed node_url = "wss://api.fr.bitsharesdex.com/ws" @@ -13,12 +15,46 @@ print("Bitshares Price Feed Test") market = Market("USD:BTS") -#print(market.ticker()) +print(market.ticker()) + +pf = PriceFeed(market=market, bitshares_instance=bts) -pf = PriceFeed(market=market) +market = pf.market +print("Market we are examining:", market, sep=':') center_price = pf.get_market_center_price(base_amount=0, quote_amount=0, suppress_errors=False) -print(center_price) +print("center price:", center_price, sep=':') + +print("\nList of buy orders:") +buy_orders = pf.get_market_buy_orders(depth=10) +for order in buy_orders: + print(order) + +order1 = buy_orders[0] +print("\nGet top of buy orders", order1, sep=':') + +print("\nList of Buy orders in ASC price") +asc_buy_orders = pf.sort_orders_by_price(buy_orders, sort='ASC') +for order in asc_buy_orders: + print(order) + +sell_orders = pf.get_market_sell_orders(depth=10) +print("\nMarket Sell Orders", sell_orders, sep=':') + +mkt_orders = pf.get_market_orders(depth=1, updated=True) +print("\nMarket Orders", mkt_orders, sep=":") + +mkt_buy_price = pf.get_market_buy_price(quote_amount=0, base_amount=0) +print("market buy price", mkt_buy_price, sep=':') + +mkt_sell_price = pf.get_market_sell_price(quote_amount=0, base_amount=0) +print("market sell price", mkt_sell_price, sep=':') + +mkt_spread = pf.get_market_spread(quote_amount=0, base_amount=0) +print("market spread", mkt_spread, sep=':') + + +# todo: +# filter buy/sell orders (2) +# get_updated_limit_order (static method) -orders = pf.get_market_buy_orders(depth=10) -print(orders) From fab3dcacea08c2dd34104ddf807d9500ad92c712 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Wed, 17 Apr 2019 12:15:16 -0700 Subject: [PATCH 1345/1846] update sqlalchemy due to vulnerability --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e81f40f7c..0ec4c1aca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,5 +15,5 @@ appdirs>=1.4.3 pycryptodomex==3.6.4 websocket-client==0.54.0 sdnotify==0.3.2 -sqlalchemy==1.2.11 +sqlalchemy>=1.3.0 click==7.0 From 76e21a5aa07751bad894964d5a1fce851622f75d Mon Sep 17 00:00:00 2001 From: octomatic <39811582+thehapax@users.noreply.github.com> Date: Wed, 17 Apr 2019 16:37:52 -0700 Subject: [PATCH 1346/1846] adjust version restriction locking sqlalchemy version it to 1.3.0 to keep things in order --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0ec4c1aca..c8843bdd0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,5 +15,5 @@ appdirs>=1.4.3 pycryptodomex==3.6.4 websocket-client==0.54.0 sdnotify==0.3.2 -sqlalchemy>=1.3.0 +sqlalchemy==1.3.0 click==7.0 From 05ce0fdaed9435566b8caac55d8b456a7d95db94 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Wed, 17 Apr 2019 19:15:41 -0700 Subject: [PATCH 1347/1846] created tests for btsengine, btsprice feed. split methods --- dexbot/orderengines/bts_engine.py | 963 ++++++++++++++++++++++++- dexbot/orderengines/bts_engine_test.py | 64 ++ dexbot/pricefeeds/bts_feed.py | 71 +- dexbot/pricefeeds/bts_feed_test.py | 20 +- 4 files changed, 1087 insertions(+), 31 deletions(-) create mode 100644 dexbot/orderengines/bts_engine_test.py diff --git a/dexbot/orderengines/bts_engine.py b/dexbot/orderengines/bts_engine.py index 039a217d2..9b70cc9ff 100644 --- a/dexbot/orderengines/bts_engine.py +++ b/dexbot/orderengines/bts_engine.py @@ -1,14 +1,971 @@ +import datetime +import logging +import math +import time + +from dexbot.config import Config +from dexbot.storage import Storage +from dexbot.helper import truncate +from dexbot.qt_queue.idle_queue import idle_add +from dexbot.strategies.config_parts.base_config import BaseConfig + +from events import Events +import bitshares.exceptions +import bitsharesapi +import bitsharesapi.exceptions +from bitshares.account import Account +from bitshares.amount import Amount, Asset +from bitshares.dex import Dex from bitshares.instance import shared_bitshares_instance +from bitshares.market import Market +from bitshares.price import FilledOrder, Order, UpdateCallOrder +from bitshares.utils import formatTime + +# Number of maximum retries used to retry action before failing +MAX_TRIES = 3 + + +class BitsharesOrderEngine(Storage, Events): + """ + All prices are passed and returned as BASE/QUOTE. + (In the BREAD:USD market that would be USD/BREAD, 2.5 USD / 1 BREAD). + - Buy orders reserve BASE + - Sell orders reserve QUOTE + + OrderEngine inherits: + * :class:`dexbot.storage.Storage` : Stores data to sqlite database + * ``Events`` :The websocket endpoint of BitShares has notifications that are subscribed to + and dispatched by dexbot. This uses python's native Events + Available attributes: + * ``worker.bitshares``: instance of ´`bitshares.BitShares()`` + * ``worker.account``: The Account object of this worker + * ``worker.market``: The market used by this worker + * ``worker.orders``: List of open orders of the worker's account in the worker's market + * ``worker.balance``: List of assets and amounts available in the worker's account + * ``worker.log``: a per-worker logger (actually LoggerAdapter) adds worker-specific context: + worker name & account (Because some UIs might want to display per-worker logs) -class OrderEngine: + Also, Worker inherits :class:`dexbot.storage.Storage` + which allows to permanently store data in a sqlite database + using: + """ + # put configure methods in strategies + @classmethod + def configure(cls, return_base_config=True): + return BaseConfig.configure(return_base_config) + + @classmethod + def configure_details(cls, include_default_tabs=True): + return BaseConfig.configure_details(include_default_tabs) + + """ + Events are bitshares websocket specific + """ + __events__ = [ + 'onAccount', + 'onMarketUpdate', + 'onOrderMatched', + 'onOrderPlaced', + 'ontick', + 'onUpdateCallOrder', + 'error_onAccount', + 'error_onMarketUpdate', + 'error_ontick', + ] def __init__(self, - bitshares_instance=None): - + name, + config=None, + onAccount=None, + onOrderMatched=None, + onOrderPlaced=None, + onMarketUpdate=None, + onUpdateCallOrder=None, + ontick=None, + bitshares_instance=None, + *args, + **kwargs): + # BitShares instance self.bitshares = bitshares_instance or shared_bitshares_instance() # Dex instance used to get different fees for the market self.dex = Dex(self.bitshares) + # Storage + Storage.__init__(self, name) + + # Events + Events.__init__(self) + + if ontick: + self.ontick += ontick + if onMarketUpdate: + self.onMarketUpdate += onMarketUpdate + if onAccount: + self.onAccount += onAccount + if onOrderMatched: + self.onOrderMatched += onOrderMatched + if onOrderPlaced: + self.onOrderPlaced += onOrderPlaced + if onUpdateCallOrder: + self.onUpdateCallOrder += onUpdateCallOrder + + # Redirect this event to also call order placed and order matched + self.onMarketUpdate += self._callbackPlaceFillOrders + + if config: + self.config = config + else: + self.config = config = Config.get_worker_config_file(name) + + # Get worker's parameters from the config + self.worker = config["workers"][name] + + # Get Bitshares account and market for this worker + self._account = Account(self.worker["account"], full=True, bitshares_instance=self.bitshares) + + self._market = Market(config["workers"][name]["market"], bitshares_instance=self.bitshares) + + # Recheck flag - Tell the strategy to check for updated orders + self.recheck_orders = False + + # Count of orders to be fetched from the API + self.fetch_depth = 8 + + # Set fee asset + fee_asset_symbol = self.worker.get('fee_asset') + + if fee_asset_symbol: + try: + self.fee_asset = Asset(fee_asset_symbol) + except bitshares.exceptions.AssetDoesNotExistsException: + self.fee_asset = Asset('1.3.0') + else: + # If there is no fee asset, use BTS + self.fee_asset = Asset('1.3.0') + + # CER cache + self.core_exchange_rate = None + + # Ticker + self.ticker = self.market.ticker + + # 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 will be reset to False after reset only + self.disabled = False + + # Order expiration time in seconds + self.expiration = 60 * 60 * 24 * 365 * 5 + + # buy/sell actions will return order id by default + self.returnOrderId = 'head' + + # 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 + } + ) + + self.orders_log = logging.LoggerAdapter( + logging.getLogger('dexbot.orders_log'), {} + ) + + def _callbackPlaceFillOrders(self, d): + """ This method distinguishes notifications caused by Matched orders from those caused by placed orders + """ + if isinstance(d, FilledOrder): + self.onOrderMatched(d) + elif isinstance(d, Order): + self.onOrderPlaced(d) + elif isinstance(d, UpdateCallOrder): + self.onUpdateCallOrder(d) + else: + pass + + def _cancel_orders(self, orders): + try: + self.retry_action( + self.bitshares.cancel, + orders, account=self.account, fee_asset=self.fee_asset['id'] + ) + except bitsharesapi.exceptions.UnhandledRPCError as exception: + if str(exception).startswith('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: + self.log.exception("Unable to cancel order") + return False + except bitshares.exceptions.MissingKeyError: + self.log.exception('Unable to cancel order(s), private key missing.') + return False + + return True + + def account_total_value(self, return_asset): + """ Returns the total value of the account in given asset + + :param string | return_asset: Balance is returned as this asset + :return: float: Value of the account in one asset + """ + total_value = 0 + + # Total balance calculation + for balance in self.balances: + if balance['symbol'] != return_asset: + # Convert to asset if different + total_value += self.convert_asset(balance['amount'], balance['symbol'], return_asset) + else: + total_value += balance['amount'] + + # Orders balance calculation + for order in self.all_own_orders: + updated_order = self.get_updated_order(order['id']) + + if not order: + continue + if updated_order['base']['symbol'] == return_asset: + total_value += updated_order['base']['amount'] + else: + total_value += self.convert_asset( + updated_order['base']['amount'], + updated_order['base']['symbol'], + return_asset + ) + + return total_value + + def balance(self, asset, fee_reservation=0): + """ Return the balance of your worker's account in a specific asset. + + :param string | asset: In what asset the balance is wanted to be returned + :param float | fee_reservation: How much is saved in reserve for the fees + :return: Balance of specific asset + """ + balance = self._account.balance(asset) + + if fee_reservation > 0: + balance['amount'] = balance['amount'] - fee_reservation + + return balance + + 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 calculate_worker_value(self, unit_of_measure): + """ Returns the combined value of allocated and available BASE and QUOTE. Total value is + measured in "unit_of_measure", which is either BASE or QUOTE symbol. + + :param string | unit_of_measure: Asset symbol + :return: Value of the worker as float + """ + base_total = 0 + quote_total = 0 + + # Calculate total balances + balances = self.balances + for balance in balances: + if balance['symbol'] == self.base_asset: + base_total += balance['amount'] + elif balance['symbol'] == self.quote_asset: + quote_total += balance['amount'] + + # Calculate value of the orders in unit of measure + orders = self.get_own_orders + for order in orders: + if order['base']['symbol'] == self.quote_asset: + # Pick sell orders order's BASE amount, which is same as worker's QUOTE, to worker's BASE + quote_total += order['base']['amount'] + else: + base_total += order['base']['amount'] + + # Finally convert asset to another and return the sum + if unit_of_measure == self.base_asset: + quote_total = self.convert_asset(quote_total, self.quote_asset, unit_of_measure) + elif unit_of_measure == self.quote_asset: + base_total = self.convert_asset(base_total, self.base_asset, unit_of_measure) + + # Fixme: Make sure that decimal precision is correct. + return base_total + quote_total + + def cancel_all_orders(self): + """ Cancel all orders of the worker's account + """ + self.log.info('Canceling all orders') + + if self.all_own_orders: + self.cancel_orders(self.all_own_orders) + + self.log.info("Orders canceled") + + def cancel_orders(self, orders, batch_only=False): + """ Cancel specific order(s) + + :param list | orders: List of orders to cancel + :param bool | batch_only: Try cancel orders only in batch mode without one-by-one fallback + :return: + """ + if not isinstance(orders, (list, set, tuple)): + orders = [orders] + + orders = [order['id'] for order in orders if 'id' in order] + + success = self._cancel_orders(orders) + if not success and batch_only: + return False + if not success and len(orders) > 1 and not batch_only: + # One of the order cancels failed, cancel the orders one by one + for order in orders: + success = self._cancel_orders(order) + if not success: + return False + return success + + def count_asset(self, order_ids=None, return_asset=False): + """ Returns the combined amount of the given order ids and the account balance + The amounts are returned in quote and base assets of the market + + :param list | order_ids: list of order ids to be added to the balance + :param bool | return_asset: true if returned values should be Amount instances + :return: dict with keys quote and base + Todo: When would we want the sum of a subset of orders? Why order_ids? Maybe just specify asset? + """ + quote = 0 + base = 0 + quote_asset = self.market['quote']['id'] + base_asset = self.market['base']['id'] + + # Total balance calculation + for balance in self.balances: + if balance.asset['id'] == quote_asset: + quote += balance['amount'] + elif balance.asset['id'] == base_asset: + base += balance['amount'] + + if order_ids is None: + # Get all orders from Blockchain + order_ids = [order['id'] for order in self.get_own_orders] + if order_ids: + orders_balance = self.get_allocated_assets(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 get_allocated_assets(self, order_ids=None, return_asset=False): + """ Returns the amount of QUOTE and BASE allocated in orders, and that do not show up in available balance + + :param list | order_ids: + :param bool | return_asset: + :return: Dictionary of QUOTE and BASE amounts + """ + 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'] + + # Return as Amount objects instead of only float values + if return_asset: + quote = Amount(quote, quote_asset) + base = Amount(base, base_asset) + + return {'quote': quote, 'base': base} + + def get_order_cancellation_fee(self, fee_asset): + """ Returns the order cancellation fee in the specified asset. + + :param string | fee_asset: Asset in which the fee is wanted + :return: Cancellation fee as fee asset + """ + # Get fee + fees = self.dex.returnFees() + limit_order_cancel = fees['limit_order_cancel'] + return self.convert_fee(limit_order_cancel['fee'], fee_asset) + + def get_order_creation_fee(self, fee_asset): + """ Returns the cost of creating an order in the asset specified + + :param fee_asset: QUOTE, BASE, BTS, or any other + :return: + """ + # Get fee + fees = self.dex.returnFees() + limit_order_create = fees['limit_order_create'] + return self.convert_fee(limit_order_create['fee'], fee_asset) + + def get_own_spread(self): + """ Returns the difference between own closest opposite orders. + + :return: float or None: Own spread + """ + try: + # Try fetching own orders + highest_own_buy_price = self.get_highest_own_buy_order().get('price') + lowest_own_sell_price = self.get_lowest_own_sell_order().get('price') + except AttributeError: + return None + + # Calculate actual spread + actual_spread = lowest_own_sell_price / highest_own_buy_price - 1 + return actual_spread + + def get_updated_order(self, order_id): + """ Tries to get the updated order from the API. Returns None if the order doesn't exist + + :param str|dict order_id: blockchain Order object or id of the order + """ + if isinstance(order_id, dict): + order_id = order_id['id'] + + # At first, try to look up own orders. This prevents RPC calls whether requested order is own order + order = None + for limit_order in self.account['limit_orders']: + if order_id == limit_order['id']: + order = limit_order + break + else: + # We are using direct rpc call here because passing an Order object to self.get_updated_limit_order() give + # us weird error "Object of type 'BitShares' is not JSON serializable" + order = self.bitshares.rpc.get_objects([order_id])[0] + + # Do not try to continue whether there is no order in the blockchain + if not order: + return None + + updated_order = self.get_updated_limit_order(order) + return Order(updated_order, bitshares_instance=self.bitshares) + + def execute(self): + """ Execute a bundle of operations + + :return: dict: transaction + """ + self.bitshares.blocking = "head" + r = self.bitshares.txbuffer.broadcast() + self.bitshares.blocking = False + return r + + def is_buy_order(self, order): + """ Check whether an order is buy order + + :param dict | order: dict or Order object + :return bool + """ + # Check if the order is buy order, by comparing asset symbol of the order and the market + if order['base']['symbol'] == self.market['base']['symbol']: + return True + else: + return False + + def is_current_market(self, base_asset_id, quote_asset_id): + """ Returns True if given asset id's are of the current market + + :return: bool: True = Current market, False = Not current market + """ + if quote_asset_id == self.market['quote']['id']: + if base_asset_id == self.market['base']['id']: + return True + return False + + # Todo: Should we return true if market is opposite? + if quote_asset_id == self.market['base']['id']: + if base_asset_id == self.market['quote']['id']: + return True + return False + + return False + + def is_sell_order(self, order): + """ Check whether an order is sell order + + :param dict | order: dict or Order object + :return bool + """ + # Check if the order is sell order, by comparing asset symbol of the order and the market + if order['base']['symbol'] == self.market['quote']['symbol']: + return True + else: + return False + + def pause(self): + """ Pause the worker + + Note: By default pause cancels orders, but this can be overridden by strategy + """ + # Cancel all orders from the market + self.cancel_all_orders() + + # Removes worker's orders from local database + self.clear_orders() + + def clear_all_worker_data(self): + """ Clear all the worker data from the database and cancel all orders + """ + # Removes worker's orders from local database + self.clear_orders() + + # Cancel all orders from the market + self.cancel_all_orders() + + # Finally clear all worker data from the database + self.clear() + + def place_market_buy_order(self, amount, price, return_none=False, *args, **kwargs): + """ Places a buy order in the market + + :param float | amount: Order amount in QUOTE + :param float | price: Order price in BASE + :param bool | return_none: + :param args: + :param kwargs: + :return: + """ + symbol = self.market['base']['symbol'] + precision = self.market['base']['precision'] + base_amount = truncate(price * amount, precision) + return_order_id = kwargs.pop('returnOrderId', self.returnOrderId) + + # Don't try to place an order of size 0 + if not base_amount: + self.log.critical('Trying to buy 0') + self.disabled = True + return None + + # Make sure we have enough balance for the order + if return_order_id and self.balance(self.market['base']) < base_amount: + self.log.critical("Insufficient buy balance, needed {} {}".format(base_amount, symbol)) + self.disabled = True + return None + + self.log.info('Placing a buy order with {:.{prec}f} {} @ {:.8f}' + .format(base_amount, symbol, price, prec=precision)) + + # Place the order + buy_transaction = self.retry_action( + self.market.buy, + price, + Amount(amount=amount, asset=self.market["quote"]), + account=self.account.name, + expiration=self.expiration, + returnOrderId=return_order_id, + fee_asset=self.fee_asset['id'], + *args, + **kwargs + ) + + self.log.debug('Placed buy order {}'.format(buy_transaction)) + if return_order_id: + buy_order = self.get_order(buy_transaction['orderid'], return_none=return_none) + if buy_order and 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 + else: + return True + + def place_market_sell_order(self, amount, price, return_none=False, invert=False, *args, **kwargs): + """ Places a sell order in the market + + :param float | amount: Order amount in QUOTE + :param float | price: Order price in BASE + :param bool | return_none: + :param bool | invert: True = return inverted sell order + :param args: + :param kwargs: + :return: + """ + symbol = self.market['quote']['symbol'] + precision = self.market['quote']['precision'] + quote_amount = truncate(amount, precision) + return_order_id = kwargs.pop('returnOrderId', self.returnOrderId) + + # Don't try to place an order of size 0 + if not quote_amount: + self.log.critical('Trying to sell 0') + self.disabled = True + return None + + # Make sure we have enough balance for the order + if return_order_id and self.balance(self.market['quote']) < quote_amount: + self.log.critical("Insufficient sell balance, needed {} {}".format(amount, symbol)) + self.disabled = True + return None + + self.log.info('Placing a sell order with {:.{prec}f} {} @ {:.8f}' + .format(quote_amount, symbol, price, prec=precision)) + + # Place the order + sell_transaction = self.retry_action( + self.market.sell, + price, + Amount(amount=amount, asset=self.market["quote"]), + account=self.account.name, + expiration=self.expiration, + returnOrderId=return_order_id, + fee_asset=self.fee_asset['id'], + *args, + **kwargs + ) + + self.log.debug('Placed sell order {}'.format(sell_transaction)) + if return_order_id: + sell_order = self.get_order(sell_transaction['orderid'], return_none=return_none) + if sell_order and 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) + self.recheck_orders = True + if sell_order and invert: + sell_order.invert() + return sell_order + else: + return True + + def retry_action(self, action, *args, **kwargs): + """ Perform an action, and if certain suspected-to-be-spurious grapheme 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 + + :param action: + :return: + """ + tries = 0 + while True: + try: + return action(*args, **kwargs) + except bitsharesapi.exceptions.UnhandledRPCError as exception: + if "Assert Exception: amount_to_sell.amount > 0" in str(exception): + if tries > MAX_TRIES: + raise + else: + tries += 1 + self.log.warning("Ignoring: '{}'".format(str(exception))) + self.bitshares.txbuffer.clear() + self.account.refresh() + time.sleep(2) + elif "now <= trx.expiration" in str(exception): # Usually loss of sync to blockchain + if tries > MAX_TRIES: + raise + else: + tries += 1 + self.log.warning("retrying on '{}'".format(str(exception))) + self.bitshares.txbuffer.clear() + time.sleep(6) # Wait at least a BitShares block + elif "trx.expiration <= now + chain_parameters.maximum_time_until_expiration" in str(exception): + if tries > MAX_TRIES: + info = self.bitshares.info() + raise Exception('Too much difference between node block time and trx expiration, please change ' + 'the node. Block time: {}, local time: {}' + .format(info['time'], formatTime(datetime.datetime.utcnow()))) + else: + tries += 1 + self.log.warning('Too much difference between node block time and trx expiration, switching ' + 'node') + self.bitshares.txbuffer.clear() + self.bitshares.rpc.next() + elif "Assert Exception: delta.amount > 0: Insufficient Balance" in str(exception): + self.log.critical('Insufficient balance of fee asset') + raise + else: + raise + + def store_profit_estimation_data(self): + """ Save total quote, total base, center_price, and datetime in to the database + """ + assets = self.count_asset() + account = self.config['workers'][self.worker_name].get('account') + base_amount = assets['base'] + base_symbol = self.market['base'].get('symbol') + quote_amount = assets['quote'] + quote_symbol = self.market['quote'].get('symbol') + center_price = self.get_market_center_price() + timestamp = time.time() + + self.store_balance_entry(account, self.worker_name, base_amount, base_symbol, + quote_amount, quote_symbol, center_price, timestamp) + + def get_profit_estimation_data(self, seconds): + """ Get balance history closest to the given time + + :returns The data as dict from the first timestamp going backwards from seconds argument + """ + return self.get_balance_history(self.config['workers'][self.worker_name].get('account'), + self.worker_name, seconds) + + def calc_profit(self): + """ Calculate relative profit for the current worker + """ + profit = 0 + time_range = 60 * 60 * 24 * 7 # 7 days + current_time = time.time() + timestamp = current_time - time_range + + # Fetch the balance from history + old_data = self.get_balance_history(self.config['workers'][self.worker_name].get('account'), self.worker_name, + timestamp, self.base_asset, self.quote_asset) + if old_data: + earlier_base = old_data.base_total + earlier_quote = old_data.quote_total + old_center_price = old_data.center_price + center_price = self.get_market_center_price() + + if not (old_center_price or center_price): + return profit + + # Calculate max theoretical balances based on starting price + old_max_quantity_base = earlier_base + earlier_quote * old_center_price + old_max_quantity_quote = earlier_quote + earlier_base / old_center_price + + if not (old_max_quantity_base or old_max_quantity_quote): + return profit + + # Current balances + balance = self.count_asset() + base_balance = balance['base'] + quote_balance = balance['quote'] + + # Calculate max theoretical current balances + max_quantity_base = base_balance + quote_balance * center_price + max_quantity_quote = quote_balance + base_balance / center_price + + base_roi = max_quantity_base / old_max_quantity_base + quote_roi = max_quantity_quote / old_max_quantity_quote + profit = round(math.sqrt(base_roi * quote_roi) - 1, 4) + + return profit + + def write_order_log(self, worker_name, order): + """ Write order log to csv file + + :param string | worker_name: Name of the worker + :param object | order: Order that was fulfilled + """ + operation_type = 'TRADE' + + if order['base']['symbol'] == self.market['base']['symbol']: + base_symbol = order['base']['symbol'] + base_amount = -order['base']['amount'] + quote_symbol = order['quote']['symbol'] + quote_amount = order['quote']['amount'] + else: + base_symbol = order['quote']['symbol'] + base_amount = order['quote']['amount'] + quote_symbol = order['base']['symbol'] + quote_amount = -order['base']['amount'] + + message = '{};{};{};{};{};{};{};{}'.format( + worker_name, + order['id'], + operation_type, + base_symbol, + base_amount, + quote_symbol, + quote_amount, + datetime.datetime.now().isoformat() + ) + + self.orders_log.info(message) + + @property + def account(self): + """ Return the full account as :class:`bitshares.account.Account` object! + Can be refreshed by using ``x.refresh()`` + + :return: object | Account + """ + return self._account + + @property + def balances(self): + """ Returns all the balances of the account assigned for the worker. + + :return: Balances in list where each asset is in their own Amount object + """ + return self._account.balances + + @property + def base_asset(self): + return self.worker['market'].split('/')[1] + + @property + def quote_asset(self): + return self.worker['market'].split('/')[0] + + @property + def all_own_orders(self, refresh=True): + """ Return the worker's open orders in all markets + + :param bool | refresh: Use most recent data + :return: List of Order objects + """ + # Refresh account data + if refresh: + self.account.refresh() + + orders = [] + for order in self.account.openorders: + orders.append(order) + + return orders + + @property + def get_own_orders(self): + """ Return the account's open orders in the current market + + :return: List of Order objects + """ + orders = [] + + # Refresh account data + self.account.refresh() + + for order in self.account.openorders: + if self.worker["market"] == order.market and self.account.openorders: + orders.append(order) + + return orders + + @property #todo: duplicate definition, also in price feed + def market(self): + """ Return the market object as :class:`bitshares.market.Market` + """ + return self._market + + @staticmethod + def get_updated_limit_order(limit_order): + """ Returns a modified limit_order so that when passed to Order class, + will return an Order object with updated amount values + + :param limit_order: an item of Account['limit_orders'] or bitshares.rpc.get_limit_orders() + :return: Order + """ + order = copy.deepcopy(limit_order) + price = float(order['sell_price']['base']['amount']) / float(order['sell_price']['quote']['amount']) + base_amount = float(order['for_sale']) + quote_amount = base_amount / price + order['sell_price']['base']['amount'] = base_amount + order['sell_price']['quote']['amount'] = quote_amount + return order + + @staticmethod + def convert_asset(from_value, from_asset, to_asset): + """ Converts asset to another based on the latest market value + + :param float | from_value: Amount of the input asset + :param string | from_asset: Symbol of the input asset + :param string | to_asset: Symbol of the output asset + :return: float Asset converted to another asset as float value + """ + market = Market('{}/{}'.format(from_asset, to_asset)) + ticker = market.ticker() + latest_price = ticker.get('latest', {}).get('price', None) + precision = market['base']['precision'] + + return truncate((from_value * latest_price), precision) + + def convert_fee(self, fee_amount, fee_asset): + """ Convert fee amount in BTS to fee in fee_asset + + :param float | fee_amount: fee amount paid in BTS + :param Asset | fee_asset: fee asset to pay fee in + :return: float | amount of fee_asset to pay fee + """ + if isinstance(fee_asset, str): + fee_asset = Asset(fee_asset) + + if fee_asset['id'] == '1.3.0': + # Fee asset is BTS, so no further calculations are needed + return fee_amount + else: + if not self.core_exchange_rate: + # Determine how many fee_asset is needed for core-exchange + temp_market = Market(base=fee_asset, quote=Asset('1.3.0')) + self.core_exchange_rate = temp_market.ticker()['core_exchange_rate'] + return fee_amount * self.core_exchange_rate['base']['amount'] + + @staticmethod + def get_order(order_id, return_none=True): + """ Get Order object with order_id + + :param str | dict order_id: blockchain object id of the order can be an order 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 + :return: Order object + """ + if not order_id: + return None + if 'id' in order_id: + order_id = order_id['id'] + try: + order = Order(order_id) + except Exception: + logging.getLogger(__name__).error('Got an exception getting order id {}'.format(order_id)) + raise + if return_none and order['deleted']: + return None + return order + + @staticmethod + def purge_all_local_worker_data(worker_name): + """ Removes worker's data and orders from local sqlite database + + :param worker_name: Name of the worker to be removed + """ + Storage.clear_worker_data(worker_name) + + # GUI updaters + def update_gui_slider(self): + ticker = self.market.ticker() + latest_price = ticker.get('latest', {}).get('price', None) + if not latest_price: + return + + total_balance = self.count_asset() + 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 + + def update_gui_profit(self): + profit = self.calc_profit() + + # Add to idle queue + idle_add(self.view.set_worker_profit, self.worker_name, float(profit)) + self['profit'] = profit diff --git a/dexbot/orderengines/bts_engine_test.py b/dexbot/orderengines/bts_engine_test.py new file mode 100644 index 000000000..bdda8c0d0 --- /dev/null +++ b/dexbot/orderengines/bts_engine_test.py @@ -0,0 +1,64 @@ +from bitshares.bitshares import BitShares +from bitshares.market import Market + +from dexbot.config import Config + +from dexbot.orderengines.bts_engine import BitsharesOrderEngine +from dexbot.strategies.base import StrategyBase + +def fixture_data(): + + TEST_CONFIG = { + 'node': 'wss://api.fr.bitsharesdex.com/ws', + 'workers': { + 'worker 1': { + 'account': 'octet5', + 'amount': 1.0, + 'center_price': 0.3, + 'center_price_depth': 0.0, + 'center_price_dynamic': False, + 'center_price_offset': False, + 'custom_expiration': False, + 'dynamic_spread': False, + 'dynamic_spread_factor': 1.0, + 'expiration_time': 157680000.0, + 'external_feed': False, + 'external_price_source': 'null', + 'fee_asset': 'BTS', + 'manual_offset': 0.0, + 'market': 'OPEN.BTC/BTS', + 'market_depth_amount': 0.0, + 'module': 'dexbot.strategies.relative_orders', + 'partial_fill_threshold': 30.0, + 'price_change_threshold': 2.0, + 'relative_order_size': False, + 'reset_on_partial_fill': True, + 'reset_on_price_change': False, + 'spread': 5.0 + } + } + } + return TEST_CONFIG + + +yml_data = fixture_data() +config = Config(config=yml_data) +bts = BitShares(node=config['node']) +print("Bitshares Price Feed Test") + + +for worker_name, worker in config["workers"].items(): + print(worker_name) + print(worker) + pair = config["workers"][worker_name]["market"] + market = Market(config["workers"][worker_name]["market"]) + print(pair) + + print("instantiating StrategyBase as a base comparison") + strategy = StrategyBase(worker_name, config=config, bitshares_instance=bts) + + print("instantiating Bitshares order engine") + orderEngine = BitsharesOrderEngine(worker_name, config=config, bitshares_instance=bts) + + + diff --git a/dexbot/pricefeeds/bts_feed.py b/dexbot/pricefeeds/bts_feed.py index 655051513..cd7dacd86 100644 --- a/dexbot/pricefeeds/bts_feed.py +++ b/dexbot/pricefeeds/bts_feed.py @@ -5,8 +5,18 @@ from bitshares.instance import shared_bitshares_instance from bitshares.price import Order -class PriceFeed: - +class BitsharesPriceFeed: + """ + Price Feed class enables usage of Bitshares DEX for market center and order + book pricing, without requiring a registered account. It may be use for both + strategy and indicator analysis tools. + + All prices are passed and returned as BASE/QUOTE. + (In the BREAD/USD market that would be USD/BREAD, 2.5 USD / 1 BREAD). + - Buy orders reserve BASE + - Sell orders reserve QUOTE + + """ def __init__(self, market, bitshares_instance=None): @@ -35,8 +45,9 @@ def get_market_orders(self, depth=1, updated=True): :return: Returns a list of orders or None """ orders = self.bitshares.rpc.get_limit_orders(self.market['base']['id'], self.market['quote']['id'], depth) - if updated: - orders = [self.get_updated_limit_order(o) for o in orders] +# if updated: +# orders = [self.get_updated_limit_order(o) for o in orders] + # ## todo: how to best split between order/pricefeed? orders = [Order(o, bitshares_instance=self.bitshares) for o in orders] return orders @@ -86,6 +97,39 @@ def filter_sell_orders(self, orders, sort=None, invert=True): return sell_orders + def get_highest_market_buy_order(self, orders=None): + """ Returns the highest buy order that is not own, regardless of order size. + + :param list | orders: Optional list of orders, if none given fetch newest from market + :return: Highest market buy order or None + """ + if not orders: + orders = self.get_market_buy_orders(1) + + try: + order = orders[0] + except IndexError: + self.log.info('Market has no buy orders.') + return None + + return order + + def get_lowest_market_sell_order(self, orders=None): + """ Returns the lowest sell order that is not own, regardless of order size. + + :param list | orders: Optional list of orders, if none given fetch newest from market + :return: Lowest market sell order or None + """ + if not orders: + orders = self.get_market_sell_orders(1) + + try: + order = orders[0] + except IndexError: + self.log.info('Market has no sell orders.') + return None + + return order def get_market_buy_orders(self, depth=10): """ Fetches most recent data and returns list of buy orders. @@ -107,7 +151,7 @@ def get_market_sell_orders(self, depth=10): sell_orders = self.filter_sell_orders(orders) return sell_orders - def get_market_buy_price(self, quote_amount=0, base_amount=0): + def get_market_buy_price(self, quote_amount=0, base_amount=0): # TODO: refactor to use orders instead of exclude_own_orders """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with moving average or weighted moving average @@ -171,7 +215,7 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0): return base_amount / quote_amount - def get_market_sell_price(self, quote_amount=0, base_amount=0): + def get_market_sell_price(self, quote_amount=0, base_amount=0):# TODO: refactor to use orders instead of exclude_own_orders """ Returns the BASE/QUOTE price for which [quote_amount] worth of QUOTE could be bought, enhanced with moving average or weighted moving average. @@ -288,21 +332,6 @@ def market(self): """ return self._market - @staticmethod - def get_updated_limit_order(limit_order): - """ Returns a modified limit_order so that when passed to Order class, - will return an Order object with updated amount values - - :param limit_order: an item of Account['limit_orders'] or bitshares.rpc.get_limit_orders() - :return: Order - """ - order = copy.deepcopy(limit_order) - price = float(order['sell_price']['base']['amount']) / float(order['sell_price']['quote']['amount']) - base_amount = float(order['for_sale']) - quote_amount = base_amount / price - order['sell_price']['base']['amount'] = base_amount - order['sell_price']['quote']['amount'] = quote_amount - return order @staticmethod def sort_orders_by_price(orders, sort='DESC'): diff --git a/dexbot/pricefeeds/bts_feed_test.py b/dexbot/pricefeeds/bts_feed_test.py index 4817b2de9..059fe17f5 100644 --- a/dexbot/pricefeeds/bts_feed_test.py +++ b/dexbot/pricefeeds/bts_feed_test.py @@ -1,8 +1,7 @@ from bitshares.bitshares import BitShares from bitshares.market import Market -#from bitshares.price import Order -from dexbot.pricefeeds.bts_feed import PriceFeed +from dexbot.pricefeeds.bts_feed import BitsharesPriceFeed node_url = "wss://api.fr.bitsharesdex.com/ws" @@ -17,13 +16,13 @@ market = Market("USD:BTS") print(market.ticker()) -pf = PriceFeed(market=market, bitshares_instance=bts) +pf = BitsharesPriceFeed(market=market, bitshares_instance=bts) market = pf.market -print("Market we are examining:", market, sep=':') +print("\nMarket we are examining:", market, sep=':') center_price = pf.get_market_center_price(base_amount=0, quote_amount=0, suppress_errors=False) -print("center price:", center_price, sep=':') +print("\nCenter price:", center_price, sep=':') print("\nList of buy orders:") buy_orders = pf.get_market_buy_orders(depth=10) @@ -53,8 +52,15 @@ mkt_spread = pf.get_market_spread(quote_amount=0, base_amount=0) print("market spread", mkt_spread, sep=':') +highest = pf.get_highest_market_buy_order(asc_buy_orders) +print("Highest market buy order", highest, sep=':') + +lowest = pf.get_lowest_market_sell_order(sell_orders) +print("Lowest market sell order", lowest, sep=':') + # todo: -# filter buy/sell orders (2) -# get_updated_limit_order (static method) + +# get_updated_limit_order (static method) resolve the usage across 2 classes +# refactor get_market_buy/sell_price to use orders instead of "exclude_own_price" From f68df3731454e4eac52eacf091cb3370cb516390 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Wed, 17 Apr 2019 23:08:51 -0700 Subject: [PATCH 1348/1846] temporary btsengine in relative feeds, to test --- dexbot/orderengines/bts_engine.py | 16 +++++++ dexbot/orderengines/bts_engine_test.py | 60 ++++++++++++-------------- dexbot/pricefeeds/bts_feed.py | 27 +++++++----- dexbot/strategies/relative_orders.py | 6 ++- 4 files changed, 66 insertions(+), 43 deletions(-) diff --git a/dexbot/orderengines/bts_engine.py b/dexbot/orderengines/bts_engine.py index 9b70cc9ff..bb88accaa 100644 --- a/dexbot/orderengines/bts_engine.py +++ b/dexbot/orderengines/bts_engine.py @@ -404,6 +404,22 @@ def get_allocated_assets(self, order_ids=None, return_asset=False): return {'quote': quote, 'base': base} + def get_market_orders(self, depth=1, updated=True): + """ Returns orders from the current market. Orders are sorted by price. + + get_market_orders() call does not have any depth limit. + + :param int | depth: Amount of orders per side will be fetched, default=1 + :param bool | updated: Return updated orders. "Updated" means partially filled orders will represent + remainders and not just initial amounts + :return: Returns a list of orders or None + """ + orders = self.bitshares.rpc.get_limit_orders(self.market['base']['id'], self.market['quote']['id'], depth) + if updated: + orders = [self.get_updated_limit_order(o) for o in orders] + orders = [Order(o, bitshares_instance=self.bitshares) for o in orders] + return orders + def get_order_cancellation_fee(self, fee_asset): """ Returns the order cancellation fee in the specified asset. diff --git a/dexbot/orderengines/bts_engine_test.py b/dexbot/orderengines/bts_engine_test.py index bdda8c0d0..1e4a8f2ce 100644 --- a/dexbot/orderengines/bts_engine_test.py +++ b/dexbot/orderengines/bts_engine_test.py @@ -2,40 +2,39 @@ from bitshares.market import Market from dexbot.config import Config - from dexbot.orderengines.bts_engine import BitsharesOrderEngine from dexbot.strategies.base import StrategyBase -def fixture_data(): +def fixture_data(): TEST_CONFIG = { 'node': 'wss://api.fr.bitsharesdex.com/ws', 'workers': { - 'worker 1': { - 'account': 'octet5', - 'amount': 1.0, - 'center_price': 0.3, - 'center_price_depth': 0.0, - 'center_price_dynamic': False, - 'center_price_offset': False, - 'custom_expiration': False, - 'dynamic_spread': False, - 'dynamic_spread_factor': 1.0, - 'expiration_time': 157680000.0, - 'external_feed': False, - 'external_price_source': 'null', - 'fee_asset': 'BTS', - 'manual_offset': 0.0, - 'market': 'OPEN.BTC/BTS', - 'market_depth_amount': 0.0, - 'module': 'dexbot.strategies.relative_orders', - 'partial_fill_threshold': 30.0, - 'price_change_threshold': 2.0, - 'relative_order_size': False, - 'reset_on_partial_fill': True, - 'reset_on_price_change': False, - 'spread': 5.0 - } + 'worker 1': { + 'account': 'octet5', # edit this for TESTNET + 'amount': 1.0, + 'center_price': 0.3, + 'center_price_depth': 0.0, + 'center_price_dynamic': False, + 'center_price_offset': False, + 'custom_expiration': False, + 'dynamic_spread': False, + 'dynamic_spread_factor': 1.0, + 'expiration_time': 157680000.0, + 'external_feed': False, + 'external_price_source': 'null', + 'fee_asset': 'BTS', + 'manual_offset': 0.0, + 'market': 'OPEN.BTC/BTS', + 'market_depth_amount': 0.0, + 'module': 'dexbot.strategies.relative_orders', + 'partial_fill_threshold': 30.0, + 'price_change_threshold': 2.0, + 'relative_order_size': False, + 'reset_on_partial_fill': True, + 'reset_on_price_change': False, + 'spread': 5.0 + } } } return TEST_CONFIG @@ -46,10 +45,8 @@ def fixture_data(): bts = BitShares(node=config['node']) print("Bitshares Price Feed Test") - for worker_name, worker in config["workers"].items(): - print(worker_name) - print(worker) + print(worker_name, worker) pair = config["workers"][worker_name]["market"] market = Market(config["workers"][worker_name]["market"]) print(pair) @@ -60,5 +57,4 @@ def fixture_data(): print("instantiating Bitshares order engine") orderEngine = BitsharesOrderEngine(worker_name, config=config, bitshares_instance=bts) - - + print("Getting own orders:", orderEngine.get_own_orders) \ No newline at end of file diff --git a/dexbot/pricefeeds/bts_feed.py b/dexbot/pricefeeds/bts_feed.py index cd7dacd86..eaa80d50b 100644 --- a/dexbot/pricefeeds/bts_feed.py +++ b/dexbot/pricefeeds/bts_feed.py @@ -34,23 +34,30 @@ def __init__(self, logging.getLogger('dexbot.pricefeed_log'), {} ) - def get_market_orders(self, depth=1, updated=True): + def get_limit_orders(self, depth=1): """ Returns orders from the current market. Orders are sorted by price. - get_market_orders() call does not have any depth limit. - + get_limit_orders() call does not have any depth limit. :param int | depth: Amount of orders per side will be fetched, default=1 - :param bool | updated: Return updated orders. "Updated" means partially filled orders will represent - remainders and not just initial amounts :return: Returns a list of orders or None """ orders = self.bitshares.rpc.get_limit_orders(self.market['base']['id'], self.market['quote']['id'], depth) -# if updated: -# orders = [self.get_updated_limit_order(o) for o in orders] - # ## todo: how to best split between order/pricefeed? orders = [Order(o, bitshares_instance=self.bitshares) for o in orders] return orders + def get_orderbook_orders(self, depth=1): + """ Returns orders from the current market split in bids and asks. Orders are sorted by price. + + Market.orderbook() call has hard-limit of depth=50 enforced by bitshares node. + + bids = buy orders + asks = sell orders + + :param int | depth: Amount of orders per side will be fetched, default=1 + :return: Returns a dictionary of orders or None + """ + return self.market.orderbook(depth) + def filter_buy_orders(self, orders, sort=None): """ Return own buy orders from list of orders. Can be used to pick buy orders from a list that is not up to date with the blockchain data. @@ -137,7 +144,7 @@ def get_market_buy_orders(self, depth=10): :param int | depth: Amount of buy orders returned, Default=10 :return: List of market sell orders """ - orders = self.get_market_orders(depth=depth) + orders = self.get_limit_orders(depth=depth) buy_orders = self.filter_buy_orders(orders) return buy_orders @@ -147,7 +154,7 @@ def get_market_sell_orders(self, depth=10): :param int | depth: Amount of sell orders returned, Default=10 :return: List of market sell orders """ - orders = self.get_market_orders(depth=depth) + orders = self.get_limit_orders(depth=depth) sell_orders = self.filter_sell_orders(orders) return sell_orders diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 2b1e6d348..2c5350b3e 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -4,8 +4,12 @@ from .base import StrategyBase from .config_parts.relative_config import RelativeConfig from dexbot.strategies.external_feeds.price_feed import PriceFeed +from dexbot.orderengines.bts_engine import BitsharesOrderEngine +from dexbot.pricefeeds.bts_feed import BitsharesPriceFeed -class Strategy(StrategyBase): +#class Strategy(StrategyBase): + +class Strategy(BitsharesOrderEngine, BitsharesPriceFeed): """ Relative Orders strategy """ From 57bcee6eabff26c8761228b061b7066afc3d1fed Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Wed, 17 Apr 2019 23:36:04 -0700 Subject: [PATCH 1349/1846] move base methods into bts engine --- dexbot/orderengines/bts_engine.py | 52 ++++++++++++++++++++++++++++++- dexbot/pricefeeds/bts_feed.py | 12 ++++--- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/dexbot/orderengines/bts_engine.py b/dexbot/orderengines/bts_engine.py index bb88accaa..f2c24f388 100644 --- a/dexbot/orderengines/bts_engine.py +++ b/dexbot/orderengines/bts_engine.py @@ -404,6 +404,34 @@ def get_allocated_assets(self, order_ids=None, return_asset=False): return {'quote': quote, 'base': base} + def get_highest_own_buy_order(self, orders=None): + """ Returns highest own buy order. + + :param list | orders: + :return: Highest own buy order by price at the market or None + """ + if not orders: + orders = self.get_own_buy_orders() + + try: + return orders[0] + except IndexError: + return None + + def get_lowest_own_sell_order(self, orders=None): + """ Returns lowest own sell order. + + :param list | orders: + :return: Lowest own sell order by price at the market + """ + if not orders: + orders = self.get_own_sell_orders() + + try: + return orders[0] + except IndexError: + return None + def get_market_orders(self, depth=1, updated=True): """ Returns orders from the current market. Orders are sorted by price. @@ -442,6 +470,28 @@ def get_order_creation_fee(self, fee_asset): limit_order_create = fees['limit_order_create'] return self.convert_fee(limit_order_create['fee'], fee_asset) + def get_own_buy_orders(self, orders=None): + """ Get own buy orders from current market, or from a set of orders passed for this function. + + :return: List of buy orders + """ + if not orders: + # List of orders was not given so fetch everything from the market + orders = self.get_own_orders + + return self.filter_buy_orders(orders) + + def get_own_sell_orders(self, orders=None): + """ Get own sell orders from current market + + :return: List of sell orders + """ + if not orders: + # List of orders was not given so fetch everything from the market + orders = self.get_own_orders + + return self.filter_sell_orders(orders) + def get_own_spread(self): """ Returns the difference between own closest opposite orders. @@ -875,7 +925,7 @@ def get_own_orders(self): return orders - @property #todo: duplicate definition, also in price feed + @property #todo: duplicate property, also in price feed def market(self): """ Return the market object as :class:`bitshares.market.Market` """ diff --git a/dexbot/pricefeeds/bts_feed.py b/dexbot/pricefeeds/bts_feed.py index eaa80d50b..c3ce99cab 100644 --- a/dexbot/pricefeeds/bts_feed.py +++ b/dexbot/pricefeeds/bts_feed.py @@ -7,8 +7,8 @@ class BitsharesPriceFeed: """ - Price Feed class enables usage of Bitshares DEX for market center and order - book pricing, without requiring a registered account. It may be use for both + This Price Feed class enables usage of Bitshares DEX for market center and order + book pricing, without requiring a registered account. It may be used for both strategy and indicator analysis tools. All prices are passed and returned as BASE/QUOTE. @@ -35,7 +35,7 @@ def __init__(self, ) def get_limit_orders(self, depth=1): - """ Returns orders from the current market. Orders are sorted by price. + """ Returns orders from the current market. Orders are sorted by price. Does not require account info. get_limit_orders() call does not have any depth limit. :param int | depth: Amount of orders per side will be fetched, default=1 @@ -158,7 +158,8 @@ def get_market_sell_orders(self, depth=10): sell_orders = self.filter_sell_orders(orders) return sell_orders - def get_market_buy_price(self, quote_amount=0, base_amount=0): # TODO: refactor to use orders instead of exclude_own_orders + def get_market_buy_price(self, quote_amount=0, base_amount=0): + # TODO: refactor to use orders instead of exclude_own_orders """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with moving average or weighted moving average @@ -222,7 +223,8 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0): # TODO: refactor return base_amount / quote_amount - def get_market_sell_price(self, quote_amount=0, base_amount=0):# TODO: refactor to use orders instead of exclude_own_orders + def get_market_sell_price(self, quote_amount=0, base_amount=0): + # TODO: refactor to use orders instead of exclude_own_orders """ Returns the BASE/QUOTE price for which [quote_amount] worth of QUOTE could be bought, enhanced with moving average or weighted moving average. From 833814db76116003457251d1f6b259507fc5b164 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Wed, 17 Apr 2019 23:38:02 -0700 Subject: [PATCH 1350/1846] update bts_feed_test for method change --- dexbot/pricefeeds/bts_feed_test.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dexbot/pricefeeds/bts_feed_test.py b/dexbot/pricefeeds/bts_feed_test.py index 059fe17f5..d441f4711 100644 --- a/dexbot/pricefeeds/bts_feed_test.py +++ b/dexbot/pricefeeds/bts_feed_test.py @@ -40,7 +40,7 @@ sell_orders = pf.get_market_sell_orders(depth=10) print("\nMarket Sell Orders", sell_orders, sep=':') -mkt_orders = pf.get_market_orders(depth=1, updated=True) +mkt_orders = pf.get_limit_orders(depth=1) print("\nMarket Orders", mkt_orders, sep=":") mkt_buy_price = pf.get_market_buy_price(quote_amount=0, base_amount=0) @@ -61,6 +61,3 @@ # todo: -# get_updated_limit_order (static method) resolve the usage across 2 classes -# refactor get_market_buy/sell_price to use orders instead of "exclude_own_price" - From b654bbd9597e900d57608137cb14ed1c4a47eaa1 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 18 Apr 2019 12:57:52 -0700 Subject: [PATCH 1351/1846] move get_market_xxx_price with exclude flag to relative orders. add comments --- dexbot/orderengines/bts_engine.py | 6 +- dexbot/orderengines/bts_engine_test.py | 56 +++++---- dexbot/strategies/relative_orders.py | 155 +++++++++++++++++++++++++ 3 files changed, 189 insertions(+), 28 deletions(-) diff --git a/dexbot/orderengines/bts_engine.py b/dexbot/orderengines/bts_engine.py index f2c24f388..6517215e3 100644 --- a/dexbot/orderengines/bts_engine.py +++ b/dexbot/orderengines/bts_engine.py @@ -24,7 +24,7 @@ # Number of maximum retries used to retry action before failing MAX_TRIES = 3 - +## this is TEMPORARY CLASS before we move the Orders out class BitsharesOrderEngine(Storage, Events): """ All prices are passed and returned as BASE/QUOTE. @@ -769,7 +769,7 @@ def retry_action(self, action, *args, **kwargs): else: raise - def store_profit_estimation_data(self): + def store_profit_estimation_data(self): # todo: move this method into strategy """ Save total quote, total base, center_price, and datetime in to the database """ assets = self.count_asset() @@ -792,7 +792,7 @@ def get_profit_estimation_data(self, seconds): return self.get_balance_history(self.config['workers'][self.worker_name].get('account'), self.worker_name, seconds) - def calc_profit(self): + def calc_profit(self): # todo: move this method into strategy """ Calculate relative profit for the current worker """ profit = 0 diff --git a/dexbot/orderengines/bts_engine_test.py b/dexbot/orderengines/bts_engine_test.py index 1e4a8f2ce..a57d23376 100644 --- a/dexbot/orderengines/bts_engine_test.py +++ b/dexbot/orderengines/bts_engine_test.py @@ -11,27 +11,27 @@ def fixture_data(): 'node': 'wss://api.fr.bitsharesdex.com/ws', 'workers': { 'worker 1': { - 'account': 'octet5', # edit this for TESTNET - 'amount': 1.0, - 'center_price': 0.3, - 'center_price_depth': 0.0, - 'center_price_dynamic': False, - 'center_price_offset': False, + 'account': 'octet5', # edit this for TESTNET Account + 'amount': 0.015, + 'center_price': 0.0, + 'center_price_depth': 0.4, + 'center_price_dynamic': True, + 'center_price_offset': True, 'custom_expiration': False, 'dynamic_spread': False, 'dynamic_spread_factor': 1.0, 'expiration_time': 157680000.0, - 'external_feed': False, - 'external_price_source': 'null', + 'external_feed': True, + 'external_price_source': 'gecko', 'fee_asset': 'BTS', 'manual_offset': 0.0, - 'market': 'OPEN.BTC/BTS', - 'market_depth_amount': 0.0, + 'market': 'OPEN.XMR/BTS', + 'market_depth_amount': 0.20, 'module': 'dexbot.strategies.relative_orders', 'partial_fill_threshold': 30.0, 'price_change_threshold': 2.0, 'relative_order_size': False, - 'reset_on_partial_fill': True, + 'reset_on_partial_fill': False, 'reset_on_price_change': False, 'spread': 5.0 } @@ -39,22 +39,28 @@ def fixture_data(): } return TEST_CONFIG +def setup_test(): -yml_data = fixture_data() -config = Config(config=yml_data) -bts = BitShares(node=config['node']) -print("Bitshares Price Feed Test") + yml_data = fixture_data() + config = Config(config=yml_data) + bts = BitShares(node=config['node']) + print("Bitshares Price Feed Test") -for worker_name, worker in config["workers"].items(): - print(worker_name, worker) - pair = config["workers"][worker_name]["market"] - market = Market(config["workers"][worker_name]["market"]) - print(pair) + for worker_name, worker in config["workers"].items(): + print(worker_name, worker) + pair = config["workers"][worker_name]["market"] + market = Market(config["workers"][worker_name]["market"]) + print(pair) - print("instantiating StrategyBase as a base comparison") - strategy = StrategyBase(worker_name, config=config, bitshares_instance=bts) + print("instantiating StrategyBase as a base comparison") + strategy = StrategyBase(worker_name, config=config, bitshares_instance=bts) - print("instantiating Bitshares order engine") - orderEngine = BitsharesOrderEngine(worker_name, config=config, bitshares_instance=bts) + print("instantiating Bitshares order engine") + orderEngine = BitsharesOrderEngine(worker_name, config=config, bitshares_instance=bts) - print("Getting own orders:", orderEngine.get_own_orders) \ No newline at end of file + print("Getting own orders:", orderEngine.get_own_orders) + + +# don't run this as is, or else it will over write the default config.yml +# put into unit testing structure +#setup_test() \ No newline at end of file diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 2c5350b3e..3efe484f5 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -9,6 +9,7 @@ #class Strategy(StrategyBase): +# this inheritance is temporary before we finish refactoring strategybase class Strategy(BitsharesOrderEngine, BitsharesPriceFeed): """ Relative Orders strategy """ @@ -239,6 +240,160 @@ def update_orders(self): if len(order_ids) < expected_num_orders and not self.disabled: self.update_orders() + def get_market_buy_price(self, quote_amount=0, base_amount=0, exclude_own_orders=True): + """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with + moving average or weighted moving average + + :param float | quote_amount: + :param float | base_amount: + :param bool | exclude_own_orders: Exclude own orders when calculating a price + :return: price as float + """ + market_buy_orders = [] + + # Exclude own orders from orderbook if needed + if exclude_own_orders: + market_buy_orders = self.get_market_buy_orders(depth=self.fetch_depth) + own_buy_orders_ids = [o['id'] for o in self.get_own_buy_orders()] + market_buy_orders = [o for o in market_buy_orders if o['id'] not in own_buy_orders_ids] + + # In case amount is not given, return price of the highest buy order on the market + if quote_amount == 0 and base_amount == 0: + if exclude_own_orders: + if market_buy_orders: + return float(market_buy_orders[0]['price']) + else: + return '0.0' + else: + return float(self.ticker().get('highestBid')) + + # Like get_market_sell_price(), but defaulting to base_amount if both base and quote are specified. + asset_amount = base_amount + + """ Since the purpose is never get both quote and base amounts, favor base amount if both given because + this function is looking for buy price. + """ + if base_amount > quote_amount: + base = True + else: + asset_amount = quote_amount + base = False + + if not market_buy_orders: + market_buy_orders = self.get_market_buy_orders(depth=self.fetch_depth) + market_fee = self.market['base'].market_fee_percent + + target_amount = asset_amount * (1 + market_fee) + + quote_amount = 0 + base_amount = 0 + missing_amount = target_amount + + for order in market_buy_orders: + if base: + # BASE amount was given + if order['base']['amount'] <= missing_amount: + quote_amount += order['quote']['amount'] + base_amount += order['base']['amount'] + missing_amount -= order['base']['amount'] + else: + base_amount += missing_amount + quote_amount += missing_amount / order['price'] + break + elif not base: + # QUOTE amount was given + if order['quote']['amount'] <= missing_amount: + quote_amount += order['quote']['amount'] + base_amount += order['base']['amount'] + missing_amount -= order['quote']['amount'] + else: + base_amount += missing_amount * order['price'] + quote_amount += missing_amount + break + + # Prevent division by zero + if not quote_amount: + return 0.0 + + return base_amount / quote_amount + + def get_market_sell_price(self, quote_amount=0, base_amount=0, exclude_own_orders=True): + """ Returns the BASE/QUOTE price for which [quote_amount] worth of QUOTE could be bought, + enhanced with moving average or weighted moving average. + + [quote/base]_amount = 0 means lowest regardless of size + + :param float | quote_amount: + :param float | base_amount: + :param bool | exclude_own_orders: Exclude own orders when calculating a price + :return: + """ + market_sell_orders = [] + + # Exclude own orders from orderbook if needed + if exclude_own_orders: + market_sell_orders = self.get_market_sell_orders(depth=self.fetch_depth) + own_sell_orders_ids = [o['id'] for o in self.get_own_sell_orders()] + market_sell_orders = [o for o in market_sell_orders if o['id'] not in own_sell_orders_ids] + + # In case amount is not given, return price of the lowest sell order on the market + if quote_amount == 0 and base_amount == 0: + if exclude_own_orders: + if market_sell_orders: + return float(market_sell_orders[0]['price']) + else: + return '0.0' + else: + return float(self.ticker().get('lowestAsk')) + + asset_amount = quote_amount + + """ Since the purpose is never get both quote and base amounts, favor quote amount if both given because + this function is looking for sell price. + """ + if quote_amount > base_amount: + quote = True + else: + asset_amount = base_amount + quote = False + + if not market_sell_orders: + market_sell_orders = self.get_market_sell_orders(depth=self.fetch_depth) + market_fee = self.market['quote'].market_fee_percent + + target_amount = asset_amount * (1 + market_fee) + + quote_amount = 0 + base_amount = 0 + missing_amount = target_amount + + for order in market_sell_orders: + if quote: + # QUOTE amount was given + if order['quote']['amount'] <= missing_amount: + quote_amount += order['quote']['amount'] + base_amount += order['base']['amount'] + missing_amount -= order['quote']['amount'] + else: + base_amount += missing_amount * order['price'] + quote_amount += missing_amount + break + elif not quote: + # BASE amount was given + if order['base']['amount'] <= missing_amount: + quote_amount += order['quote']['amount'] + base_amount += order['base']['amount'] + missing_amount -= order['base']['amount'] + else: + base_amount += missing_amount + quote_amount += missing_amount / order['price'] + break + + # Prevent division by zero + if not quote_amount: + return 0.0 + + return base_amount / quote_amount def get_external_market_center_price(self, external_price_source): """ Get center price from an external market for current market pair From 52db345189422027af9cc96312442d28fddf9b77 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 18 Apr 2019 13:00:26 -0700 Subject: [PATCH 1352/1846] rename files with bitshares instead of bts --- dexbot/orderengines/{bts_engine.py => bitshares_engine.py} | 0 .../orderengines/{bts_engine_test.py => bitshares_engine_test.py} | 0 dexbot/pricefeeds/{bts_feed.py => bitshares_feed.py} | 0 dexbot/pricefeeds/{bts_feed_test.py => bitshares_feed_test.py} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename dexbot/orderengines/{bts_engine.py => bitshares_engine.py} (100%) rename dexbot/orderengines/{bts_engine_test.py => bitshares_engine_test.py} (100%) rename dexbot/pricefeeds/{bts_feed.py => bitshares_feed.py} (100%) rename dexbot/pricefeeds/{bts_feed_test.py => bitshares_feed_test.py} (100%) diff --git a/dexbot/orderengines/bts_engine.py b/dexbot/orderengines/bitshares_engine.py similarity index 100% rename from dexbot/orderengines/bts_engine.py rename to dexbot/orderengines/bitshares_engine.py diff --git a/dexbot/orderengines/bts_engine_test.py b/dexbot/orderengines/bitshares_engine_test.py similarity index 100% rename from dexbot/orderengines/bts_engine_test.py rename to dexbot/orderengines/bitshares_engine_test.py diff --git a/dexbot/pricefeeds/bts_feed.py b/dexbot/pricefeeds/bitshares_feed.py similarity index 100% rename from dexbot/pricefeeds/bts_feed.py rename to dexbot/pricefeeds/bitshares_feed.py diff --git a/dexbot/pricefeeds/bts_feed_test.py b/dexbot/pricefeeds/bitshares_feed_test.py similarity index 100% rename from dexbot/pricefeeds/bts_feed_test.py rename to dexbot/pricefeeds/bitshares_feed_test.py From 2ae26cc4097204181a358dcfd7a5f6ebe49cc73a Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 18 Apr 2019 13:34:49 -0700 Subject: [PATCH 1353/1846] fix import statements after renmaing bts to bitshares --- 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 3efe484f5..810bba7c8 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -4,8 +4,8 @@ from .base import StrategyBase from .config_parts.relative_config import RelativeConfig from dexbot.strategies.external_feeds.price_feed import PriceFeed -from dexbot.orderengines.bts_engine import BitsharesOrderEngine -from dexbot.pricefeeds.bts_feed import BitsharesPriceFeed +from dexbot.orderengines.bitshares_engine import BitsharesOrderEngine +from dexbot.pricefeeds.bitshares_feed import BitsharesPriceFeed #class Strategy(StrategyBase): From c33090afd8f285094d340d3e40c8a6a1779d7477 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 18 Apr 2019 14:18:18 -0700 Subject: [PATCH 1354/1846] fix import copy in engine, modify class inheritance in staggered_orders, test --- dexbot/orderengines/bitshares_engine.py | 1 + dexbot/pricefeeds/bitshares_feed.py | 1 - dexbot/strategies/staggered_orders.py | 9 ++++++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index 6517215e3..0183cf5e4 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -1,3 +1,4 @@ +import copy import datetime import logging import math diff --git a/dexbot/pricefeeds/bitshares_feed.py b/dexbot/pricefeeds/bitshares_feed.py index c3ce99cab..daa506932 100644 --- a/dexbot/pricefeeds/bitshares_feed.py +++ b/dexbot/pricefeeds/bitshares_feed.py @@ -1,4 +1,3 @@ -import copy import logging import math diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 74e9402e2..5fc8714f4 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -6,11 +6,14 @@ from bitshares.dex import Dex from bitshares.amount import Amount -from .base import StrategyBase +#from .base import StrategyBase from .config_parts.staggered_config import StaggeredConfig +from dexbot.orderengines.bitshares_engine import BitsharesOrderEngine +from dexbot.pricefeeds.bitshares_feed import BitsharesPriceFeed - -class Strategy(StrategyBase): +#class Strategy(StrategyBase): +# this inheritance is temporary before we finish refactoring strategybase +class Strategy(BitsharesOrderEngine, BitsharesPriceFeed): """ Staggered Orders strategy """ @classmethod From 0bfc4ed1a76b00f616b1dba3291818b6936af4ec Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 18 Apr 2019 17:11:35 -0700 Subject: [PATCH 1355/1846] moved order methods out of base and into bitshares_orderengine, updated RO and SO to test --- dexbot/orderengines/bitshares_engine.py | 277 +----- dexbot/strategies/base.py | 1051 +---------------------- dexbot/strategies/relative_orders.py | 6 +- dexbot/strategies/staggered_orders.py | 6 +- 4 files changed, 41 insertions(+), 1299 deletions(-) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index 0183cf5e4..807c9fac0 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -1,20 +1,18 @@ -import copy import datetime +import copy import logging -import math import time +#import math from dexbot.config import Config from dexbot.storage import Storage from dexbot.helper import truncate -from dexbot.qt_queue.idle_queue import idle_add -from dexbot.strategies.config_parts.base_config import BaseConfig -from events import Events import bitshares.exceptions import bitsharesapi import bitsharesapi.exceptions -from bitshares.account import Account + +#from bitshares.account import Account from bitshares.amount import Amount, Asset from bitshares.dex import Dex from bitshares.instance import shared_bitshares_instance @@ -22,13 +20,16 @@ from bitshares.price import FilledOrder, Order, UpdateCallOrder from bitshares.utils import formatTime +from events import Events + + # Number of maximum retries used to retry action before failing MAX_TRIES = 3 -## this is TEMPORARY CLASS before we move the Orders out + class BitsharesOrderEngine(Storage, Events): """ - All prices are passed and returned as BASE/QUOTE. + All prices are passed and returned as BASE/QUOTE. (In the BREAD:USD market that would be USD/BREAD, 2.5 USD / 1 BREAD). - Buy orders reserve BASE - Sell orders reserve QUOTE @@ -38,53 +39,16 @@ class BitsharesOrderEngine(Storage, Events): * ``Events`` :The websocket endpoint of BitShares has notifications that are subscribed to and dispatched by dexbot. This uses python's native Events - Available attributes: - * ``worker.bitshares``: instance of ´`bitshares.BitShares()`` - * ``worker.account``: The Account object of this worker - * ``worker.market``: The market used by this worker - * ``worker.orders``: List of open orders of the worker's account in the worker's market - * ``worker.balance``: List of assets and amounts available in the worker's account - * ``worker.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, Worker inherits :class:`dexbot.storage.Storage` - which allows to permanently store data in a sqlite database - using: """ - # put configure methods in strategies - @classmethod - def configure(cls, return_base_config=True): - return BaseConfig.configure(return_base_config) - - @classmethod - def configure_details(cls, include_default_tabs=True): - return BaseConfig.configure_details(include_default_tabs) - - """ - Events are bitshares websocket specific - """ - __events__ = [ - 'onAccount', - 'onMarketUpdate', - 'onOrderMatched', - 'onOrderPlaced', - 'ontick', - 'onUpdateCallOrder', - 'error_onAccount', - 'error_onMarketUpdate', - 'error_ontick', - ] def __init__(self, name, config=None, - onAccount=None, - onOrderMatched=None, - onOrderPlaced=None, - onMarketUpdate=None, - onUpdateCallOrder=None, - ontick=None, + account=None, + market=None, + fee_asset_symbol=None, bitshares_instance=None, + bitshares_bundle=None, *args, **kwargs): @@ -100,34 +64,18 @@ def __init__(self, # Events Events.__init__(self) - if ontick: - self.ontick += ontick - if onMarketUpdate: - self.onMarketUpdate += onMarketUpdate - if onAccount: - self.onAccount += onAccount - if onOrderMatched: - self.onOrderMatched += onOrderMatched - if onOrderPlaced: - self.onOrderPlaced += onOrderPlaced - if onUpdateCallOrder: - self.onUpdateCallOrder += onUpdateCallOrder - # Redirect this event to also call order placed and order matched self.onMarketUpdate += self._callbackPlaceFillOrders if config: self.config = config else: - self.config = config = Config.get_worker_config_file(name) - - # Get worker's parameters from the config - self.worker = config["workers"][name] + self.config = Config.get_worker_config_file(name) # Get Bitshares account and market for this worker - self._account = Account(self.worker["account"], full=True, bitshares_instance=self.bitshares) + self._account = account - self._market = Market(config["workers"][name]["market"], bitshares_instance=self.bitshares) + self._market = market # Recheck flag - Tell the strategy to check for updated orders self.recheck_orders = False @@ -136,7 +84,7 @@ def __init__(self, self.fetch_depth = 8 # Set fee asset - fee_asset_symbol = self.worker.get('fee_asset') + fee_asset_symbol = fee_asset_symbol if fee_asset_symbol: try: @@ -154,7 +102,7 @@ def __init__(self, self.ticker = self.market.ticker # Settings for bitshares instance - self.bitshares.bundle = bool(self.worker.get("bundle", False)) + self.bitshares.bundle = bitshares_bundle # Disabled flag - this flag can be flipped to True by a worker and will be reset to False after reset only self.disabled = False @@ -165,17 +113,6 @@ def __init__(self, # buy/sell actions will return order id by default self.returnOrderId = 'head' - # 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 - } - ) - self.orders_log = logging.LoggerAdapter( logging.getLogger('dexbot.orders_log'), {} ) @@ -587,29 +524,6 @@ def is_sell_order(self, order): else: return False - def pause(self): - """ Pause the worker - - Note: By default pause cancels orders, but this can be overridden by strategy - """ - # Cancel all orders from the market - self.cancel_all_orders() - - # Removes worker's orders from local database - self.clear_orders() - - def clear_all_worker_data(self): - """ Clear all the worker data from the database and cancel all orders - """ - # Removes worker's orders from local database - self.clear_orders() - - # Cancel all orders from the market - self.cancel_all_orders() - - # Finally clear all worker data from the database - self.clear() - def place_market_buy_order(self, amount, price, return_none=False, *args, **kwargs): """ Places a buy order in the market @@ -770,103 +684,6 @@ def retry_action(self, action, *args, **kwargs): else: raise - def store_profit_estimation_data(self): # todo: move this method into strategy - """ Save total quote, total base, center_price, and datetime in to the database - """ - assets = self.count_asset() - account = self.config['workers'][self.worker_name].get('account') - base_amount = assets['base'] - base_symbol = self.market['base'].get('symbol') - quote_amount = assets['quote'] - quote_symbol = self.market['quote'].get('symbol') - center_price = self.get_market_center_price() - timestamp = time.time() - - self.store_balance_entry(account, self.worker_name, base_amount, base_symbol, - quote_amount, quote_symbol, center_price, timestamp) - - def get_profit_estimation_data(self, seconds): - """ Get balance history closest to the given time - - :returns The data as dict from the first timestamp going backwards from seconds argument - """ - return self.get_balance_history(self.config['workers'][self.worker_name].get('account'), - self.worker_name, seconds) - - def calc_profit(self): # todo: move this method into strategy - """ Calculate relative profit for the current worker - """ - profit = 0 - time_range = 60 * 60 * 24 * 7 # 7 days - current_time = time.time() - timestamp = current_time - time_range - - # Fetch the balance from history - old_data = self.get_balance_history(self.config['workers'][self.worker_name].get('account'), self.worker_name, - timestamp, self.base_asset, self.quote_asset) - if old_data: - earlier_base = old_data.base_total - earlier_quote = old_data.quote_total - old_center_price = old_data.center_price - center_price = self.get_market_center_price() - - if not (old_center_price or center_price): - return profit - - # Calculate max theoretical balances based on starting price - old_max_quantity_base = earlier_base + earlier_quote * old_center_price - old_max_quantity_quote = earlier_quote + earlier_base / old_center_price - - if not (old_max_quantity_base or old_max_quantity_quote): - return profit - - # Current balances - balance = self.count_asset() - base_balance = balance['base'] - quote_balance = balance['quote'] - - # Calculate max theoretical current balances - max_quantity_base = base_balance + quote_balance * center_price - max_quantity_quote = quote_balance + base_balance / center_price - - base_roi = max_quantity_base / old_max_quantity_base - quote_roi = max_quantity_quote / old_max_quantity_quote - profit = round(math.sqrt(base_roi * quote_roi) - 1, 4) - - return profit - - def write_order_log(self, worker_name, order): - """ Write order log to csv file - - :param string | worker_name: Name of the worker - :param object | order: Order that was fulfilled - """ - operation_type = 'TRADE' - - if order['base']['symbol'] == self.market['base']['symbol']: - base_symbol = order['base']['symbol'] - base_amount = -order['base']['amount'] - quote_symbol = order['quote']['symbol'] - quote_amount = order['quote']['amount'] - else: - base_symbol = order['quote']['symbol'] - base_amount = order['quote']['amount'] - quote_symbol = order['base']['symbol'] - quote_amount = -order['base']['amount'] - - message = '{};{};{};{};{};{};{};{}'.format( - worker_name, - order['id'], - operation_type, - base_symbol, - base_amount, - quote_symbol, - quote_amount, - datetime.datetime.now().isoformat() - ) - - self.orders_log.info(message) - @property def account(self): """ Return the full account as :class:`bitshares.account.Account` object! @@ -884,14 +701,6 @@ def balances(self): """ return self._account.balances - @property - def base_asset(self): - return self.worker['market'].split('/')[1] - - @property - def quote_asset(self): - return self.worker['market'].split('/')[0] - @property def all_own_orders(self, refresh=True): """ Return the worker's open orders in all markets @@ -909,24 +718,7 @@ def all_own_orders(self, refresh=True): return orders - @property - def get_own_orders(self): - """ Return the account's open orders in the current market - - :return: List of Order objects - """ - orders = [] - - # Refresh account data - self.account.refresh() - - for order in self.account.openorders: - if self.worker["market"] == order.market and self.account.openorders: - orders.append(order) - - return orders - - @property #todo: duplicate property, also in price feed + @property #todo: duplicate property, also in price feed, collisions possible? def market(self): """ Return the market object as :class:`bitshares.market.Market` """ @@ -1005,34 +797,3 @@ def get_order(order_id, return_none=True): return None return order - @staticmethod - def purge_all_local_worker_data(worker_name): - """ Removes worker's data and orders from local sqlite database - - :param worker_name: Name of the worker to be removed - """ - Storage.clear_worker_data(worker_name) - - # GUI updaters - def update_gui_slider(self): - ticker = self.market.ticker() - latest_price = ticker.get('latest', {}).get('price', None) - if not latest_price: - return - - total_balance = self.count_asset() - 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 - - def update_gui_profit(self): - profit = self.calc_profit() - - # Add to idle queue - idle_add(self.view.set_worker_profit, self.worker_name, float(profit)) - self['profit'] = profit diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 2e84a02f3..cda42f2eb 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1,32 +1,42 @@ +''' import datetime import copy +''' import logging import math import time +#from dexbot.helper import truncate from dexbot.config import Config from dexbot.storage import Storage -from dexbot.helper import truncate from dexbot.qt_queue.idle_queue import idle_add from .config_parts.base_config import BaseConfig -from events import Events +from dexbot.orderengines.bitshares_engine import BitsharesOrderEngine +#from dexbot.pricefeeds.bitshares_feed import BitsharesPriceFeed + import bitshares.exceptions -import bitsharesapi -import bitsharesapi.exceptions from bitshares.account import Account -from bitshares.amount import Amount, Asset -from bitshares.dex import Dex +from bitshares.amount import Asset from bitshares.instance import shared_bitshares_instance from bitshares.market import Market + +''' +from bitshares.amount import Amount +import bitsharesapi +import bitsharesapi.exceptions +from bitshares.dex import Dex from bitshares.price import FilledOrder, Order, UpdateCallOrder from bitshares.utils import formatTime +''' + +from events import Events # Number of maximum retries used to retry action before failing MAX_TRIES = 3 -class StrategyBase(Storage, Events): +class StrategyBase(BitsharesOrderEngine): """ A strategy based on this class is intended to work in one market. This class contains most common methods needed by the strategy. @@ -98,7 +108,7 @@ def __init__(self, self.bitshares = bitshares_instance or shared_bitshares_instance() # Dex instance used to get different fees for the market - self.dex = Dex(self.bitshares) +# self.dex = Dex(self.bitshares) # Storage Storage.__init__(self, name) @@ -182,731 +192,6 @@ def __init__(self, } ) - self.orders_log = logging.LoggerAdapter( - logging.getLogger('dexbot.orders_log'), {} - ) - - def _callbackPlaceFillOrders(self, d): - """ This method distinguishes notifications caused by Matched orders from those caused by placed orders - """ - if isinstance(d, FilledOrder): - self.onOrderMatched(d) - elif isinstance(d, Order): - self.onOrderPlaced(d) - elif isinstance(d, UpdateCallOrder): - self.onUpdateCallOrder(d) - else: - pass - - def _cancel_orders(self, orders): - try: - self.retry_action( - self.bitshares.cancel, - orders, account=self.account, fee_asset=self.fee_asset['id'] - ) - except bitsharesapi.exceptions.UnhandledRPCError as exception: - if str(exception).startswith('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: - self.log.exception("Unable to cancel order") - return False - except bitshares.exceptions.MissingKeyError: - self.log.exception('Unable to cancel order(s), private key missing.') - return False - - return True - - def account_total_value(self, return_asset): - """ Returns the total value of the account in given asset - - :param string | return_asset: Balance is returned as this asset - :return: float: Value of the account in one asset - """ - total_value = 0 - - # Total balance calculation - for balance in self.balances: - if balance['symbol'] != return_asset: - # Convert to asset if different - total_value += self.convert_asset(balance['amount'], balance['symbol'], return_asset) - else: - total_value += balance['amount'] - - # Orders balance calculation - for order in self.all_own_orders: - updated_order = self.get_updated_order(order['id']) - - if not order: - continue - if updated_order['base']['symbol'] == return_asset: - total_value += updated_order['base']['amount'] - else: - total_value += self.convert_asset( - updated_order['base']['amount'], - updated_order['base']['symbol'], - return_asset - ) - - return total_value - - def balance(self, asset, fee_reservation=0): - """ Return the balance of your worker's account in a specific asset. - - :param string | asset: In what asset the balance is wanted to be returned - :param float | fee_reservation: How much is saved in reserve for the fees - :return: Balance of specific asset - """ - balance = self._account.balance(asset) - - if fee_reservation > 0: - balance['amount'] = balance['amount'] - fee_reservation - - return balance - - 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 calculate_worker_value(self, unit_of_measure): - """ Returns the combined value of allocated and available BASE and QUOTE. Total value is - measured in "unit_of_measure", which is either BASE or QUOTE symbol. - - :param string | unit_of_measure: Asset symbol - :return: Value of the worker as float - """ - base_total = 0 - quote_total = 0 - - # Calculate total balances - balances = self.balances - for balance in balances: - if balance['symbol'] == self.base_asset: - base_total += balance['amount'] - elif balance['symbol'] == self.quote_asset: - quote_total += balance['amount'] - - # Calculate value of the orders in unit of measure - orders = self.get_own_orders - for order in orders: - if order['base']['symbol'] == self.quote_asset: - # Pick sell orders order's BASE amount, which is same as worker's QUOTE, to worker's BASE - quote_total += order['base']['amount'] - else: - base_total += order['base']['amount'] - - # Finally convert asset to another and return the sum - if unit_of_measure == self.base_asset: - quote_total = self.convert_asset(quote_total, self.quote_asset, unit_of_measure) - elif unit_of_measure == self.quote_asset: - base_total = self.convert_asset(base_total, self.base_asset, unit_of_measure) - - # Fixme: Make sure that decimal precision is correct. - return base_total + quote_total - - def cancel_all_orders(self): - """ Cancel all orders of the worker's account - """ - self.log.info('Canceling all orders') - - if self.all_own_orders: - self.cancel_orders(self.all_own_orders) - - self.log.info("Orders canceled") - - def cancel_orders(self, orders, batch_only=False): - """ Cancel specific order(s) - - :param list | orders: List of orders to cancel - :param bool | batch_only: Try cancel orders only in batch mode without one-by-one fallback - :return: - """ - if not isinstance(orders, (list, set, tuple)): - orders = [orders] - - orders = [order['id'] for order in orders if 'id' in order] - - success = self._cancel_orders(orders) - if not success and batch_only: - return False - if not success and len(orders) > 1 and not batch_only: - # One of the order cancels failed, cancel the orders one by one - for order in orders: - success = self._cancel_orders(order) - if not success: - return False - return success - - def count_asset(self, order_ids=None, return_asset=False): - """ Returns the combined amount of the given order ids and the account balance - The amounts are returned in quote and base assets of the market - - :param list | order_ids: list of order ids to be added to the balance - :param bool | return_asset: true if returned values should be Amount instances - :return: dict with keys quote and base - Todo: When would we want the sum of a subset of orders? Why order_ids? Maybe just specify asset? - """ - quote = 0 - base = 0 - quote_asset = self.market['quote']['id'] - base_asset = self.market['base']['id'] - - # Total balance calculation - for balance in self.balances: - if balance.asset['id'] == quote_asset: - quote += balance['amount'] - elif balance.asset['id'] == base_asset: - base += balance['amount'] - - if order_ids is None: - # Get all orders from Blockchain - order_ids = [order['id'] for order in self.get_own_orders] - if order_ids: - orders_balance = self.get_allocated_assets(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 get_allocated_assets(self, order_ids=None, return_asset=False): - """ Returns the amount of QUOTE and BASE allocated in orders, and that do not show up in available balance - - :param list | order_ids: - :param bool | return_asset: - :return: Dictionary of QUOTE and BASE amounts - """ - 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'] - - # Return as Amount objects instead of only float values - if return_asset: - quote = Amount(quote, quote_asset) - base = Amount(base, base_asset) - - return {'quote': quote, 'base': base} - - def get_market_buy_orders(self, depth=10): - """ Fetches most recent data and returns list of buy orders. - - :param int | depth: Amount of buy orders returned, Default=10 - :return: List of market sell orders - """ - orders = self.get_market_orders(depth=depth) - buy_orders = self.filter_buy_orders(orders) - return buy_orders - - def get_market_sell_orders(self, depth=10): - """ Fetches most recent data and returns list of sell orders. - - :param int | depth: Amount of sell orders returned, Default=10 - :return: List of market sell orders - """ - orders = self.get_market_orders(depth=depth) - sell_orders = self.filter_sell_orders(orders) - return sell_orders - - def get_highest_market_buy_order(self, orders=None): - """ Returns the highest buy order that is not own, regardless of order size. - - :param list | orders: Optional list of orders, if none given fetch newest from market - :return: Highest market buy order or None - """ - if not orders: - orders = self.get_market_buy_orders(1) - - try: - order = orders[0] - except IndexError: - self.log.info('Market has no buy orders.') - return None - - return order - - def get_highest_own_buy_order(self, orders=None): - """ Returns highest own buy order. - - :param list | orders: - :return: Highest own buy order by price at the market or None - """ - if not orders: - orders = self.get_own_buy_orders() - - try: - return orders[0] - except IndexError: - return None - - def get_lowest_market_sell_order(self, orders=None): - """ Returns the lowest sell order that is not own, regardless of order size. - - :param list | orders: Optional list of orders, if none given fetch newest from market - :return: Lowest market sell order or None - """ - if not orders: - orders = self.get_market_sell_orders(1) - - try: - order = orders[0] - except IndexError: - self.log.info('Market has no sell orders.') - return None - - return order - - def get_lowest_own_sell_order(self, orders=None): - """ Returns lowest own sell order. - - :param list | orders: - :return: Lowest own sell order by price at the market - """ - if not orders: - orders = self.get_own_sell_orders() - - try: - return orders[0] - except IndexError: - return None - - def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors=False): - """ Returns the center price of market including own orders. - - :param float | base_amount: - :param float | quote_amount: - :param bool | suppress_errors: - :return: Market center price as float - """ - center_price = None - buy_price = self.get_market_buy_price(quote_amount=quote_amount, - base_amount=base_amount, exclude_own_orders=False) - sell_price = self.get_market_sell_price(quote_amount=quote_amount, - base_amount=base_amount, exclude_own_orders=False) - if buy_price is None or buy_price == 0.0: - if not suppress_errors: - self.log.critical("Cannot estimate center price, there is no highest bid.") - self.disabled = True - return None - - if sell_price is None or sell_price == 0.0: - if not suppress_errors: - self.log.critical("Cannot estimate center price, there is no lowest ask.") - self.disabled = True - return None - # Calculate and return market center price. make sure buy_price has value - if buy_price: - center_price = buy_price * math.sqrt(sell_price / buy_price) - self.log.debug('Center price in get_market_center_price: {:.8f} '.format(center_price)) - return center_price - - def get_market_buy_price(self, quote_amount=0, base_amount=0, exclude_own_orders=True): - """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with - moving average or weighted moving average - - :param float | quote_amount: - :param float | base_amount: - :param bool | exclude_own_orders: Exclude own orders when calculating a price - :return: price as float - """ - market_buy_orders = [] - - # Exclude own orders from orderbook if needed - if exclude_own_orders: - market_buy_orders = self.get_market_buy_orders(depth=self.fetch_depth) - own_buy_orders_ids = [o['id'] for o in self.get_own_buy_orders()] - market_buy_orders = [o for o in market_buy_orders if o['id'] not in own_buy_orders_ids] - - # In case amount is not given, return price of the highest buy order on the market - if quote_amount == 0 and base_amount == 0: - if exclude_own_orders: - if market_buy_orders: - return float(market_buy_orders[0]['price']) - else: - return '0.0' - else: - return float(self.ticker().get('highestBid')) - - # Like get_market_sell_price(), but defaulting to base_amount if both base and quote are specified. - asset_amount = base_amount - - """ Since the purpose is never get both quote and base amounts, favor base amount if both given because - this function is looking for buy price. - """ - if base_amount > quote_amount: - base = True - else: - asset_amount = quote_amount - base = False - - if not market_buy_orders: - market_buy_orders = self.get_market_buy_orders(depth=self.fetch_depth) - market_fee = self.market['base'].market_fee_percent - - target_amount = asset_amount * (1 + market_fee) - - quote_amount = 0 - base_amount = 0 - missing_amount = target_amount - - for order in market_buy_orders: - if base: - # BASE amount was given - if order['base']['amount'] <= missing_amount: - quote_amount += order['quote']['amount'] - base_amount += order['base']['amount'] - missing_amount -= order['base']['amount'] - else: - base_amount += missing_amount - quote_amount += missing_amount / order['price'] - break - elif not base: - # QUOTE amount was given - if order['quote']['amount'] <= missing_amount: - quote_amount += order['quote']['amount'] - base_amount += order['base']['amount'] - missing_amount -= order['quote']['amount'] - else: - base_amount += missing_amount * order['price'] - quote_amount += missing_amount - break - - # Prevent division by zero - if not quote_amount: - return 0.0 - - return base_amount / quote_amount - - def get_market_orders(self, depth=1, updated=True): - """ Returns orders from the current market. Orders are sorted by price. - - get_market_orders() call does not have any depth limit. - - :param int | depth: Amount of orders per side will be fetched, default=1 - :param bool | updated: Return updated orders. "Updated" means partially filled orders will represent - remainders and not just initial amounts - :return: Returns a list of orders or None - """ - orders = self.bitshares.rpc.get_limit_orders(self.market['base']['id'], self.market['quote']['id'], depth) - if updated: - orders = [self.get_updated_limit_order(o) for o in orders] - orders = [Order(o, bitshares_instance=self.bitshares) for o in orders] - return orders - - def get_orderbook_orders(self, depth=1): - """ Returns orders from the current market split in bids and asks. Orders are sorted by price. - - Market.orderbook() call has hard-limit of depth=50 enforced by bitshares node. - - bids = buy orders - asks = sell orders - - :param int | depth: Amount of orders per side will be fetched, default=1 - :return: Returns a dictionary of orders or None - """ - return self.market.orderbook(depth) - - def get_market_sell_price(self, quote_amount=0, base_amount=0, exclude_own_orders=True): - """ Returns the BASE/QUOTE price for which [quote_amount] worth of QUOTE could be bought, - enhanced with moving average or weighted moving average. - - [quote/base]_amount = 0 means lowest regardless of size - - :param float | quote_amount: - :param float | base_amount: - :param bool | exclude_own_orders: Exclude own orders when calculating a price - :return: - """ - market_sell_orders = [] - - # Exclude own orders from orderbook if needed - if exclude_own_orders: - market_sell_orders = self.get_market_sell_orders(depth=self.fetch_depth) - own_sell_orders_ids = [o['id'] for o in self.get_own_sell_orders()] - market_sell_orders = [o for o in market_sell_orders if o['id'] not in own_sell_orders_ids] - - # In case amount is not given, return price of the lowest sell order on the market - if quote_amount == 0 and base_amount == 0: - if exclude_own_orders: - if market_sell_orders: - return float(market_sell_orders[0]['price']) - else: - return '0.0' - else: - return float(self.ticker().get('lowestAsk')) - - asset_amount = quote_amount - - """ Since the purpose is never get both quote and base amounts, favor quote amount if both given because - this function is looking for sell price. - """ - if quote_amount > base_amount: - quote = True - else: - asset_amount = base_amount - quote = False - - if not market_sell_orders: - market_sell_orders = self.get_market_sell_orders(depth=self.fetch_depth) - market_fee = self.market['quote'].market_fee_percent - - target_amount = asset_amount * (1 + market_fee) - - quote_amount = 0 - base_amount = 0 - missing_amount = target_amount - - for order in market_sell_orders: - if quote: - # QUOTE amount was given - if order['quote']['amount'] <= missing_amount: - quote_amount += order['quote']['amount'] - base_amount += order['base']['amount'] - missing_amount -= order['quote']['amount'] - else: - base_amount += missing_amount * order['price'] - quote_amount += missing_amount - break - elif not quote: - # BASE amount was given - if order['base']['amount'] <= missing_amount: - quote_amount += order['quote']['amount'] - base_amount += order['base']['amount'] - missing_amount -= order['base']['amount'] - else: - base_amount += missing_amount - quote_amount += missing_amount / order['price'] - break - - # Prevent division by zero - if not quote_amount: - return 0.0 - - return base_amount / quote_amount - - def get_market_spread(self, quote_amount=0, base_amount=0): - """ Returns the market spread %, including own orders, from specified depth. - - :param float | quote_amount: - :param float | base_amount: - :return: Market spread as float or None - """ - ask = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount, exclude_own_orders=False) - bid = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount, exclude_own_orders=False) - - # Calculate market spread - if ask == 0 or bid == 0: - return None - - return ask / bid - 1 - - def get_order_cancellation_fee(self, fee_asset): - """ Returns the order cancellation fee in the specified asset. - - :param string | fee_asset: Asset in which the fee is wanted - :return: Cancellation fee as fee asset - """ - # Get fee - fees = self.dex.returnFees() - limit_order_cancel = fees['limit_order_cancel'] - return self.convert_fee(limit_order_cancel['fee'], fee_asset) - - def get_order_creation_fee(self, fee_asset): - """ Returns the cost of creating an order in the asset specified - - :param fee_asset: QUOTE, BASE, BTS, or any other - :return: - """ - # Get fee - fees = self.dex.returnFees() - limit_order_create = fees['limit_order_create'] - return self.convert_fee(limit_order_create['fee'], fee_asset) - - def filter_buy_orders(self, orders, sort=None): - """ Return own buy orders from list of orders. Can be used to pick buy orders from a list - that is not up to date with the blockchain data. - - :param list | orders: List of orders - :param string | sort: DESC or ASC will sort the orders accordingly, default None - :return list | buy_orders: List of buy orders only - """ - buy_orders = [] - - # Filter buy orders - for order in orders: - # Check if the order is buy order, by comparing asset symbol of the order and the market - if order['base']['symbol'] == self.market['base']['symbol']: - buy_orders.append(order) - - if sort: - buy_orders = self.sort_orders_by_price(buy_orders, sort) - - return buy_orders - - def filter_sell_orders(self, orders, sort=None, invert=True): - """ Return sell orders from list of orders. Can be used to pick sell orders from a list - that is not up to date with the blockchain data. - - :param list | orders: List of orders - :param string | sort: DESC or ASC will sort the orders accordingly, default None - :param bool | invert: return inverted orders or not - :return list | sell_orders: List of sell orders only - """ - sell_orders = [] - - # Filter sell orders - for order in orders: - # Check if the order is buy order, by comparing asset symbol of the order and the market - if order['base']['symbol'] != self.market['base']['symbol']: - # Invert order before appending to the list, this gives easier comparison in strategy logic - if invert: - order = order.invert() - sell_orders.append(order) - - if sort: - sell_orders = self.sort_orders_by_price(sell_orders, sort) - - return sell_orders - - def get_own_buy_orders(self, orders=None): - """ Get own buy orders from current market, or from a set of orders passed for this function. - - :return: List of buy orders - """ - if not orders: - # List of orders was not given so fetch everything from the market - orders = self.get_own_orders - - return self.filter_buy_orders(orders) - - def get_own_sell_orders(self, orders=None): - """ Get own sell orders from current market - - :return: List of sell orders - """ - if not orders: - # List of orders was not given so fetch everything from the market - orders = self.get_own_orders - - return self.filter_sell_orders(orders) - - def get_own_spread(self): - """ Returns the difference between own closest opposite orders. - - :return: float or None: Own spread - """ - try: - # Try fetching own orders - highest_own_buy_price = self.get_highest_own_buy_order().get('price') - lowest_own_sell_price = self.get_lowest_own_sell_order().get('price') - except AttributeError: - return None - - # Calculate actual spread - actual_spread = lowest_own_sell_price / highest_own_buy_price - 1 - return actual_spread - - def get_updated_order(self, order_id): - """ Tries to get the updated order from the API. Returns None if the order doesn't exist - - :param str|dict order_id: blockchain Order object or id of the order - """ - if isinstance(order_id, dict): - order_id = order_id['id'] - - # At first, try to look up own orders. This prevents RPC calls whether requested order is own order - order = None - for limit_order in self.account['limit_orders']: - if order_id == limit_order['id']: - order = limit_order - break - else: - # We are using direct rpc call here because passing an Order object to self.get_updated_limit_order() give - # us weird error "Object of type 'BitShares' is not JSON serializable" - order = self.bitshares.rpc.get_objects([order_id])[0] - - # Do not try to continue whether there is no order in the blockchain - if not order: - return None - - updated_order = self.get_updated_limit_order(order) - return Order(updated_order, bitshares_instance=self.bitshares) - - def execute(self): - """ Execute a bundle of operations - - :return: dict: transaction - """ - self.bitshares.blocking = "head" - r = self.bitshares.txbuffer.broadcast() - self.bitshares.blocking = False - return r - - def is_buy_order(self, order): - """ Check whether an order is buy order - - :param dict | order: dict or Order object - :return bool - """ - # Check if the order is buy order, by comparing asset symbol of the order and the market - if order['base']['symbol'] == self.market['base']['symbol']: - return True - else: - return False - - def is_current_market(self, base_asset_id, quote_asset_id): - """ Returns True if given asset id's are of the current market - - :return: bool: True = Current market, False = Not current market - """ - if quote_asset_id == self.market['quote']['id']: - if base_asset_id == self.market['base']['id']: - return True - return False - - # Todo: Should we return true if market is opposite? - if quote_asset_id == self.market['base']['id']: - if base_asset_id == self.market['quote']['id']: - return True - return False - - return False - - def is_sell_order(self, order): - """ Check whether an order is sell order - - :param dict | order: dict or Order object - :return bool - """ - # Check if the order is sell order, by comparing asset symbol of the order and the market - if order['base']['symbol'] == self.market['quote']['symbol']: - return True - else: - return False - def pause(self): """ Pause the worker @@ -930,166 +215,6 @@ def clear_all_worker_data(self): # Finally clear all worker data from the database self.clear() - def place_market_buy_order(self, amount, price, return_none=False, *args, **kwargs): - """ Places a buy order in the market - - :param float | amount: Order amount in QUOTE - :param float | price: Order price in BASE - :param bool | return_none: - :param args: - :param kwargs: - :return: - """ - symbol = self.market['base']['symbol'] - precision = self.market['base']['precision'] - base_amount = truncate(price * amount, precision) - return_order_id = kwargs.pop('returnOrderId', self.returnOrderId) - - # Don't try to place an order of size 0 - if not base_amount: - self.log.critical('Trying to buy 0') - self.disabled = True - return None - - # Make sure we have enough balance for the order - if return_order_id and self.balance(self.market['base']) < base_amount: - self.log.critical("Insufficient buy balance, needed {} {}".format(base_amount, symbol)) - self.disabled = True - return None - - self.log.info('Placing a buy order with {:.{prec}f} {} @ {:.8f}' - .format(base_amount, symbol, price, prec=precision)) - - # Place the order - buy_transaction = self.retry_action( - self.market.buy, - price, - Amount(amount=amount, asset=self.market["quote"]), - account=self.account.name, - expiration=self.expiration, - returnOrderId=return_order_id, - fee_asset=self.fee_asset['id'], - *args, - **kwargs - ) - - self.log.debug('Placed buy order {}'.format(buy_transaction)) - if return_order_id: - buy_order = self.get_order(buy_transaction['orderid'], return_none=return_none) - if buy_order and 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 - else: - return True - - def place_market_sell_order(self, amount, price, return_none=False, invert=False, *args, **kwargs): - """ Places a sell order in the market - - :param float | amount: Order amount in QUOTE - :param float | price: Order price in BASE - :param bool | return_none: - :param bool | invert: True = return inverted sell order - :param args: - :param kwargs: - :return: - """ - symbol = self.market['quote']['symbol'] - precision = self.market['quote']['precision'] - quote_amount = truncate(amount, precision) - return_order_id = kwargs.pop('returnOrderId', self.returnOrderId) - - # Don't try to place an order of size 0 - if not quote_amount: - self.log.critical('Trying to sell 0') - self.disabled = True - return None - - # Make sure we have enough balance for the order - if return_order_id and self.balance(self.market['quote']) < quote_amount: - self.log.critical("Insufficient sell balance, needed {} {}".format(amount, symbol)) - self.disabled = True - return None - - self.log.info('Placing a sell order with {:.{prec}f} {} @ {:.8f}' - .format(quote_amount, symbol, price, prec=precision)) - - # Place the order - sell_transaction = self.retry_action( - self.market.sell, - price, - Amount(amount=amount, asset=self.market["quote"]), - account=self.account.name, - expiration=self.expiration, - returnOrderId=return_order_id, - fee_asset=self.fee_asset['id'], - *args, - **kwargs - ) - - self.log.debug('Placed sell order {}'.format(sell_transaction)) - if return_order_id: - sell_order = self.get_order(sell_transaction['orderid'], return_none=return_none) - if sell_order and 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) - self.recheck_orders = True - if sell_order and invert: - sell_order.invert() - return sell_order - else: - return True - - def retry_action(self, action, *args, **kwargs): - """ Perform an action, and if certain suspected-to-be-spurious grapheme 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 - - :param action: - :return: - """ - tries = 0 - while True: - try: - return action(*args, **kwargs) - except bitsharesapi.exceptions.UnhandledRPCError as exception: - if "Assert Exception: amount_to_sell.amount > 0" in str(exception): - if tries > MAX_TRIES: - raise - else: - tries += 1 - self.log.warning("Ignoring: '{}'".format(str(exception))) - self.bitshares.txbuffer.clear() - self.account.refresh() - time.sleep(2) - elif "now <= trx.expiration" in str(exception): # Usually loss of sync to blockchain - if tries > MAX_TRIES: - raise - else: - tries += 1 - self.log.warning("retrying on '{}'".format(str(exception))) - self.bitshares.txbuffer.clear() - time.sleep(6) # Wait at least a BitShares block - elif "trx.expiration <= now + chain_parameters.maximum_time_until_expiration" in str(exception): - if tries > MAX_TRIES: - info = self.bitshares.info() - raise Exception('Too much difference between node block time and trx expiration, please change ' - 'the node. Block time: {}, local time: {}' - .format(info['time'], formatTime(datetime.datetime.utcnow()))) - else: - tries += 1 - self.log.warning('Too much difference between node block time and trx expiration, switching ' - 'node') - self.bitshares.txbuffer.clear() - self.bitshares.rpc.next() - elif "Assert Exception: delta.amount > 0: Insufficient Balance" in str(exception): - self.log.critical('Insufficient balance of fee asset') - raise - else: - raise - def store_profit_estimation_data(self): """ Save total quote, total base, center_price, and datetime in to the database """ @@ -1155,38 +280,6 @@ def calc_profit(self): return profit - def write_order_log(self, worker_name, order): - """ Write order log to csv file - - :param string | worker_name: Name of the worker - :param object | order: Order that was fulfilled - """ - operation_type = 'TRADE' - - if order['base']['symbol'] == self.market['base']['symbol']: - base_symbol = order['base']['symbol'] - base_amount = -order['base']['amount'] - quote_symbol = order['quote']['symbol'] - quote_amount = order['quote']['amount'] - else: - base_symbol = order['quote']['symbol'] - base_amount = order['quote']['amount'] - quote_symbol = order['base']['symbol'] - quote_amount = -order['base']['amount'] - - message = '{};{};{};{};{};{};{};{}'.format( - worker_name, - order['id'], - operation_type, - base_symbol, - base_amount, - quote_symbol, - quote_amount, - datetime.datetime.now().isoformat() - ) - - self.orders_log.info(message) - @property def account(self): """ Return the full account as :class:`bitshares.account.Account` object! @@ -1212,23 +305,6 @@ def base_asset(self): def quote_asset(self): return self.worker['market'].split('/')[0] - @property - def all_own_orders(self, refresh=True): - """ Return the worker's open orders in all markets - - :param bool | refresh: Use most recent data - :return: List of Order objects - """ - # Refresh account data - if refresh: - self.account.refresh() - - orders = [] - for order in self.account.openorders: - orders.append(order) - - return orders - @property def get_own_orders(self): """ Return the account's open orders in the current market @@ -1252,79 +328,6 @@ def market(self): """ return self._market - @staticmethod - def convert_asset(from_value, from_asset, to_asset): - """ Converts asset to another based on the latest market value - - :param float | from_value: Amount of the input asset - :param string | from_asset: Symbol of the input asset - :param string | to_asset: Symbol of the output asset - :return: float Asset converted to another asset as float value - """ - market = Market('{}/{}'.format(from_asset, to_asset)) - ticker = market.ticker() - latest_price = ticker.get('latest', {}).get('price', None) - precision = market['base']['precision'] - - return truncate((from_value * latest_price), precision) - - def convert_fee(self, fee_amount, fee_asset): - """ Convert fee amount in BTS to fee in fee_asset - - :param float | fee_amount: fee amount paid in BTS - :param Asset | fee_asset: fee asset to pay fee in - :return: float | amount of fee_asset to pay fee - """ - if isinstance(fee_asset, str): - fee_asset = Asset(fee_asset) - - if fee_asset['id'] == '1.3.0': - # Fee asset is BTS, so no further calculations are needed - return fee_amount - else: - if not self.core_exchange_rate: - # Determine how many fee_asset is needed for core-exchange - temp_market = Market(base=fee_asset, quote=Asset('1.3.0')) - self.core_exchange_rate = temp_market.ticker()['core_exchange_rate'] - return fee_amount * self.core_exchange_rate['base']['amount'] - - @staticmethod - def get_order(order_id, return_none=True): - """ Get Order object with order_id - - :param str | dict order_id: blockchain object id of the order can be an order 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 - :return: Order object - """ - if not order_id: - return None - if 'id' in order_id: - order_id = order_id['id'] - try: - order = Order(order_id) - except Exception: - logging.getLogger(__name__).error('Got an exception getting order id {}'.format(order_id)) - raise - if return_none and order['deleted']: - return None - return order - - @staticmethod - def get_updated_limit_order(limit_order): - """ Returns a modified limit_order so that when passed to Order class, - will return an Order object with updated amount values - - :param limit_order: an item of Account['limit_orders'] or bitshares.rpc.get_limit_orders() - :return: Order - """ - order = copy.deepcopy(limit_order) - price = float(order['sell_price']['base']['amount']) / float(order['sell_price']['quote']['amount']) - base_amount = float(order['for_sale']) - quote_amount = base_amount / price - order['sell_price']['base']['amount'] = base_amount - order['sell_price']['quote']['amount'] = quote_amount - return order - @staticmethod def purge_all_local_worker_data(worker_name): """ Removes worker's data and orders from local sqlite database @@ -1333,24 +336,6 @@ def purge_all_local_worker_data(worker_name): """ Storage.clear_worker_data(worker_name) - @staticmethod - def sort_orders_by_price(orders, sort='DESC'): - """ Return list of orders sorted ascending or descending by price - - :param list | orders: list of orders to be sorted - :param string | sort: ASC or DESC. Default DESC - :return list: Sorted list of orders - """ - if sort.upper() == 'ASC': - reverse = False - elif sort.upper() == 'DESC': - reverse = True - else: - return None - - # Sort orders by price - return sorted(orders, key=lambda order: order['price'], reverse=reverse) - # GUI updaters def update_gui_slider(self): ticker = self.market.ticker() diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 810bba7c8..cd404d270 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -2,15 +2,13 @@ from datetime import datetime, timedelta from .base import StrategyBase + from .config_parts.relative_config import RelativeConfig from dexbot.strategies.external_feeds.price_feed import PriceFeed -from dexbot.orderengines.bitshares_engine import BitsharesOrderEngine from dexbot.pricefeeds.bitshares_feed import BitsharesPriceFeed -#class Strategy(StrategyBase): -# this inheritance is temporary before we finish refactoring strategybase -class Strategy(BitsharesOrderEngine, BitsharesPriceFeed): +class Strategy(StrategyBase, BitsharesPriceFeed): """ Relative Orders strategy """ diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 5fc8714f4..54ccdf691 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -6,14 +6,12 @@ from bitshares.dex import Dex from bitshares.amount import Amount -#from .base import StrategyBase +from .base import StrategyBase from .config_parts.staggered_config import StaggeredConfig -from dexbot.orderengines.bitshares_engine import BitsharesOrderEngine from dexbot.pricefeeds.bitshares_feed import BitsharesPriceFeed -#class Strategy(StrategyBase): # this inheritance is temporary before we finish refactoring strategybase -class Strategy(BitsharesOrderEngine, BitsharesPriceFeed): +class Strategy(StrategyBase, BitsharesPriceFeed): """ Staggered Orders strategy """ @classmethod From 064882bab4f0f20503dc4768be50c8fdeab033ca Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 18 Apr 2019 17:16:48 -0700 Subject: [PATCH 1356/1846] clean up imports --- dexbot/orderengines/bitshares_engine.py | 4 +--- dexbot/strategies/base.py | 17 ----------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index 807c9fac0..b9aa8fb4b 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -2,7 +2,6 @@ import copy import logging import time -#import math from dexbot.config import Config from dexbot.storage import Storage @@ -12,7 +11,6 @@ import bitsharesapi import bitsharesapi.exceptions -#from bitshares.account import Account from bitshares.amount import Amount, Asset from bitshares.dex import Dex from bitshares.instance import shared_bitshares_instance @@ -718,7 +716,7 @@ def all_own_orders(self, refresh=True): return orders - @property #todo: duplicate property, also in price feed, collisions possible? + @property #todo: duplicate property, also in price feed def market(self): """ Return the market object as :class:`bitshares.market.Market` """ diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index cda42f2eb..3d923c2c4 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1,12 +1,7 @@ -''' -import datetime -import copy -''' import logging import math import time -#from dexbot.helper import truncate from dexbot.config import Config from dexbot.storage import Storage from dexbot.qt_queue.idle_queue import idle_add @@ -21,15 +16,6 @@ from bitshares.instance import shared_bitshares_instance from bitshares.market import Market -''' -from bitshares.amount import Amount -import bitsharesapi -import bitsharesapi.exceptions -from bitshares.dex import Dex -from bitshares.price import FilledOrder, Order, UpdateCallOrder -from bitshares.utils import formatTime -''' - from events import Events # Number of maximum retries used to retry action before failing @@ -107,9 +93,6 @@ def __init__(self, # BitShares instance self.bitshares = bitshares_instance or shared_bitshares_instance() - # Dex instance used to get different fees for the market -# self.dex = Dex(self.bitshares) - # Storage Storage.__init__(self, name) From 32cbaad7f0ff4da08cb16871fa9dac1006a22e79 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 20 Apr 2019 17:01:19 +0500 Subject: [PATCH 1357/1846] Fix lost bitshares instance in get_order() --- dexbot/strategies/base.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 2f6b84c31..3d67f05d7 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1314,8 +1314,7 @@ def convert_fee(self, fee_amount, fee_asset): self.core_exchange_rate = temp_market.ticker()['core_exchange_rate'] return fee_amount * self.core_exchange_rate['base']['amount'] - @staticmethod - def get_order(order_id, return_none=True): + def get_order(self, order_id, return_none=True): """ Get Order object with order_id :param str | dict order_id: blockchain object id of the order can be an order dict with the id key in it @@ -1327,7 +1326,7 @@ def get_order(order_id, return_none=True): if 'id' in order_id: order_id = order_id['id'] try: - order = Order(order_id) + order = Order(order_id, blockchain_instance=self.bitshares) except Exception: logging.getLogger(__name__).error('Got an exception getting order id {}'.format(order_id)) raise From 795e32ed4e236f13b29fe1b37447769ca0133899 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 22 Apr 2019 16:03:42 +0500 Subject: [PATCH 1358/1846] Fix conditional expressions logic Closes: #567 --- dexbot/strategies/base.py | 4 ++-- dexbot/strategies/staggered_orders.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index fbc37d7a3..b84d76244 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1156,14 +1156,14 @@ def calc_profit(self): old_center_price = old_data.center_price center_price = self.get_market_center_price() - if not (old_center_price or center_price): + if not old_center_price or not center_price: return profit # Calculate max theoretical balances based on starting price old_max_quantity_base = earlier_base + earlier_quote * old_center_price old_max_quantity_quote = earlier_quote + earlier_base / old_center_price - if not (old_max_quantity_base or old_max_quantity_quote): + if not old_max_quantity_base or not old_max_quantity_quote: return profit # Current balances diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 74e9402e2..ab5cd94f3 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -154,11 +154,11 @@ def maintain_strategy(self, *args, **kwargs): self.store_profit_estimation_data() # Calculate minimal orders amounts based on asset precision - if not (self.order_min_base or self.order_min_quote): + if not self.order_min_base or not self.order_min_quote: self.calculate_min_amounts() # Calculate asset thresholds once - if not (self.quote_asset_threshold or self.base_asset_threshold): + if not self.quote_asset_threshold or not self.base_asset_threshold: self.calculate_asset_thresholds() # Remove orders that exceed boundaries From 6fb36647b233b9f58f2d8979e8d24e062765d6d9 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 23 Apr 2019 08:42:16 +0300 Subject: [PATCH 1359/1846] Change dexbot version number to 0.10.6 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index f443ece59..e8feede77 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.10.5' +VERSION = '0.10.6' AUTHOR = 'Codaone Oy' __version__ = VERSION From 66474ec8dc148e9740ba007a546cc601f48c61e6 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 23 Apr 2019 08:48:42 +0300 Subject: [PATCH 1360/1846] Change dexbot version number to 0.10.7 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index e8feede77..d388ff9a8 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.10.6' +VERSION = '0.10.7' AUTHOR = 'Codaone Oy' __version__ = VERSION From e2da36ef53858c4a2810a069e22110538f079605 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 23 Apr 2019 09:40:40 +0300 Subject: [PATCH 1361/1846] Change dexbot version number to 0.10.8 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index d388ff9a8..66fc74f11 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.10.7' +VERSION = '0.10.8' AUTHOR = 'Codaone Oy' __version__ = VERSION From d4221305e66643a398dc5c1398c26f0bf28cfa5f Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 23 Apr 2019 10:00:27 +0300 Subject: [PATCH 1362/1846] Fix PEP8 error --- dexbot/views/layouts/flow_layout.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dexbot/views/layouts/flow_layout.py b/dexbot/views/layouts/flow_layout.py index 307d6b11d..422a470ac 100644 --- a/dexbot/views/layouts/flow_layout.py +++ b/dexbot/views/layouts/flow_layout.py @@ -1,6 +1,7 @@ from PyQt5 import QtCore, QtWidgets from PyQt5.QtCore import Qt + class FlowLayout(QtWidgets.QLayout): def __init__(self, parent=None, margin=0, spacing=-1): From d6c3d8bea0c1ff94c02a578565f61f4eb1613567 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 23 Apr 2019 10:34:32 +0300 Subject: [PATCH 1363/1846] Change dexbot version number to 0.10.9 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 66fc74f11..77c424d35 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.10.8' +VERSION = '0.10.9' AUTHOR = 'Codaone Oy' __version__ = VERSION From 5d07d4cf5dd07ae40abf4ef319878e3fa7b44198 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 23 Apr 2019 12:51:10 +0500 Subject: [PATCH 1364/1846] Fix pausing of disabled worker --- dexbot/worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/worker.py b/dexbot/worker.py index a7df9c1d2..04d641193 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -205,7 +205,7 @@ def stop(self, worker_name=None, pause=False): self.config['workers'].pop(worker_name) self.accounts.remove(account) - if pause: + if pause and worker_name in self.workers: self.workers[worker_name].pause() self.workers.pop(worker_name, None) else: From 41dcaace7eb2263aa363d212dcda6f9cfe277c48 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 23 Apr 2019 12:59:57 +0500 Subject: [PATCH 1365/1846] Change market center price handling in profit methods Methods to store or get profit estimation data are depends on get_market_center_price(). When there is no bids or asks, get_market_center_price() will disable the whole worker. This is not needed when we just want to store/calc profits, so we can use `suppress_errors=True` here safely. Closes: #563 --- dexbot/strategies/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index fbc37d7a3..86d2113c1 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1125,7 +1125,7 @@ def store_profit_estimation_data(self): base_symbol = self.market['base'].get('symbol') quote_amount = assets['quote'] quote_symbol = self.market['quote'].get('symbol') - center_price = self.get_market_center_price() + center_price = self.get_market_center_price(suppress_errors=True) timestamp = time.time() self.store_balance_entry(account, self.worker_name, base_amount, base_symbol, @@ -1154,7 +1154,7 @@ def calc_profit(self): earlier_base = old_data.base_total earlier_quote = old_data.quote_total old_center_price = old_data.center_price - center_price = self.get_market_center_price() + center_price = self.get_market_center_price(suppress_errors=True) if not (old_center_price or center_price): return profit From eac6622d9368e8abda71b87991a2debe0b13fdb3 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 23 Apr 2019 18:08:19 -0700 Subject: [PATCH 1366/1846] move BitsharePriceFeed back to StrategyBase --- dexbot/strategies/base.py | 4 ++-- dexbot/strategies/relative_orders.py | 4 +--- dexbot/strategies/staggered_orders.py | 4 +--- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 3d923c2c4..b1b17dbc2 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -8,7 +8,7 @@ from .config_parts.base_config import BaseConfig from dexbot.orderengines.bitshares_engine import BitsharesOrderEngine -#from dexbot.pricefeeds.bitshares_feed import BitsharesPriceFeed +from dexbot.pricefeeds.bitshares_feed import BitsharesPriceFeed import bitshares.exceptions from bitshares.account import Account @@ -22,7 +22,7 @@ MAX_TRIES = 3 -class StrategyBase(BitsharesOrderEngine): +class StrategyBase(BitsharesOrderEngine, BitsharesPriceFeed): """ A strategy based on this class is intended to work in one market. This class contains most common methods needed by the strategy. diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index cd404d270..02ec580f0 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -5,10 +5,8 @@ from .config_parts.relative_config import RelativeConfig from dexbot.strategies.external_feeds.price_feed import PriceFeed -from dexbot.pricefeeds.bitshares_feed import BitsharesPriceFeed - -class Strategy(StrategyBase, BitsharesPriceFeed): +class Strategy(StrategyBase): """ Relative Orders strategy """ diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 54ccdf691..5159280ab 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -8,10 +8,8 @@ from .base import StrategyBase from .config_parts.staggered_config import StaggeredConfig -from dexbot.pricefeeds.bitshares_feed import BitsharesPriceFeed -# this inheritance is temporary before we finish refactoring strategybase -class Strategy(StrategyBase, BitsharesPriceFeed): +class Strategy(StrategyBase): """ Staggered Orders strategy """ @classmethod From f2ac30cefe6105e14ebb019e9e2256246ab22aaf Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 23 Apr 2019 22:04:44 -0700 Subject: [PATCH 1367/1846] rename test files --- .../{bitshares_engine_test.py => test_bitshares_engine.py} | 0 .../pricefeeds/{bitshares_feed_test.py => test_bitshares_feed.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename dexbot/orderengines/{bitshares_engine_test.py => test_bitshares_engine.py} (100%) rename dexbot/pricefeeds/{bitshares_feed_test.py => test_bitshares_feed.py} (100%) diff --git a/dexbot/orderengines/bitshares_engine_test.py b/dexbot/orderengines/test_bitshares_engine.py similarity index 100% rename from dexbot/orderengines/bitshares_engine_test.py rename to dexbot/orderengines/test_bitshares_engine.py diff --git a/dexbot/pricefeeds/bitshares_feed_test.py b/dexbot/pricefeeds/test_bitshares_feed.py similarity index 100% rename from dexbot/pricefeeds/bitshares_feed_test.py rename to dexbot/pricefeeds/test_bitshares_feed.py From 80bb5259b96838ab907999590c0d13a30f7e8fe4 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 24 Apr 2019 11:14:01 +0500 Subject: [PATCH 1368/1846] Fix formatting --- Dockerfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 97b53d6cc..3d761671b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,8 +13,8 @@ ENV DEXBOT_REPO_PATH $DEXBOT_HOME_PATH/repo ENV PATH $DEXBOT_HOME_PATH/.local/bin:$PATH # Update Ubuntu Software repository -RUN apt-get update -RUN apt-get install -y software-properties-common +RUN apt-get update +RUN apt-get install -y software-properties-common RUN add-apt-repository universe # Install dependencies @@ -33,4 +33,3 @@ RUN pip3 install --user pyyaml uptick tabulate ruamel.yaml sqlalchemy ccxt RUN git clone https://github.com/Codaone/DEXBot.git -b $VERSION $DEXBOT_REPO_PATH RUN cd $DEXBOT_REPO_PATH && make install-user RUN rm -rf $DEXBOT_REPO_PATH - From 8339f815f8273939d989e33a211b9c62419ae446 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 24 Apr 2019 15:16:05 +0500 Subject: [PATCH 1369/1846] Refactor Dockerfile - Split build stages - Update list of deb packages to install - Add volumes for storing data and configs - Build from local repo instead of doing `git clone` --- Dockerfile | 68 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3d761671b..95a0ba9ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,34 +2,50 @@ FROM ubuntu:18.04 # Variable arguments to populate labels -ARG VERSION=0.9.5 ARG USER=dexbot # Set ENV variables ENV LC_ALL C.UTF-8 ENV LANG C.UTF-8 -ENV DEXBOT_HOME_PATH /home/$USER -ENV DEXBOT_REPO_PATH $DEXBOT_HOME_PATH/repo -ENV PATH $DEXBOT_HOME_PATH/.local/bin:$PATH - -# Update Ubuntu Software repository -RUN apt-get update -RUN apt-get install -y software-properties-common -RUN add-apt-repository universe - -# Install dependencies -RUN apt-get install -y --install-recommends gcc libssl-dev python3-pip python3-dev python3-async whiptail inetutils-ping wget sudo git - -# Create user and change workdir -RUN groupadd -r $USER && useradd -r -g $USER $USER -WORKDIR $DEXBOT_HOME_PATH -RUN chown -R $USER:$USER $DEXBOT_HOME_PATH -USER dexbot - -RUN pip3 install --user pyyaml uptick tabulate ruamel.yaml sqlalchemy ccxt - -# Download and Install DEXBot - -RUN git clone https://github.com/Codaone/DEXBot.git -b $VERSION $DEXBOT_REPO_PATH -RUN cd $DEXBOT_REPO_PATH && make install-user -RUN rm -rf $DEXBOT_REPO_PATH +ENV HOME_PATH /home/$USER +ENV SRC_PATH $HOME_PATH/source +ENV PATH $HOME_PATH/.local/bin:$PATH +ENV LOCAL_DATA $HOME_PATH/.local/share +ENV CONFIG_DATA $HOME_PATH/.config + +RUN set -xe ;\ + apt-get update ;\ + apt-get install -y software-properties-common ;\ + add-apt-repository universe ;\ + # Prepare dependencies + apt-get install -y --install-recommends gcc make libssl-dev python3-pip python3-dev python3-async whiptail + +RUN set -xe ;\ + # Create user and change workdir + groupadd -r $USER ;\ + useradd -m -g $USER $USER ;\ + # Configure permissions (directories must be created with proper owner before VOLUME directive) + mkdir -p $SRC_PATH $LOCAL_DATA $CONFIG_DATA ;\ + chown -R $USER:$USER $HOME_PATH + +# Drop priveleges +USER $USER + +WORKDIR $SRC_PATH + +# Install dependencies in separate stage to speed up further builds +COPY requirements.txt $SRC_PATH/ +RUN python3 -m pip install --user -r requirements.txt + +# Copy project files +COPY dexbot $SRC_PATH/dexbot/ +COPY *.py *.cfg Makefile README.md $SRC_PATH/ + +# Build the project +RUN set -xe ;\ + python3 setup.py build ;\ + python3 setup.py install --user + +WORKDIR $HOME_PATH + +VOLUME ["$LOCAL_DATA", "$CONFIG_DATA"] From 15843122316253ac752d091e1c930c65c38fccf9 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Wed, 24 Apr 2019 23:43:59 -0700 Subject: [PATCH 1370/1846] moved and updated unit tests --- ...res_engine.py => bitshares_engine_test.py} | 0 ...tshares_feed.py => bitshares_feed_test.py} | 0 tests/pricefeeds/bitshares_feed_test.py | 100 ++++++++++++++++++ 3 files changed, 100 insertions(+) rename dexbot/orderengines/{test_bitshares_engine.py => bitshares_engine_test.py} (100%) rename dexbot/pricefeeds/{test_bitshares_feed.py => bitshares_feed_test.py} (100%) create mode 100644 tests/pricefeeds/bitshares_feed_test.py diff --git a/dexbot/orderengines/test_bitshares_engine.py b/dexbot/orderengines/bitshares_engine_test.py similarity index 100% rename from dexbot/orderengines/test_bitshares_engine.py rename to dexbot/orderengines/bitshares_engine_test.py diff --git a/dexbot/pricefeeds/test_bitshares_feed.py b/dexbot/pricefeeds/bitshares_feed_test.py similarity index 100% rename from dexbot/pricefeeds/test_bitshares_feed.py rename to dexbot/pricefeeds/bitshares_feed_test.py diff --git a/tests/pricefeeds/bitshares_feed_test.py b/tests/pricefeeds/bitshares_feed_test.py new file mode 100644 index 000000000..f09c8a7d0 --- /dev/null +++ b/tests/pricefeeds/bitshares_feed_test.py @@ -0,0 +1,100 @@ +from dexbot.pricefeeds.bitshares_feed import BitsharesPriceFeed +from bitshares.bitshares import BitShares +from bitshares.market import Market +import logging, os +import pytest + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s %(funcName)s %(lineno)d : %(message)s' +) + +class Test_PriceFeed: + def setup_class(self): + self.node_url = "wss://api.fr.bitsharesdex.com/ws" + self.TEST_CONFIG = { + 'node': self.node_url + } + self.bts = BitShares(node=self.TEST_CONFIG['node']) + self.market = Market("USD:BTS") + logging.info(self.market.ticker()) + + self.pf = BitsharesPriceFeed(market=self.market, bitshares_instance=self.bts) + logging.info("Setup Bitshares Price Feed Test: {}".format(self.bts)) + + + def teardown_class(self): + pass + + def setup_method(self): + pass + + def teardown_method(self): + pass + + def test_configure(self): + logging.info("Creating Bitshares Price Feed") + + def test_get_ticker(self): + ticker = self.pf.market + logging.info("Market ticker: {}".format(ticker)) + + def test_get_limit_orders(self): + mkt_orders = self.pf.get_limit_orders(depth=1) + logging.info("Limit Orders: {} ".format(mkt_orders)) + + def test_get_orderbook_orders(self): + orderbook = self.pf.get_orderbook_orders(depth=1) + logging.info("Orderbook orders: {} ".format(orderbook)) + + def test_get_market_center_price(self): + center_price = self.pf.get_market_center_price(base_amount=0, quote_amount=0, suppress_errors=False) + logging.info("Center price: {}".format(center_price)) + + def test_get_market_buy_price(self): + mkt_buy_price = self.pf.get_market_buy_price(quote_amount=0, base_amount=0) + logging.info("Get market buy price: {}".format(mkt_buy_price)) + + def test_get_market_sell_price(self): + mkt_sell_price = self.pf.get_market_sell_price(quote_amount=0, base_amount=0) + logging.info("Get market sell price: {}".format(mkt_sell_price)) + + def test_get_market_spread(self): + mkt_spread = self.pf.get_market_spread(quote_amount=0, base_amount=0) + logging.info("Market spread: {}".format(mkt_spread)) + + def test_get_market_buy_orders(self): + buy_orders = self.pf.get_market_buy_orders(depth=10) + logging.info("List of buy orders: {}".format(buy_orders)) + return buy_orders + + def test_sort_orders_by_price(self): + buy_orders = self.test_get_market_buy_orders() + asc_buy_orders = self.pf.sort_orders_by_price(buy_orders, sort='ASC') + logging.info("List of Buy orders in ASC price: {} ".format(asc_buy_orders)) + return asc_buy_orders + + def test_get_highest_market_buy_order(self): + asc_buy_orders = self.test_sort_orders_by_price() + highest = self.pf.get_highest_market_buy_order(asc_buy_orders) + logging.info("Highest market buy order: {}".format(highest)) + + def test_get_market_sell_orders(self): + sell_orders = self.pf.get_market_sell_orders(depth=10) + logging.info("Market Sell Orders: {}".format(sell_orders)) + return sell_orders + + def test_get_lowest_market_sell_order(self): + sell_orders = self.test_get_market_sell_orders() + lowest = self.pf.get_lowest_market_sell_order(sell_orders) + logging.info("Lowest market sell order: {} ".format(lowest)) + + + +if __name__ == '__main__': + cur_dir = os.path.dirname(__file__) + test_file = os.path.join(cur_dir, 'bitshares_feed_test.py') + pytest.main(['--capture=no', test_file]) + + + From 5556d3ef7ee350547977da75460ae1454ad9f0a6 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Wed, 24 Apr 2019 23:46:07 -0700 Subject: [PATCH 1371/1846] remove test from pricefeeds directory --- dexbot/pricefeeds/bitshares_feed_test.py | 63 ------------------------ 1 file changed, 63 deletions(-) delete mode 100644 dexbot/pricefeeds/bitshares_feed_test.py diff --git a/dexbot/pricefeeds/bitshares_feed_test.py b/dexbot/pricefeeds/bitshares_feed_test.py deleted file mode 100644 index d441f4711..000000000 --- a/dexbot/pricefeeds/bitshares_feed_test.py +++ /dev/null @@ -1,63 +0,0 @@ -from bitshares.bitshares import BitShares -from bitshares.market import Market - -from dexbot.pricefeeds.bts_feed import BitsharesPriceFeed - -node_url = "wss://api.fr.bitsharesdex.com/ws" - -TEST_CONFIG = { - 'node': node_url -} - -bts = BitShares(node=TEST_CONFIG['node']) - -print("Bitshares Price Feed Test") - -market = Market("USD:BTS") -print(market.ticker()) - -pf = BitsharesPriceFeed(market=market, bitshares_instance=bts) - -market = pf.market -print("\nMarket we are examining:", market, sep=':') - -center_price = pf.get_market_center_price(base_amount=0, quote_amount=0, suppress_errors=False) -print("\nCenter price:", center_price, sep=':') - -print("\nList of buy orders:") -buy_orders = pf.get_market_buy_orders(depth=10) -for order in buy_orders: - print(order) - -order1 = buy_orders[0] -print("\nGet top of buy orders", order1, sep=':') - -print("\nList of Buy orders in ASC price") -asc_buy_orders = pf.sort_orders_by_price(buy_orders, sort='ASC') -for order in asc_buy_orders: - print(order) - -sell_orders = pf.get_market_sell_orders(depth=10) -print("\nMarket Sell Orders", sell_orders, sep=':') - -mkt_orders = pf.get_limit_orders(depth=1) -print("\nMarket Orders", mkt_orders, sep=":") - -mkt_buy_price = pf.get_market_buy_price(quote_amount=0, base_amount=0) -print("market buy price", mkt_buy_price, sep=':') - -mkt_sell_price = pf.get_market_sell_price(quote_amount=0, base_amount=0) -print("market sell price", mkt_sell_price, sep=':') - -mkt_spread = pf.get_market_spread(quote_amount=0, base_amount=0) -print("market spread", mkt_spread, sep=':') - -highest = pf.get_highest_market_buy_order(asc_buy_orders) -print("Highest market buy order", highest, sep=':') - -lowest = pf.get_lowest_market_sell_order(sell_orders) -print("Lowest market sell order", lowest, sep=':') - - -# todo: - From 8b8f3015601907d364ea82bc5feb94aeb6fc2587 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 25 Apr 2019 14:29:20 +0500 Subject: [PATCH 1372/1846] Update param description --- dexbot/strategies/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 86d2113c1..c7fe71bfb 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -524,7 +524,7 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors :param float | base_amount: :param float | quote_amount: - :param bool | suppress_errors: + :param bool suppress_errors: True = return None on errors, False = disable worker :return: Market center price as float """ center_price = None From 37b8f6eeb164513b71a01d6dac3b47900daa4296 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 25 Apr 2019 14:31:45 +0500 Subject: [PATCH 1373/1846] Don't write profit data if center_price not available --- dexbot/strategies/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index c7fe71bfb..62983031e 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1126,6 +1126,9 @@ def store_profit_estimation_data(self): quote_amount = assets['quote'] quote_symbol = self.market['quote'].get('symbol') center_price = self.get_market_center_price(suppress_errors=True) + if not center_price: + # Don't write anything until center price will be available + return None timestamp = time.time() self.store_balance_entry(account, self.worker_name, base_amount, base_symbol, From f3ad49916fb8915373e33c594ac38cfea4520045 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 25 Apr 2019 14:55:46 +0500 Subject: [PATCH 1374/1846] Change default logfile path for cli In b3e8ff104abbf3804dee761a0508f6c1a86c7816 default logfile was changed to the same directory where the cli executable is. This is a wrong approach as dexbot can be installed in multiple ways. System-wide install will put the binary into /usr/bin/dexbot-cli. Applicateion must not try to log into /usr/bin/. This change makes logging consistent with GUI, which stores the log into user data directory, like ~/.local/share/dexbot/dexbot.log Closes: #490, #540 --- dexbot/ui.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/dexbot/ui.py b/dexbot/ui.py index 319a196b9..69e725b25 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -7,10 +7,13 @@ import click from ruamel import yaml +from appdirs import user_data_dir + from bitshares import BitShares from bitshares.instance import set_shared_bitshares_instance from bitshares.exceptions import WrongMasterPasswordException +from dexbot import VERSION, APP_NAME, AUTHOR from dexbot.config import Config log = logging.getLogger(__name__) @@ -47,8 +50,12 @@ def new_func(ctx, *args, **kwargs): # Logging to a file filename = ctx.obj.get('logfile') if not filename: - # By default, log to a file located where the script is - filename = os.path.join(os.path.dirname(sys.argv[0]), 'dexbot.log') + # By default, log to a user data dir + data_dir = user_data_dir(APP_NAME, AUTHOR) + filename = os.path.join(data_dir, 'dexbot.log') + # Print logfile using main logger + logging.getLogger("dexbot").info('Dexbot version {}, logfile: {}'.format(VERSION, filename)) + fh = logging.FileHandler(filename) fh.setFormatter(formatter2) logger.addHandler(fh) From 99bcd24e4f75fa55cf61c97f75128a3ca2a191b3 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 25 Apr 2019 15:23:50 +0500 Subject: [PATCH 1375/1846] Build docker image from Travis Build image to make sure build is not broken. --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 0ef3afa85..fbb7b297e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,6 @@ sudo: true +services: + - docker matrix: include: - os: linux @@ -9,6 +11,9 @@ matrix: python: '3.6' before_install: - brew update +before_install: + # Make sure docker image can be built + - docker build -t dexbot/dexbot . install: - pip install pyinstaller - pip install --upgrade setuptools From 4eee6600b382e85686e6e01f4cda7331298c274f Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 25 Apr 2019 16:07:18 +0500 Subject: [PATCH 1376/1846] Don't ignore hadolint config --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7c4f705e7..4d4356354 100644 --- a/.gitignore +++ b/.gitignore @@ -78,4 +78,7 @@ venv/ dexbot/views/ui/**/*_ui.py dexbot/resources/*_rc.py archive -*~ \ No newline at end of file +*~ + +# Dockerfile linter +!.hadolint.yaml From b73c44c544b2b62d654ef61caef529fa0edbeacb Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 25 Apr 2019 16:09:59 +0500 Subject: [PATCH 1377/1846] Ignore warning about versions pinning Hadolint suggests pinning versions via `apt-get install =` We don't have packages which needs to be pinned. --- .hadolint.yaml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .hadolint.yaml diff --git a/.hadolint.yaml b/.hadolint.yaml new file mode 100644 index 000000000..8f7e23e45 --- /dev/null +++ b/.hadolint.yaml @@ -0,0 +1,2 @@ +ignored: + - DL3008 From 63e4c1dfcb5b31f1f9e8f7327943394e011df5f3 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 25 Apr 2019 16:33:11 +0500 Subject: [PATCH 1378/1846] Optimize build deps Install only sufficient packages and remove package lists after. --- Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 95a0ba9ae..5b77bcd8a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,10 +15,11 @@ ENV CONFIG_DATA $HOME_PATH/.config RUN set -xe ;\ apt-get update ;\ - apt-get install -y software-properties-common ;\ - add-apt-repository universe ;\ # Prepare dependencies - apt-get install -y --install-recommends gcc make libssl-dev python3-pip python3-dev python3-async whiptail + apt-get install -y --no-install-recommends gcc make libssl-dev python3-pip python3-dev python3-setuptools \ + python3-async whiptail ;\ + apt-get clean ;\ + rm -rf /var/lib/apt/lists/* RUN set -xe ;\ # Create user and change workdir From ab21d832fe19363ff45ca0fa600fcaf3ff95d79d Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 25 Apr 2019 17:41:21 +0500 Subject: [PATCH 1379/1846] Add contribution links --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index f95303cc5..f25980f51 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,9 @@ master: Install the software, use it and report any problems by creating a ticket. +* [New Contributors Guide](https://github.com/Codaone/DEXBot/wiki/New-Contributors-Guide) +* [Git Workflow](https://github.com/Codaone/DEXBot/wiki/Git-Workflow) + # IMPORTANT NOTE THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR From af5442b0759184a88e6ac465cd9a7354456dff5e Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 25 Apr 2019 17:42:23 +0500 Subject: [PATCH 1380/1846] Reorganize sections --- README.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f25980f51..8eec4ccdb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # DEXBot +[![Build Status (master)](https://travis-ci.org/Codaone/DEXBot.svg?branch=master)](https://travis-ci.org/Codaone/DEXBot) + ![GUI](https://i.imgur.com/rW8XKQ4.png)The Dashboard of the GUI version of DEXBot ![CLI](https://i.imgur.com/H1N96nI.png)The CLI version of DEXBot in configuration dialog @@ -18,22 +20,16 @@ The _Relative Orders_ strategy is the one most think of when speaking of _Market ## Does it make profit? If you properly predict future market conditions, you can manage to make profit. All strategies rely on assumptions. The strategies that rely on less assumptions are less risky, and more risky strategies _can_ make more profit. During long declines the effect is decreased losses - not actual profits. So we can only say that it can make profit, without forgetting that it can also make losses. Good luck. -## Getting help -Join the [Telegram Chat for DEXBot](https://t.me/DEXBOTbts). - ## Installing and running the software See instructions in the [Wiki](https://github.com/Codaone/DEXBot/wiki) for [Linux](https://github.com/Codaone/DEXBot/wiki/Setup-Guide-for-Linux), [Windows](https://github.com/Codaone/DEXBot/wiki/Setup-Guide-for-Windows), [OSX](https://github.com/Codaone/DEXBot/wiki/Setup-Guide-for-Mac-OS-X). [Raspberry Pi](https://github.com/Codaone/DEXBot/wiki/Setup-guide-for-Raspberry-Pi). Other users can try downloading the package or following the Linux guide. -## Build status - -master: -[![Build Status](https://travis-ci.org/Codaone/DEXBot.svg?branch=master)](https://travis-ci.org/Codaone/DEXBot) - - **Warning**: This is highly experimental code! Use at your OWN risk! +## Getting help + +Join the [Telegram Chat for DEXBot](https://t.me/DEXBOTbts). ## Contributing From 1e1bccac6a2da248714b9522d882125f60d530e0 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 25 Apr 2019 17:42:42 +0500 Subject: [PATCH 1381/1846] Add example for running in docker --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 8eec4ccdb..68d4b81ed 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,22 @@ See instructions in the [Wiki](https://github.com/Codaone/DEXBot/wiki) for [Linu **Warning**: This is highly experimental code! Use at your OWN risk! +## Running in docker + +By default, local data is stored inside docker volumes. To avoid loosing configs and data, it's advised to mount custom +directories inside the container as shown below. + +``` +mkdir dexbot-data dexbot-config +docker run -it --rm -v `pwd`/dexbot-data:/home/dexbot/.local/share dexbot/dexbot:latest uptick addkey +docker run -it --rm -v `pwd`/dexbot-config:/home/dexbot/.config/dexbot -v `pwd`/dexbot-data:/home/dexbot/.local/share dexbot/dexbot:latest dexbot-cli configure +``` + +To run in unattended mode you need to provide wallet passphrase: + +``` +docker run -d --name dexbot -e UNLOCK=pass -v `pwd`/dexbot-config:/home/dexbot/.config/dexbot -v `pwd`/dexbot-data:/home/dexbot/.local/share dexbot/dexbot:latest dexbot-cli run +``` ## Getting help From f984faea8efcda917fda2373e5c54ed4b342d0d5 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 25 Apr 2019 17:49:34 +0500 Subject: [PATCH 1382/1846] Add readthedocs bage --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 68d4b81ed..d5119df5b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # DEXBot [![Build Status (master)](https://travis-ci.org/Codaone/DEXBot.svg?branch=master)](https://travis-ci.org/Codaone/DEXBot) +[![Documentation +Status](https://readthedocs.org/projects/dexbot/badge/?version=latest)](https://dexbot.readthedocs.io/en/latest/?badge=latest) ![GUI](https://i.imgur.com/rW8XKQ4.png)The Dashboard of the GUI version of DEXBot From e4f5b9c2cff060e58144a1793463275dd5721b20 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Fri, 26 Apr 2019 01:22:52 -0700 Subject: [PATCH 1383/1846] unit tests methods updated with pytest --- tests/pricefeeds/bitshares_feed_test.py | 67 ++++++++++++++----------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/tests/pricefeeds/bitshares_feed_test.py b/tests/pricefeeds/bitshares_feed_test.py index f09c8a7d0..26677b0ef 100644 --- a/tests/pricefeeds/bitshares_feed_test.py +++ b/tests/pricefeeds/bitshares_feed_test.py @@ -47,9 +47,39 @@ def test_get_orderbook_orders(self): orderbook = self.pf.get_orderbook_orders(depth=1) logging.info("Orderbook orders: {} ".format(orderbook)) - def test_get_market_center_price(self): - center_price = self.pf.get_market_center_price(base_amount=0, quote_amount=0, suppress_errors=False) - logging.info("Center price: {}".format(center_price)) + def test_filter_buy_orders(self): + buy_orders = self.pf.get_market_buy_orders(depth=10) + asc_orders = self.pf.filter_buy_orders(buy_orders, sort='ASC') + logging.info("Filter Buy Orders ASC {}".format(asc_orders)) + desc_orders = self.pf.filter_buy_orders(buy_orders, sort='DESC') + logging.info("Filter Buy Orders ASC {}".format(desc_orders)) + + def test_filter_sell_orders(self): + sell_orders = self.pf.get_market_sell_orders(depth=10) + asc_orders = self.pf.filter_sell_orders(sell_orders, sort='ASC') + logging.info("Filter Sell Orders ASC {}".format(asc_orders)) + desc_orders = self.pf.filter_sell_orders(sell_orders, sort='DESC') + logging.info("Filter Sell Orders DESC {}".format(desc_orders)) + + def test_get_highest_market_buy_order(self): + asc_buy_orders = self.test_sort_orders_by_price() + highest = self.pf.get_highest_market_buy_order(asc_buy_orders) + logging.info("Highest market buy order: {}".format(highest)) + + def test_get_lowest_market_sell_order(self): + sell_orders = self.test_get_market_sell_orders() + lowest = self.pf.get_lowest_market_sell_order(sell_orders) + logging.info("Lowest market sell order: {} ".format(lowest)) + + def test_get_market_buy_orders(self): + buy_orders = self.pf.get_market_buy_orders(depth=10) + logging.info("List of buy orders: {}".format(buy_orders)) + return buy_orders + + def test_get_market_sell_orders(self): + sell_orders = self.pf.get_market_sell_orders(depth=10) + logging.info("Market Sell Orders: {}".format(sell_orders)) + return sell_orders def test_get_market_buy_price(self): mkt_buy_price = self.pf.get_market_buy_price(quote_amount=0, base_amount=0) @@ -59,42 +89,21 @@ def test_get_market_sell_price(self): mkt_sell_price = self.pf.get_market_sell_price(quote_amount=0, base_amount=0) logging.info("Get market sell price: {}".format(mkt_sell_price)) + def test_get_market_center_price(self): + center_price = self.pf.get_market_center_price(base_amount=0, quote_amount=0, suppress_errors=False) + logging.info("Center price: {}".format(center_price)) + def test_get_market_spread(self): mkt_spread = self.pf.get_market_spread(quote_amount=0, base_amount=0) logging.info("Market spread: {}".format(mkt_spread)) - def test_get_market_buy_orders(self): - buy_orders = self.pf.get_market_buy_orders(depth=10) - logging.info("List of buy orders: {}".format(buy_orders)) - return buy_orders - def test_sort_orders_by_price(self): buy_orders = self.test_get_market_buy_orders() asc_buy_orders = self.pf.sort_orders_by_price(buy_orders, sort='ASC') logging.info("List of Buy orders in ASC price: {} ".format(asc_buy_orders)) return asc_buy_orders - def test_get_highest_market_buy_order(self): - asc_buy_orders = self.test_sort_orders_by_price() - highest = self.pf.get_highest_market_buy_order(asc_buy_orders) - logging.info("Highest market buy order: {}".format(highest)) - - def test_get_market_sell_orders(self): - sell_orders = self.pf.get_market_sell_orders(depth=10) - logging.info("Market Sell Orders: {}".format(sell_orders)) - return sell_orders - - def test_get_lowest_market_sell_order(self): - sell_orders = self.test_get_market_sell_orders() - lowest = self.pf.get_lowest_market_sell_order(sell_orders) - logging.info("Lowest market sell order: {} ".format(lowest)) - - - if __name__ == '__main__': cur_dir = os.path.dirname(__file__) test_file = os.path.join(cur_dir, 'bitshares_feed_test.py') - pytest.main(['--capture=no', test_file]) - - - + pytest.main(['--capture=no', test_file]) \ No newline at end of file From 02779b02be4e6f0cdcd553eb9e7be9f5d659c616 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Sun, 28 Apr 2019 23:42:07 -0700 Subject: [PATCH 1384/1846] add test for bitshares engine. rename test_bitshares_feed --- tests/orderengine/test_bitshares_engine.py | 86 +++++++++++++++++++ ...es_feed_test.py => test_bitshares_feed.py} | 1 + 2 files changed, 87 insertions(+) create mode 100644 tests/orderengine/test_bitshares_engine.py rename tests/pricefeeds/{bitshares_feed_test.py => test_bitshares_feed.py} (99%) diff --git a/tests/orderengine/test_bitshares_engine.py b/tests/orderengine/test_bitshares_engine.py new file mode 100644 index 000000000..6693d1a72 --- /dev/null +++ b/tests/orderengine/test_bitshares_engine.py @@ -0,0 +1,86 @@ +from bitshares.bitshares import BitShares +from bitshares.market import Market + +from dexbot.config import Config +from dexbot.orderengines.bitshares_engine import BitsharesOrderEngine + +import pytest +import logging, os + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s %(funcName)s %(lineno)d : %(message)s' +) + + +def test_fixtures(): + TEST_CONFIG = { + 'node': 'wss://api.fr.bitsharesdex.com/ws', + 'workers': { + 'worker 1': { + 'account': 'octet5', # edit this for TESTNET Account + 'amount': 0.015, + 'center_price': 0.0, + 'center_price_depth': 0.4, + 'center_price_dynamic': True, + 'center_price_offset': True, + 'custom_expiration': False, + 'dynamic_spread': False, + 'dynamic_spread_factor': 1.0, + 'expiration_time': 157680000.0, + 'external_feed': True, + 'external_price_source': 'gecko', + 'fee_asset': 'BTS', + 'manual_offset': 0.0, + 'market': 'OPEN.XMR/BTS', + 'market_depth_amount': 0.20, + 'module': 'dexbot.strategies.relative_orders', + 'partial_fill_threshold': 30.0, + 'price_change_threshold': 2.0, + 'relative_order_size': False, + 'reset_on_partial_fill': False, + 'reset_on_price_change': False, + 'spread': 5.0 + } + } + } + return TEST_CONFIG + + +class Test_OrderEngine: + def setup_class(self): + self.yml_data = test_fixtures() + self.config = Config(config=self.yml_data) + self.bitshares_instance = BitShares(node=self.config['node']) + + logging.info("Bitshares Price Feed Test") + + for worker_name, worker in self.config["workers"].items(): + logging.info(worker_name, worker) + self.pair = self.config["workers"][worker_name]["market"] + self.market = Market(self.config["workers"][worker_name]["market"]) + self.orderEngine = BitsharesOrderEngine(worker_name, config=self.config, market=self.market, + bitshares_instance=self.bitshares_instance) + logging.info("instantiating Bitshares order engine") + + def teardown_class(self): + pass + + def setup_method(self): + pass + + def teardown_method(self): + pass + + def test_all_own_orders(self): + orders = self.orderEngine.all_own_orders() + logging.info("All own orders: ".format(orders)) + + def test_market(self): + logging.info(self.market.ticker()) + + +if __name__ == '__main__': + cur_dir = os.path.dirname(__file__) + test_file = os.path.join(cur_dir, 'bitshares_engine_test.py') + pytest.main(['--capture=no', test_file]) diff --git a/tests/pricefeeds/bitshares_feed_test.py b/tests/pricefeeds/test_bitshares_feed.py similarity index 99% rename from tests/pricefeeds/bitshares_feed_test.py rename to tests/pricefeeds/test_bitshares_feed.py index 26677b0ef..daf233d87 100644 --- a/tests/pricefeeds/bitshares_feed_test.py +++ b/tests/pricefeeds/test_bitshares_feed.py @@ -103,6 +103,7 @@ def test_sort_orders_by_price(self): logging.info("List of Buy orders in ASC price: {} ".format(asc_buy_orders)) return asc_buy_orders + if __name__ == '__main__': cur_dir = os.path.dirname(__file__) test_file = os.path.join(cur_dir, 'bitshares_feed_test.py') From 9b4f505cf567e1ea2ce115b4b7d6d46abaa914e3 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 30 Apr 2019 13:56:39 +0300 Subject: [PATCH 1385/1846] Change dexbot version number to 0.10.10 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 77c424d35..958315942 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.10.9' +VERSION = '0.10.10' AUTHOR = 'Codaone Oy' __version__ = VERSION From 111f89d50be448fb5a8dbc2db1f68e2bf556df11 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Thu, 2 May 2019 09:02:05 +0300 Subject: [PATCH 1386/1846] Change dexbot version number to 0.10.11 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 958315942..a0e21b1e2 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.10.10' +VERSION = '0.10.11' AUTHOR = 'Codaone Oy' __version__ = VERSION From 97f1c00c3015bafd9c631a86253013e9c8e4115d Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Thu, 2 May 2019 09:25:05 +0300 Subject: [PATCH 1387/1846] Change dexbot version number to 0.10.12 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index a0e21b1e2..847b259f4 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.10.11' +VERSION = '0.10.12' AUTHOR = 'Codaone Oy' __version__ = VERSION From c7c1bfe09098b3121bca834c21a2078273bf3e2e Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 3 May 2019 19:42:45 +0500 Subject: [PATCH 1388/1846] Rename variable --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5f9a4a998..72fb60c8a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -154,9 +154,9 @@ def unused_account(bitshares): """ def _unused_account(): - range = 100000 + _range = 100000 while True: - account = 'worker-{}'.format(random.randint(1, range)) + account = 'worker-{}'.format(random.randint(1, _range)) try: Account(account, bitshares_instance=bitshares) except AccountDoesNotExistsException: From 6eacf601914821f65cd78817487c655f23cb0ac2 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 3 May 2019 19:48:20 +0500 Subject: [PATCH 1389/1846] Apply several formatting fixes --- tests/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/README.md b/tests/README.md index 37905366d..9d1328036 100644 --- a/tests/README.md +++ b/tests/README.md @@ -36,12 +36,11 @@ pytest tests/test_prepared_testnet.py ``` How to prepare genesis.json -=========================== +--------------------------- genesis.json contains initial accounts including witnesses and committee members. Every account has it's public key. For the sake of simplicity, pick any keypair and use it's public key for every account. - Balances -------- From 4816602178afd204002180b373a23f9ecc851e84 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 5 May 2019 21:52:43 +0500 Subject: [PATCH 1390/1846] Reformat expression --- dexbot/strategies/staggered_orders.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 667ac1f3a..e2601bdf8 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -403,8 +403,11 @@ def refresh_balances(self, use_cached_orders=False): # Calc avail balance; avail balances used in maintain_strategy to pass into allocate_asset # avail = total - real_orders - virtual_orders - self.quote_balance['amount'] = self.quote_total_balance - own_orders_balance['quote'] \ - - virtual_orders_quote_balance + self.quote_balance['amount'] = ( + self.quote_total_balance + - own_orders_balance['quote'] + - virtual_orders_quote_balance + ) self.base_balance['amount'] = self.base_total_balance - own_orders_balance['base'] - virtual_orders_base_balance # Reserve fees for N orders From b0b39ee35764ee0283463fcf1a4d231e5e75f6f2 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 5 May 2019 22:17:48 +0500 Subject: [PATCH 1391/1846] Fix worker removal when account still in use When removing a worker which uses account being in use by another worker too, we must not remove account subscription. This is a fix for the following condition discovered by @joelva: 1. Create two workers that have same account 2. Start both workers 3. Remove worker 1 4. Remove worker 2 --- dexbot/worker.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dexbot/worker.py b/dexbot/worker.py index a79cfc1e0..31414c12b 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -194,7 +194,14 @@ def stop(self, worker_name=None, pause=False): account = self.config['workers'][worker_name]['account'] self.config['workers'].pop(worker_name) - self.accounts.remove(account) + # We should remove account subscription only if account is not used by another worker + account_is_in_use = False + for _, worker in self.workers.items(): + if worker.account.name == account: + account_is_in_use = True + + if not account_is_in_use: + self.accounts.remove(account) if pause: self.workers[worker_name].pause() self.workers.pop(worker_name, None) From cd755b9dfcdc0ac8f3541088d06a24f3d6cec83a Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 5 May 2019 22:30:06 +0500 Subject: [PATCH 1392/1846] Fix flake8 warning --- dexbot/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dexbot/config.py b/dexbot/config.py index 8016525dd..b6aa756e8 100644 --- a/dexbot/config.py +++ b/dexbot/config.py @@ -210,7 +210,9 @@ def update_data(asset, operational_percent): raise ValueError('Operational percent for asset {} is more than 100%' .format(asset)) - tree = lambda: defaultdict(tree) + def tree(): + return defaultdict(tree) + data = tree() for worker_name, worker in config['workers'].items(): From 529899f8f01dd3da538120eb9ab4e474328d68bd Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 5 May 2019 23:14:01 +0500 Subject: [PATCH 1393/1846] Fix typo --- 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 e2601bdf8..c136defb2 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -144,7 +144,7 @@ def maintain_strategy(self, *args, **kwargs): self.market_center_price = self.center_price else: # Still not have market_center_price? Empty market, don't continue - self.log.warning('Cannot calculate center price on empty market, please set is manually') + self.log.warning('Cannot calculate center price on empty market, please set it manually') return # Calculate balances, and use orders from previous call of self.refresh_orders() to reduce API calls From 33d9fcd953952d977ae86ed81c49878adb4d0464 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 5 May 2019 23:14:48 +0500 Subject: [PATCH 1394/1846] Rename input fields for operational_percent_xxx To be consistent with param names, input field should be named as `param_name_input`. --- dexbot/views/ui/create_worker_window.ui | 4 ++-- dexbot/views/ui/edit_worker_window.ui | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dexbot/views/ui/create_worker_window.ui b/dexbot/views/ui/create_worker_window.ui index fc4a3b94b..1d2ce8a3a 100644 --- a/dexbot/views/ui/create_worker_window.ui +++ b/dexbot/views/ui/create_worker_window.ui @@ -813,7 +813,7 @@ - + 145 @@ -850,7 +850,7 @@ - + 145 diff --git a/dexbot/views/ui/edit_worker_window.ui b/dexbot/views/ui/edit_worker_window.ui index 09f4b5467..92c6f2430 100644 --- a/dexbot/views/ui/edit_worker_window.ui +++ b/dexbot/views/ui/edit_worker_window.ui @@ -7,7 +7,7 @@ 0 0 428 - 351 + 365 @@ -580,7 +580,7 @@ - + 145 @@ -617,7 +617,7 @@ - + 145 From 1c90b4bbe478bf6deb8b7eac91537db426610219 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 5 May 2019 23:16:04 +0500 Subject: [PATCH 1395/1846] Handle operational_percent_xxx when editing worker When creating or editing worker, param values should be saved to / taken from config. --- dexbot/controllers/worker_controller.py | 4 ++++ dexbot/views/edit_worker.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index 56e5a61cd..fd1c5c390 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -170,6 +170,8 @@ def handle_save(self): base_asset = self.view.base_asset_input.text() quote_asset = self.view.quote_asset_input.text() fee_asset = self.view.fee_asset_input.text() + operational_percent_quote = self.view.operational_percent_quote_input.value() + operational_percent_base = self.view.operational_percent_base_input.value() strategy_module = self.view.strategy_input.currentData() self.view.worker_data = { @@ -177,6 +179,8 @@ def handle_save(self): 'market': '{}/{}'.format(quote_asset, base_asset), 'module': strategy_module, 'fee_asset': fee_asset, + 'operational_percent_quote': operational_percent_quote, + 'operational_percent_base': operational_percent_base, **self.view.strategy_widget.values } self.view.worker_name = self.view.worker_name_input.text() diff --git a/dexbot/views/edit_worker.py b/dexbot/views/edit_worker.py index 8e35faba6..f0b43b204 100644 --- a/dexbot/views/edit_worker.py +++ b/dexbot/views/edit_worker.py @@ -31,6 +31,8 @@ def __init__(self, parent_widget, bitshares_instance, worker_name, config): self.quote_asset_input.setText(self.controller.get_quote_asset(worker_data)) self.fee_asset_input.setText(worker_data.get('fee_asset', 'BTS')) self.account_name.setText(self.controller.get_account(worker_data)) + self.operational_percent_quote_input.setValue(worker_data.get('operational_percent_quote', 0)) + self.operational_percent_base_input.setValue(worker_data.get('operational_percent_base', 0)) # Force uppercase to the assets fields validator = UppercaseValidator(self) From a2897471cb8b521657bac545ddf82b4f85b5a97c Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 5 May 2019 23:20:01 +0500 Subject: [PATCH 1396/1846] Remove unused variable --- dexbot/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/config.py b/dexbot/config.py index b6aa756e8..abc18e75a 100644 --- a/dexbot/config.py +++ b/dexbot/config.py @@ -215,7 +215,7 @@ def tree(): data = tree() - for worker_name, worker in config['workers'].items(): + for _, worker in config['workers'].items(): account = worker['account'] quote_asset = worker['market'].split('/')[0] base_asset = worker['market'].split('/')[1] From c2a1ff99aed2864c65b6886a55b99839f7d3cadf Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 9 May 2019 11:23:18 +0500 Subject: [PATCH 1397/1846] Switch to own forks of pybitshares and pygraphene Closes: #574, #570 --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index c8843bdd0..ea7cc4f99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,8 @@ yarl==1.1.0 ccxt==1.17.434 pywaves==0.8.20 bitshares==0.2.1 +git+https://github.com/Codaone/python-bitshares.git#egg=bitshares +git+https://github.com/Codaone/python-graphenelib.git#egg=graphenelib uptick==0.2.1 ruamel.yaml>=0.15.37 appdirs>=1.4.3 From 1db29ef531a0cf1054dc467d160c965da771991c Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 9 May 2019 16:52:08 +0500 Subject: [PATCH 1398/1846] Fix methods broken by python-bitshares upgrade --- dexbot/cli_conf.py | 13 ++++++++++++- dexbot/config_validator.py | 9 +++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 4f677d291..1159c9b28 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -21,6 +21,8 @@ import re import subprocess +from bitshares.account import Account + from dexbot.whiptail import get_whiptail from dexbot.strategies.base import StrategyBase from dexbot.config_validator import ConfigValidator @@ -431,7 +433,16 @@ def list_accounts(bitshares_instance): :return: list of tuples (int, 'account_name - key_type') """ - accounts = bitshares_instance.wallet.getAccounts() + accounts = [] + pubkeys = bitshares_instance.wallet.getPublicKeys(current=True) + + for pubkey in pubkeys: + account_ids = bitshares_instance.wallet.getAccountsFromPublicKey(pubkey) + for account_id in account_ids: + account = Account(account_id, bitshares_instance=bitshares_instance) + key_type = bitshares_instance.wallet.getKeyType(account, pubkey) + accounts.append({'name': account.name, 'type': key_type}) + account_list = [ (str(num), '{} - {}'.format(account['name'], account['type'])) for num, account in enumerate(accounts) ] diff --git a/dexbot/config_validator.py b/dexbot/config_validator.py index 6d9b438b3..4eaa659f2 100644 --- a/dexbot/config_validator.py +++ b/dexbot/config_validator.py @@ -38,8 +38,9 @@ def validate_private_key(self, account, private_key): wallet = self.bitshares.wallet if not private_key: # Check if the account is already in the database - accounts = wallet.getAccounts() - if any(account == d['name'] for d in accounts): + account_ids = wallet.getAccounts() + accounts = [Account(id, bitshares_instance=self.bitshares) for id in account_ids] + if any(account == a['name'] for a in accounts): return True return False @@ -49,8 +50,8 @@ def validate_private_key(self, account, private_key): return False # Load all accounts with corresponding public key from the blockchain - accounts = wallet.getAllAccounts(pubkey) - account_names = [account['name'] for account in accounts] + account_ids = wallet.getAccountsFromPublicKey(pubkey) + account_names = [Account(id, bitshares_instance=self.bitshares).name for id in account_ids] if account in account_names: return True From f4c81a817b71dab7861c5039883ff54adc7042db Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 9 May 2019 17:01:37 +0500 Subject: [PATCH 1399/1846] Remove excess dependency --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ea7cc4f99..c8f9d70f1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,6 @@ requests>=2.20.0 yarl==1.1.0 ccxt==1.17.434 pywaves==0.8.20 -bitshares==0.2.1 git+https://github.com/Codaone/python-bitshares.git#egg=bitshares git+https://github.com/Codaone/python-graphenelib.git#egg=graphenelib uptick==0.2.1 From e37cf41a9af1143f12ec3274491d3e68ffd2c964 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 20 Apr 2019 17:00:11 +0500 Subject: [PATCH 1400/1846] Add bindmount for logging.ini --- tests/conftest.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 72fb60c8a..96ad1d99b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -66,7 +66,13 @@ def bitshares_testnet(session_id, unused_port, docker_manager): image='bitshares/bitshares-core:testnet', name='bitshares-testnet-{}'.format(session_id), ports={'8091': port}, - volumes={'{}/tests/node_config'.format(os.path.abspath('.')): {'bind': '/etc/bitshares/', 'mode': 'ro'}}, + volumes={ + '{}/tests/node_config'.format(os.path.abspath('.')): {'bind': '/etc/bitshares/', 'mode': 'ro'}, + '{}/tests/node_config/logging.ini'.format(os.path.abspath('.')): { + 'bind': '/var/lib/bitshares/logging.ini', + 'mode': 'ro', + }, + }, detach=True, ) container.service_port = port From d203680789bab33e228253928fa28671991cbfc5 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 9 May 2019 14:38:31 -0700 Subject: [PATCH 1401/1846] remove old unit tests --- tests/orderengine/test_bitshares_engine.py | 86 ---------------- tests/pricefeeds/test_bitshares_feed.py | 110 --------------------- 2 files changed, 196 deletions(-) delete mode 100644 tests/orderengine/test_bitshares_engine.py delete mode 100644 tests/pricefeeds/test_bitshares_feed.py diff --git a/tests/orderengine/test_bitshares_engine.py b/tests/orderengine/test_bitshares_engine.py deleted file mode 100644 index 6693d1a72..000000000 --- a/tests/orderengine/test_bitshares_engine.py +++ /dev/null @@ -1,86 +0,0 @@ -from bitshares.bitshares import BitShares -from bitshares.market import Market - -from dexbot.config import Config -from dexbot.orderengines.bitshares_engine import BitsharesOrderEngine - -import pytest -import logging, os - -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s %(funcName)s %(lineno)d : %(message)s' -) - - -def test_fixtures(): - TEST_CONFIG = { - 'node': 'wss://api.fr.bitsharesdex.com/ws', - 'workers': { - 'worker 1': { - 'account': 'octet5', # edit this for TESTNET Account - 'amount': 0.015, - 'center_price': 0.0, - 'center_price_depth': 0.4, - 'center_price_dynamic': True, - 'center_price_offset': True, - 'custom_expiration': False, - 'dynamic_spread': False, - 'dynamic_spread_factor': 1.0, - 'expiration_time': 157680000.0, - 'external_feed': True, - 'external_price_source': 'gecko', - 'fee_asset': 'BTS', - 'manual_offset': 0.0, - 'market': 'OPEN.XMR/BTS', - 'market_depth_amount': 0.20, - 'module': 'dexbot.strategies.relative_orders', - 'partial_fill_threshold': 30.0, - 'price_change_threshold': 2.0, - 'relative_order_size': False, - 'reset_on_partial_fill': False, - 'reset_on_price_change': False, - 'spread': 5.0 - } - } - } - return TEST_CONFIG - - -class Test_OrderEngine: - def setup_class(self): - self.yml_data = test_fixtures() - self.config = Config(config=self.yml_data) - self.bitshares_instance = BitShares(node=self.config['node']) - - logging.info("Bitshares Price Feed Test") - - for worker_name, worker in self.config["workers"].items(): - logging.info(worker_name, worker) - self.pair = self.config["workers"][worker_name]["market"] - self.market = Market(self.config["workers"][worker_name]["market"]) - self.orderEngine = BitsharesOrderEngine(worker_name, config=self.config, market=self.market, - bitshares_instance=self.bitshares_instance) - logging.info("instantiating Bitshares order engine") - - def teardown_class(self): - pass - - def setup_method(self): - pass - - def teardown_method(self): - pass - - def test_all_own_orders(self): - orders = self.orderEngine.all_own_orders() - logging.info("All own orders: ".format(orders)) - - def test_market(self): - logging.info(self.market.ticker()) - - -if __name__ == '__main__': - cur_dir = os.path.dirname(__file__) - test_file = os.path.join(cur_dir, 'bitshares_engine_test.py') - pytest.main(['--capture=no', test_file]) diff --git a/tests/pricefeeds/test_bitshares_feed.py b/tests/pricefeeds/test_bitshares_feed.py deleted file mode 100644 index daf233d87..000000000 --- a/tests/pricefeeds/test_bitshares_feed.py +++ /dev/null @@ -1,110 +0,0 @@ -from dexbot.pricefeeds.bitshares_feed import BitsharesPriceFeed -from bitshares.bitshares import BitShares -from bitshares.market import Market -import logging, os -import pytest - -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s %(funcName)s %(lineno)d : %(message)s' -) - -class Test_PriceFeed: - def setup_class(self): - self.node_url = "wss://api.fr.bitsharesdex.com/ws" - self.TEST_CONFIG = { - 'node': self.node_url - } - self.bts = BitShares(node=self.TEST_CONFIG['node']) - self.market = Market("USD:BTS") - logging.info(self.market.ticker()) - - self.pf = BitsharesPriceFeed(market=self.market, bitshares_instance=self.bts) - logging.info("Setup Bitshares Price Feed Test: {}".format(self.bts)) - - - def teardown_class(self): - pass - - def setup_method(self): - pass - - def teardown_method(self): - pass - - def test_configure(self): - logging.info("Creating Bitshares Price Feed") - - def test_get_ticker(self): - ticker = self.pf.market - logging.info("Market ticker: {}".format(ticker)) - - def test_get_limit_orders(self): - mkt_orders = self.pf.get_limit_orders(depth=1) - logging.info("Limit Orders: {} ".format(mkt_orders)) - - def test_get_orderbook_orders(self): - orderbook = self.pf.get_orderbook_orders(depth=1) - logging.info("Orderbook orders: {} ".format(orderbook)) - - def test_filter_buy_orders(self): - buy_orders = self.pf.get_market_buy_orders(depth=10) - asc_orders = self.pf.filter_buy_orders(buy_orders, sort='ASC') - logging.info("Filter Buy Orders ASC {}".format(asc_orders)) - desc_orders = self.pf.filter_buy_orders(buy_orders, sort='DESC') - logging.info("Filter Buy Orders ASC {}".format(desc_orders)) - - def test_filter_sell_orders(self): - sell_orders = self.pf.get_market_sell_orders(depth=10) - asc_orders = self.pf.filter_sell_orders(sell_orders, sort='ASC') - logging.info("Filter Sell Orders ASC {}".format(asc_orders)) - desc_orders = self.pf.filter_sell_orders(sell_orders, sort='DESC') - logging.info("Filter Sell Orders DESC {}".format(desc_orders)) - - def test_get_highest_market_buy_order(self): - asc_buy_orders = self.test_sort_orders_by_price() - highest = self.pf.get_highest_market_buy_order(asc_buy_orders) - logging.info("Highest market buy order: {}".format(highest)) - - def test_get_lowest_market_sell_order(self): - sell_orders = self.test_get_market_sell_orders() - lowest = self.pf.get_lowest_market_sell_order(sell_orders) - logging.info("Lowest market sell order: {} ".format(lowest)) - - def test_get_market_buy_orders(self): - buy_orders = self.pf.get_market_buy_orders(depth=10) - logging.info("List of buy orders: {}".format(buy_orders)) - return buy_orders - - def test_get_market_sell_orders(self): - sell_orders = self.pf.get_market_sell_orders(depth=10) - logging.info("Market Sell Orders: {}".format(sell_orders)) - return sell_orders - - def test_get_market_buy_price(self): - mkt_buy_price = self.pf.get_market_buy_price(quote_amount=0, base_amount=0) - logging.info("Get market buy price: {}".format(mkt_buy_price)) - - def test_get_market_sell_price(self): - mkt_sell_price = self.pf.get_market_sell_price(quote_amount=0, base_amount=0) - logging.info("Get market sell price: {}".format(mkt_sell_price)) - - def test_get_market_center_price(self): - center_price = self.pf.get_market_center_price(base_amount=0, quote_amount=0, suppress_errors=False) - logging.info("Center price: {}".format(center_price)) - - def test_get_market_spread(self): - mkt_spread = self.pf.get_market_spread(quote_amount=0, base_amount=0) - logging.info("Market spread: {}".format(mkt_spread)) - - def test_sort_orders_by_price(self): - buy_orders = self.test_get_market_buy_orders() - asc_buy_orders = self.pf.sort_orders_by_price(buy_orders, sort='ASC') - logging.info("List of Buy orders in ASC price: {} ".format(asc_buy_orders)) - return asc_buy_orders - - -if __name__ == '__main__': - cur_dir = os.path.dirname(__file__) - test_file = os.path.join(cur_dir, 'bitshares_feed_test.py') - pytest.main(['--capture=no', test_file]) \ No newline at end of file From 79a3b4f7c2b014f4022b15b9dc8849373db8c1e1 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 9 May 2019 15:03:04 -0700 Subject: [PATCH 1402/1846] remove old bitshares_engine unit test, add comment to bitshares_engine.py --- dexbot/orderengines/bitshares_engine.py | 2 +- dexbot/orderengines/bitshares_engine_test.py | 66 -------------------- 2 files changed, 1 insertion(+), 67 deletions(-) delete mode 100644 dexbot/orderengines/bitshares_engine_test.py diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index b9aa8fb4b..49ff62ecf 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -716,7 +716,7 @@ def all_own_orders(self, refresh=True): return orders - @property #todo: duplicate property, also in price feed + @property #todo: property, also in price feed,need to considerinheritance priority def market(self): """ Return the market object as :class:`bitshares.market.Market` """ diff --git a/dexbot/orderengines/bitshares_engine_test.py b/dexbot/orderengines/bitshares_engine_test.py deleted file mode 100644 index a57d23376..000000000 --- a/dexbot/orderengines/bitshares_engine_test.py +++ /dev/null @@ -1,66 +0,0 @@ -from bitshares.bitshares import BitShares -from bitshares.market import Market - -from dexbot.config import Config -from dexbot.orderengines.bts_engine import BitsharesOrderEngine -from dexbot.strategies.base import StrategyBase - - -def fixture_data(): - TEST_CONFIG = { - 'node': 'wss://api.fr.bitsharesdex.com/ws', - 'workers': { - 'worker 1': { - 'account': 'octet5', # edit this for TESTNET Account - 'amount': 0.015, - 'center_price': 0.0, - 'center_price_depth': 0.4, - 'center_price_dynamic': True, - 'center_price_offset': True, - 'custom_expiration': False, - 'dynamic_spread': False, - 'dynamic_spread_factor': 1.0, - 'expiration_time': 157680000.0, - 'external_feed': True, - 'external_price_source': 'gecko', - 'fee_asset': 'BTS', - 'manual_offset': 0.0, - 'market': 'OPEN.XMR/BTS', - 'market_depth_amount': 0.20, - 'module': 'dexbot.strategies.relative_orders', - 'partial_fill_threshold': 30.0, - 'price_change_threshold': 2.0, - 'relative_order_size': False, - 'reset_on_partial_fill': False, - 'reset_on_price_change': False, - 'spread': 5.0 - } - } - } - return TEST_CONFIG - -def setup_test(): - - yml_data = fixture_data() - config = Config(config=yml_data) - bts = BitShares(node=config['node']) - print("Bitshares Price Feed Test") - - for worker_name, worker in config["workers"].items(): - print(worker_name, worker) - pair = config["workers"][worker_name]["market"] - market = Market(config["workers"][worker_name]["market"]) - print(pair) - - print("instantiating StrategyBase as a base comparison") - strategy = StrategyBase(worker_name, config=config, bitshares_instance=bts) - - print("instantiating Bitshares order engine") - orderEngine = BitsharesOrderEngine(worker_name, config=config, bitshares_instance=bts) - - print("Getting own orders:", orderEngine.get_own_orders) - - -# don't run this as is, or else it will over write the default config.yml -# put into unit testing structure -#setup_test() \ No newline at end of file From 4aa00e410523cce9cb4b75813c1587d54d986b7a Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 9 May 2019 17:34:45 -0700 Subject: [PATCH 1403/1846] manual merge from pulls #562, 568 and 558. has errors, need to debug --- dexbot/orderengines/bitshares_engine.py | 21 +++++++++++---------- dexbot/strategies/base.py | 10 +++++----- dexbot/strategies/staggered_orders.py | 12 ++++++------ requirements.txt | 2 +- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index 49ff62ecf..b0f06bd1a 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -195,10 +195,10 @@ def balance(self, asset, fee_reservation=0): return balance def calculate_order_data(self, order, amount, price): - quote_asset = Amount(amount, self.market['quote']['symbol']) + quote_asset = Amount(amount, self.market['quote']['symbol'], bitshares_instance=self.bitshares) order['quote'] = quote_asset order['price'] = price - base_asset = Amount(amount * price, self.market['base']['symbol']) + base_asset = Amount(amount * price, self.market['base']['symbol'], bitshares_instance=self.bitshares) order['base'] = base_asset return order @@ -301,8 +301,8 @@ def count_asset(self, order_ids=None, return_asset=False): base += orders_balance['base'] if return_asset: - quote = Amount(quote, quote_asset) - base = Amount(base, base_asset) + quote = Amount(quote, quote_asset, bitshares_instance=self.bitshares) + base = Amount(base, base_asset, bitshares_instance=self.bitshares) return {'quote': quote, 'base': base} @@ -335,8 +335,8 @@ def get_allocated_assets(self, order_ids=None, return_asset=False): # Return as Amount objects instead of only float values if return_asset: - quote = Amount(quote, quote_asset) - base = Amount(base, base_asset) + quote = Amount(quote, quote_asset, bitshares_instance=self.bitshares) + base = Amount(base, base_asset, bitshares_instance=self.bitshares) return {'quote': quote, 'base': base} @@ -556,7 +556,7 @@ def place_market_buy_order(self, amount, price, return_none=False, *args, **kwar buy_transaction = self.retry_action( self.market.buy, price, - Amount(amount=amount, asset=self.market["quote"]), + Amount(amount=amount, asset=self.market["quote"], bitshares_instance=self.bitshares), account=self.account.name, expiration=self.expiration, returnOrderId=return_order_id, @@ -762,7 +762,7 @@ def convert_fee(self, fee_amount, fee_asset): :return: float | amount of fee_asset to pay fee """ if isinstance(fee_asset, str): - fee_asset = Asset(fee_asset) + fee_asset = Asset(fee_asset, bitshares_instance=self.bitshares) if fee_asset['id'] == '1.3.0': # Fee asset is BTS, so no further calculations are needed @@ -775,7 +775,8 @@ def convert_fee(self, fee_amount, fee_asset): return fee_amount * self.core_exchange_rate['base']['amount'] @staticmethod - def get_order(order_id, return_none=True): + def get_order(self, order_id, return_none=True): +# order_id, return_none=True): """ Get Order object with order_id :param str | dict order_id: blockchain object id of the order can be an order dict with the id key in it @@ -787,7 +788,7 @@ def get_order(order_id, return_none=True): if 'id' in order_id: order_id = order_id['id'] try: - order = Order(order_id) + order = Order(order_id, bitshares_instance=self.bitshares) except Exception: logging.getLogger(__name__).error('Got an exception getting order id {}'.format(order_id)) raise diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index b1b17dbc2..447389420 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -139,12 +139,12 @@ def __init__(self, if fee_asset_symbol: try: - self.fee_asset = Asset(fee_asset_symbol) + self.fee_asset = Asset(fee_asset_symbol, bitshares_instance=self.bitshares) except bitshares.exceptions.AssetDoesNotExistsException: - self.fee_asset = Asset('1.3.0') + self.fee_asset = Asset('1.3.0', bitshares_instance=self.bitshares) else: # If there is no fee asset, use BTS - self.fee_asset = Asset('1.3.0') + self.fee_asset = Asset('1.3.0', bitshares_instance=self.bitshares) # CER cache self.core_exchange_rate = None @@ -238,14 +238,14 @@ def calc_profit(self): old_center_price = old_data.center_price center_price = self.get_market_center_price() - if not (old_center_price or center_price): + if not old_center_price or center_price: return profit # Calculate max theoretical balances based on starting price old_max_quantity_base = earlier_base + earlier_quote * old_center_price old_max_quantity_quote = earlier_quote + earlier_base / old_center_price - if not (old_max_quantity_base or old_max_quantity_quote): + if not old_max_quantity_base or old_max_quantity_quote: return profit # Current balances diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 5159280ab..87caa5903 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -153,11 +153,11 @@ def maintain_strategy(self, *args, **kwargs): self.store_profit_estimation_data() # Calculate minimal orders amounts based on asset precision - if not (self.order_min_base or self.order_min_quote): + if not self.order_min_base or self.order_min_quote: self.calculate_min_amounts() # Calculate asset thresholds once - if not (self.quote_asset_threshold or self.base_asset_threshold): + if not self.quote_asset_threshold or self.base_asset_threshold: self.calculate_asset_thresholds() # Remove orders that exceed boundaries @@ -1772,10 +1772,10 @@ def place_virtual_buy_order(self, amount, price): order = VirtualOrder() order['price'] = price - quote_asset = Amount(amount, self.market['quote']['symbol']) + quote_asset = Amount(amount, self.market['quote']['symbol'], bitshares_instance=self.bitshares) order['quote'] = quote_asset - base_asset = Amount(amount * price, self.market['base']['symbol']) + base_asset = Amount(amount * price, self.market['base']['symbol'], bitshares_instance=self.bitshares) order['base'] = base_asset order['for_sale'] = base_asset @@ -1801,10 +1801,10 @@ def place_virtual_sell_order(self, amount, price): order = VirtualOrder() order['price'] = price ** -1 - quote_asset = Amount(amount * price, self.market['base']['symbol']) + quote_asset = Amount(amount * price, self.market['base']['symbol'], bitshares_instance=self.bitshares) order['quote'] = quote_asset - base_asset = Amount(amount, self.market['quote']['symbol']) + base_asset = Amount(amount, self.market['quote']['symbol'], bitshares_instance=self.bitshares) order['base'] = base_asset order['for_sale'] = base_asset diff --git a/requirements.txt b/requirements.txt index e81f40f7c..c8843bdd0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,5 +15,5 @@ appdirs>=1.4.3 pycryptodomex==3.6.4 websocket-client==0.54.0 sdnotify==0.3.2 -sqlalchemy==1.2.11 +sqlalchemy==1.3.0 click==7.0 From 5f666a1ef421865a93a9fb182c8539f4cd0dc524 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 9 May 2019 17:55:59 -0700 Subject: [PATCH 1404/1846] fixed get_order static method, merges pull #562 #568, #558 now work --- dexbot/orderengines/bitshares_engine.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index b0f06bd1a..6a5e7ab03 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -774,9 +774,7 @@ def convert_fee(self, fee_amount, fee_asset): self.core_exchange_rate = temp_market.ticker()['core_exchange_rate'] return fee_amount * self.core_exchange_rate['base']['amount'] - @staticmethod def get_order(self, order_id, return_none=True): -# order_id, return_none=True): """ Get Order object with order_id :param str | dict order_id: blockchain object id of the order can be an order dict with the id key in it From 01c773205e35fd5605352c033ebd3d67a3d77f1d Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 9 May 2019 18:07:12 -0700 Subject: [PATCH 1405/1846] make note on future todo in StrategyBase comments --- dexbot/strategies/base.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 447389420..ee7f83c85 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -26,6 +26,20 @@ class StrategyBase(BitsharesOrderEngine, BitsharesPriceFeed): """ A strategy based on this class is intended to work in one market. This class contains most common methods needed by the strategy. + NOTE: StrategyBase currently requires BitsharesOrderEngine inheritance + as all configuration from Worker is located here. + + Post Core-refactor, in the future it should not be this way. + + TODO: The StrategyBase should be able to select any {N} OrderEngine(s) and {M} PriceFeed(s) + and not be tied to the BitsharesOrderEngine only. (where N and M are integers) + This would allow for cross dex or cex strategy flexibility + + In process: make StrategyBase an ABC. + + Unit tests should take above into consideration + + All prices are passed and returned as BASE/QUOTE. (In the BREAD:USD market that would be USD/BREAD, 2.5 USD / 1 BREAD). - Buy orders reserve BASE From 5e969400666710ed169ff71fc4ef0bb32c4a5559 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 10 May 2019 08:30:25 +0300 Subject: [PATCH 1406/1846] Fix PEP8 errors --- dexbot/orderengines/bitshares_engine.py | 4 ++-- dexbot/pricefeeds/bitshares_feed.py | 14 ++++++-------- dexbot/strategies/relative_orders.py | 22 ---------------------- 3 files changed, 8 insertions(+), 32 deletions(-) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index 6a5e7ab03..9a061c133 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -716,8 +716,9 @@ def all_own_orders(self, refresh=True): return orders - @property #todo: property, also in price feed,need to considerinheritance priority + @property def market(self): + # TODO: property, also in price feed, need to consider inheritance priority """ Return the market object as :class:`bitshares.market.Market` """ return self._market @@ -793,4 +794,3 @@ def get_order(self, order_id, return_none=True): if return_none and order['deleted']: return None return order - diff --git a/dexbot/pricefeeds/bitshares_feed.py b/dexbot/pricefeeds/bitshares_feed.py index daa506932..f55ad68c7 100644 --- a/dexbot/pricefeeds/bitshares_feed.py +++ b/dexbot/pricefeeds/bitshares_feed.py @@ -4,17 +4,16 @@ from bitshares.instance import shared_bitshares_instance from bitshares.price import Order + class BitsharesPriceFeed: - """ - This Price Feed class enables usage of Bitshares DEX for market center and order - book pricing, without requiring a registered account. It may be used for both - strategy and indicator analysis tools. + """ This Price Feed class enables usage of Bitshares DEX for market center and order + book pricing, without requiring a registered account. It may be used for both + strategy and indicator analysis tools. - All prices are passed and returned as BASE/QUOTE. - (In the BREAD/USD market that would be USD/BREAD, 2.5 USD / 1 BREAD). + All prices are passed and returned as BASE/QUOTE. + (In the BREAD/USD market that would be USD/BREAD, 2.5 USD / 1 BREAD). - Buy orders reserve BASE - Sell orders reserve QUOTE - """ def __init__(self, market, @@ -340,7 +339,6 @@ def market(self): """ return self._market - @staticmethod def sort_orders_by_price(orders, sort='DESC'): """ Return list of orders sorted ascending or descending by price diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 46bf6edc8..74841b43a 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -412,28 +412,6 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, exclude_own_order return base_amount / quote_amount - def get_external_market_center_price(self, external_price_source): - """ Get center price from an external market for current market pair - - :param external_price_source: External market name - :return: Center price as float - """ - self.log.debug('inside get_external_mcp, exchange: {} '.format(external_price_source)) - market = self.market.get_string('/') - self.log.debug('market: {} '.format(market)) - price_feed = PriceFeed(external_price_source, market) - price_feed.filter_symbols() - center_price = price_feed.get_center_price(None) - self.log.debug('PriceFeed: {}'.format(center_price)) - - if center_price is None: # Try USDT - center_price = price_feed.get_center_price("USDT") - self.log.debug('Substitute USD/USDT center price: {}'.format(center_price)) - if center_price is None: # Try consolidated - center_price = price_feed.get_consolidated_price() - self.log.debug('Consolidated center price: {}'.format(center_price)) - return center_price - def _calculate_center_price(self, suppress_errors=False): highest_bid = float(self.ticker().get('highestBid')) lowest_ask = float(self.ticker().get('lowestAsk')) From 9f3cdf8e645dc89eb4417e92ab4e850dbc087e64 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 10 May 2019 09:01:05 +0300 Subject: [PATCH 1407/1846] Change documentation --- dexbot/pricefeeds/bitshares_feed.py | 12 ++++++------ dexbot/strategies/relative_orders.py | 11 +++++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/dexbot/pricefeeds/bitshares_feed.py b/dexbot/pricefeeds/bitshares_feed.py index f55ad68c7..bccae6619 100644 --- a/dexbot/pricefeeds/bitshares_feed.py +++ b/dexbot/pricefeeds/bitshares_feed.py @@ -174,9 +174,9 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0): # Like get_market_sell_price(), but defaulting to base_amount if both base and quote are specified. asset_amount = base_amount - """ Since the purpose is never get both quote and base amounts, favor base amount if both given because - this function is looking for buy price. - """ + # Since the purpose is never get both quote and base amounts, favor base amount if both given because + # this function is looking for buy price. + if base_amount > quote_amount: base = True else: @@ -240,9 +240,9 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0): asset_amount = quote_amount - """ Since the purpose is never get both quote and base amounts, favor quote amount if both given because - this function is looking for sell price. - """ + # Since the purpose is never get both quote and base amounts, favor quote amount if both given because + # this function is looking for sell price. + if quote_amount > base_amount: quote = True else: diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 74841b43a..12088bc3d 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -287,9 +287,9 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, exclude_own_orders # Like get_market_sell_price(), but defaulting to base_amount if both base and quote are specified. asset_amount = base_amount - """ Since the purpose is never get both quote and base amounts, favor base amount if both given because - this function is looking for buy price. - """ + # Since the purpose is never get both quote and base amounts, favor base amount if both given because + # this function is looking for buy price. + if base_amount > quote_amount: base = True else: @@ -365,9 +365,8 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, exclude_own_order asset_amount = quote_amount - """ Since the purpose is never get both quote and base amounts, favor quote amount if both given because - this function is looking for sell price. - """ + # Since the purpose is never get both quote and base amounts, favor quote amount if both given because + # this function is looking for sell price. if quote_amount > base_amount: quote = True else: From cd1edd2534c4ebe6dd83061bb457d0f5c17d8a81 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 10 May 2019 10:01:49 +0300 Subject: [PATCH 1408/1846] Fix parameters differ from overridden method error --- dexbot/pricefeeds/bitshares_feed.py | 13 +++++++------ dexbot/strategies/relative_orders.py | 12 ++++++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/dexbot/pricefeeds/bitshares_feed.py b/dexbot/pricefeeds/bitshares_feed.py index bccae6619..c0f463384 100644 --- a/dexbot/pricefeeds/bitshares_feed.py +++ b/dexbot/pricefeeds/bitshares_feed.py @@ -156,13 +156,14 @@ def get_market_sell_orders(self, depth=10): sell_orders = self.filter_sell_orders(orders) return sell_orders - def get_market_buy_price(self, quote_amount=0, base_amount=0): + def get_market_buy_price(self, quote_amount=0, base_amount=0, **kwargs): # TODO: refactor to use orders instead of exclude_own_orders """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with moving average or weighted moving average :param float | quote_amount: :param float | base_amount: + :param dict | kwargs: :return: price as float """ market_buy_orders = [] @@ -221,7 +222,7 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0): return base_amount / quote_amount - def get_market_sell_price(self, quote_amount=0, base_amount=0): + def get_market_sell_price(self, quote_amount=0, base_amount=0, **kwargs): # TODO: refactor to use orders instead of exclude_own_orders """ Returns the BASE/QUOTE price for which [quote_amount] worth of QUOTE could be bought, enhanced with moving average or weighted moving average. @@ -230,6 +231,7 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0): :param float | quote_amount: :param float | base_amount: + :param dict | kwargs: :return: """ market_sell_orders = [] @@ -296,10 +298,9 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors :return: Market center price as float """ center_price = None - buy_price = self.get_market_buy_price(quote_amount=quote_amount, - base_amount=base_amount) - sell_price = self.get_market_sell_price(quote_amount=quote_amount, - base_amount=base_amount) + buy_price = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount) + sell_price = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount) + if buy_price is None or buy_price == 0.0: if not suppress_errors: self.log.critical("Cannot estimate center price, there is no highest bid.") diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 12088bc3d..7d75234c3 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -257,15 +257,17 @@ def update_orders(self): if len(order_ids) < expected_num_orders and not self.disabled: self.update_orders() - def get_market_buy_price(self, quote_amount=0, base_amount=0, exclude_own_orders=True): + def get_market_buy_price(self, quote_amount=0, base_amount=0, **kwargs): """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with moving average or weighted moving average :param float | quote_amount: :param float | base_amount: - :param bool | exclude_own_orders: Exclude own orders when calculating a price + :param dict | kwargs: + bool | exclude_own_orders: Exclude own orders when calculating a price :return: price as float """ + exclude_own_orders = kwargs.get('exclude_own_orders', True) market_buy_orders = [] # Exclude own orders from orderbook if needed @@ -334,7 +336,7 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, exclude_own_orders return base_amount / quote_amount - def get_market_sell_price(self, quote_amount=0, base_amount=0, exclude_own_orders=True): + def get_market_sell_price(self, quote_amount=0, base_amount=0, **kwargs): """ Returns the BASE/QUOTE price for which [quote_amount] worth of QUOTE could be bought, enhanced with moving average or weighted moving average. @@ -342,9 +344,11 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, exclude_own_order :param float | quote_amount: :param float | base_amount: - :param bool | exclude_own_orders: Exclude own orders when calculating a price + :param dict | kwargs: + bool | exclude_own_orders: Exclude own orders when calculating a price :return: """ + exclude_own_orders = kwargs.get('exclude_own_orders', True) market_sell_orders = [] # Exclude own orders from orderbook if needed From e6bad19348ad2411c30765aadc1cded8464ddb48 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 10 May 2019 10:22:28 +0300 Subject: [PATCH 1409/1846] Change variable to more readable o -> order --- dexbot/strategies/relative_orders.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 7d75234c3..e7aa8adf4 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -273,8 +273,8 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, **kwargs): # Exclude own orders from orderbook if needed if exclude_own_orders: market_buy_orders = self.get_market_buy_orders(depth=self.fetch_depth) - own_buy_orders_ids = [o['id'] for o in self.get_own_buy_orders()] - market_buy_orders = [o for o in market_buy_orders if o['id'] not in own_buy_orders_ids] + own_buy_orders_ids = [order['id'] for order in self.get_own_buy_orders()] + market_buy_orders = [order for order in market_buy_orders if order['id'] not in own_buy_orders_ids] # In case amount is not given, return price of the highest buy order on the market if quote_amount == 0 and base_amount == 0: @@ -354,8 +354,8 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, **kwargs): # Exclude own orders from orderbook if needed if exclude_own_orders: market_sell_orders = self.get_market_sell_orders(depth=self.fetch_depth) - own_sell_orders_ids = [o['id'] for o in self.get_own_sell_orders()] - market_sell_orders = [o for o in market_sell_orders if o['id'] not in own_sell_orders_ids] + own_sell_orders_ids = [order['id'] for order in self.get_own_sell_orders()] + market_sell_orders = [order for order in market_sell_orders if order['id'] not in own_sell_orders_ids] # In case amount is not given, return price of the lowest sell order on the market if quote_amount == 0 and base_amount == 0: From fb2bc621c904be0739011c1add35799ee840ccbb Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 10 May 2019 10:57:14 +0300 Subject: [PATCH 1410/1846] Fix lost change in refactor merge Lost change from 795e32e --- dexbot/strategies/base.py | 4 ++-- dexbot/strategies/staggered_orders.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 337237a2b..2cdd357be 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -252,14 +252,14 @@ def calc_profit(self): old_center_price = old_data.center_price center_price = self.get_market_center_price() - if not old_center_price or center_price: + if not old_center_price or not center_price: return profit # Calculate max theoretical balances based on starting price old_max_quantity_base = earlier_base + earlier_quote * old_center_price old_max_quantity_quote = earlier_quote + earlier_base / old_center_price - if not old_max_quantity_base or old_max_quantity_quote: + if not old_max_quantity_base or not old_max_quantity_quote: return profit # Current balances diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index b2cbc6cc4..e977c4804 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -154,11 +154,11 @@ def maintain_strategy(self, *args, **kwargs): self.store_profit_estimation_data() # Calculate minimal orders amounts based on asset precision - if not self.order_min_base or self.order_min_quote: + if not self.order_min_base or not self.order_min_quote: self.calculate_min_amounts() # Calculate asset thresholds once - if not self.quote_asset_threshold or self.base_asset_threshold: + if not self.quote_asset_threshold or not self.base_asset_threshold: self.calculate_asset_thresholds() # Remove orders that exceed boundaries From 4f2a8ccc3b252fec6ea0683ba382615389f07bb6 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 10 May 2019 13:35:02 +0500 Subject: [PATCH 1411/1846] Restore lost fee_asset changes From f9922c720b5cc7d332af66b2feeb8d6d61724617 --- dexbot/orderengines/bitshares_engine.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index 9a061c133..5f46bfd1b 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -86,12 +86,12 @@ def __init__(self, if fee_asset_symbol: try: - self.fee_asset = Asset(fee_asset_symbol) + self.fee_asset = Asset(fee_asset_symbol, bitshares_instance=self.bitshares) except bitshares.exceptions.AssetDoesNotExistsException: - self.fee_asset = Asset('1.3.0') + self.fee_asset = Asset('1.3.0', bitshares_instance=self.bitshares) else: # If there is no fee asset, use BTS - self.fee_asset = Asset('1.3.0') + self.fee_asset = Asset('1.3.0', bitshares_instance=self.bitshares) # CER cache self.core_exchange_rate = None @@ -771,7 +771,7 @@ def convert_fee(self, fee_amount, fee_asset): else: if not self.core_exchange_rate: # Determine how many fee_asset is needed for core-exchange - temp_market = Market(base=fee_asset, quote=Asset('1.3.0')) + temp_market = Market(base=fee_asset, quote=Asset('1.3.0', bitshares_instance=self.bitshares)) self.core_exchange_rate = temp_market.ticker()['core_exchange_rate'] return fee_amount * self.core_exchange_rate['base']['amount'] From 1f1ee092d49bc36d1cc68e67520c1f1c38ed9aad Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 10 May 2019 13:55:52 +0500 Subject: [PATCH 1412/1846] Minor docstrings correction --- dexbot/orderengines/bitshares_engine.py | 3 ++- dexbot/pricefeeds/bitshares_feed.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index 5f46bfd1b..539bc8352 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -278,6 +278,7 @@ def count_asset(self, order_ids=None, return_asset=False): :param list | order_ids: list of order ids to be added to the balance :param bool | return_asset: true if returned values should be Amount instances :return: dict with keys quote and base + Todo: When would we want the sum of a subset of orders? Why order_ids? Maybe just specify asset? """ quote = 0 @@ -371,7 +372,7 @@ def get_lowest_own_sell_order(self, orders=None): def get_market_orders(self, depth=1, updated=True): """ Returns orders from the current market. Orders are sorted by price. - get_market_orders() call does not have any depth limit. + get_limit_orders() call does not have any depth limit. :param int | depth: Amount of orders per side will be fetched, default=1 :param bool | updated: Return updated orders. "Updated" means partially filled orders will represent diff --git a/dexbot/pricefeeds/bitshares_feed.py b/dexbot/pricefeeds/bitshares_feed.py index c0f463384..3918db383 100644 --- a/dexbot/pricefeeds/bitshares_feed.py +++ b/dexbot/pricefeeds/bitshares_feed.py @@ -36,7 +36,8 @@ def get_limit_orders(self, depth=1): """ Returns orders from the current market. Orders are sorted by price. Does not require account info. get_limit_orders() call does not have any depth limit. - :param int | depth: Amount of orders per side will be fetched, default=1 + + :param int depth: Amount of orders per side will be fetched, default=1 :return: Returns a list of orders or None """ orders = self.bitshares.rpc.get_limit_orders(self.market['base']['id'], self.market['quote']['id'], depth) From 643b7e90cd5aa9e93fcc6251665c8f9d1a8eae1a Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 10 May 2019 12:09:37 +0300 Subject: [PATCH 1413/1846] Change dexbot version number to 0.11.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 847b259f4..14b90dc6f 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.10.12' +VERSION = '0.11.0' AUTHOR = 'Codaone Oy' __version__ = VERSION From 6f483d4b4ecfdd26cec02dd5e3ad82bdca41d340 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 10 May 2019 15:57:34 +0500 Subject: [PATCH 1414/1846] Move get_own_orders into BitsharesOrderEngine Before this BitsharesOrderEngine class tried to use get_own_orders() which is nor defined neighter inherited. --- dexbot/orderengines/bitshares_engine.py | 17 +++++++++++++++++ dexbot/strategies/base.py | 17 ----------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index 539bc8352..3e271b84f 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -717,6 +717,23 @@ def all_own_orders(self, refresh=True): return orders + @property + def get_own_orders(self): + """ Return the account's open orders in the current market + + :return: List of Order objects + """ + orders = [] + + # Refresh account data + self.account.refresh() + + for order in self.account.openorders: + if self.worker["market"] == order.market and self.account.openorders: + orders.append(order) + + return orders + @property def market(self): # TODO: property, also in price feed, need to consider inheritance priority diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 2cdd357be..207acb415 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -302,23 +302,6 @@ def base_asset(self): def quote_asset(self): return self.worker['market'].split('/')[0] - @property - def get_own_orders(self): - """ Return the account's open orders in the current market - - :return: List of Order objects - """ - orders = [] - - # Refresh account data - self.account.refresh() - - for order in self.account.openorders: - if self.worker["market"] == order.market and self.account.openorders: - orders.append(order) - - return orders - @property def market(self): """ Return the market object as :class:`bitshares.market.Market` From 8a146d4d7554dfae270f091eb546bca9d9893324 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 10 May 2019 14:03:06 +0300 Subject: [PATCH 1415/1846] Change dexbot version number to 0.11.1 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 14b90dc6f..86d19dfb9 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.11.0' +VERSION = '0.11.1' AUTHOR = 'Codaone Oy' __version__ = VERSION From a44ee48c5b98e4df085f4424938e47062b91326a Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 10 May 2019 16:05:03 +0500 Subject: [PATCH 1416/1846] Remove update_gui_slider from strategy_template.py This method is already defined inside StrategyBase class --- dexbot/strategies/strategy_template.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/dexbot/strategies/strategy_template.py b/dexbot/strategies/strategy_template.py index c5ac7f9ac..f5ec6d426 100644 --- a/dexbot/strategies/strategy_template.py +++ b/dexbot/strategies/strategy_template.py @@ -4,7 +4,6 @@ # Project imports from .base import StrategyBase from .config_parts.strategy_config import StrategyConfig -from dexbot.qt_queue.idle_queue import idle_add # Third party imports # from bitshares.market import Market @@ -138,25 +137,3 @@ def tick(self, d): if not (self.counter or 0) % 3: self.maintain_strategy() self.counter += 1 - - def update_gui_slider(self): - """ Updates GUI slider on the workers list """ - latest_price = self.ticker().get('latest', {}).get('price', None) - if not latest_price: - return - - order_ids = None - orders = self.get_own_orders - - if orders: - order_ids = [order['id'] for order in orders if 'id' in order] - - total_balance = self.count_asset(order_ids) - 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 From bdba4b67a7f80ddb4940130941ad2b270721cf4a Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 10 May 2019 14:09:39 +0300 Subject: [PATCH 1417/1846] Change dexbot version number to 0.11.2 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 86d19dfb9..5e981b81c 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.11.1' +VERSION = '0.11.2' AUTHOR = 'Codaone Oy' __version__ = VERSION From 1712d4b98845c9b20e9c176fa4c85399cd5334c7 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 10 May 2019 16:10:36 +0500 Subject: [PATCH 1418/1846] Refactor methods for getting own orders * consistent property names: own_orders, all_own_orders * separate methods get_all_own_orders, get_own_orders: allows to use `refresh=True` as needed, also fixes ability to use `refresh` kwarg because it was unable to use on property --- dexbot/orderengines/bitshares_engine.py | 48 +++++++++++++++---------- dexbot/strategies/staggered_orders.py | 4 +-- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index 3e271b84f..54bebcd53 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -221,7 +221,7 @@ def calculate_worker_value(self, unit_of_measure): quote_total += balance['amount'] # Calculate value of the orders in unit of measure - orders = self.get_own_orders + orders = self.own_orders for order in orders: if order['base']['symbol'] == self.quote_asset: # Pick sell orders order's BASE amount, which is same as worker's QUOTE, to worker's BASE @@ -295,7 +295,7 @@ def count_asset(self, order_ids=None, return_asset=False): if order_ids is None: # Get all orders from Blockchain - order_ids = [order['id'] for order in self.get_own_orders] + order_ids = [order['id'] for order in self.own_orders] if order_ids: orders_balance = self.get_allocated_assets(order_ids) quote += orders_balance['quote'] @@ -414,7 +414,7 @@ def get_own_buy_orders(self, orders=None): """ if not orders: # List of orders was not given so fetch everything from the market - orders = self.get_own_orders + orders = self.own_orders return self.filter_buy_orders(orders) @@ -425,7 +425,7 @@ def get_own_sell_orders(self, orders=None): """ if not orders: # List of orders was not given so fetch everything from the market - orders = self.get_own_orders + orders = self.own_orders return self.filter_sell_orders(orders) @@ -700,40 +700,52 @@ def balances(self): """ return self._account.balances - @property - def all_own_orders(self, refresh=True): - """ Return the worker's open orders in all markets + def get_own_orders(self, refresh=True): + """ Return the account's open orders in the current market - :param bool | refresh: Use most recent data + :param bool refresh: Use most recent data :return: List of Order objects """ + orders = [] + # Refresh account data if refresh: self.account.refresh() - orders = [] for order in self.account.openorders: - orders.append(order) + if self.worker["market"] == order.market and self.account.openorders: + orders.append(order) return orders - @property - def get_own_orders(self): - """ Return the account's open orders in the current market + def get_all_own_orders(self, refresh=True): + """ Return the worker's open orders in all markets + :param bool refresh: Use most recent data :return: List of Order objects """ - orders = [] - # Refresh account data - self.account.refresh() + if refresh: + self.account.refresh() + orders = [] for order in self.account.openorders: - if self.worker["market"] == order.market and self.account.openorders: - orders.append(order) + orders.append(order) return orders + @property + def all_own_orders(self): + """ Return the worker's open orders in all markets + """ + return self.get_all_own_orders() + + @property + def own_orders(self): + """ Return the account's open orders in the current market + """ + return self.get_own_orders() + @property def market(self): # TODO: property, also in price feed, need to consider inheritance priority diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index e977c4804..629e5b618 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -381,7 +381,7 @@ def refresh_balances(self, total_balances=True, use_cached_orders=False): if use_cached_orders and self.cached_orders: orders = self.cached_orders else: - orders = self.get_own_orders + orders = self.own_orders order_ids = [order['id'] for order in orders] orders_balance = self.get_allocated_assets(order_ids) @@ -392,7 +392,7 @@ def refresh_balances(self, total_balances=True, use_cached_orders=False): def refresh_orders(self): """ Updates buy and sell orders """ - orders = self.get_own_orders + orders = self.own_orders self.cached_orders = orders # Sort virtual orders From 3c24f568d49de91a2eebdeff243f0622fdc55319 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 10 May 2019 14:48:43 +0300 Subject: [PATCH 1419/1846] Change dexbot version number to 0.11.3 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 5e981b81c..384ff4adb 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.11.2' +VERSION = '0.11.3' AUTHOR = 'Codaone Oy' __version__ = VERSION From b6f2e2f5706cf9693c83b3fcba49781bf1298f32 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 10 May 2019 15:02:36 +0300 Subject: [PATCH 1420/1846] Change dexbot version number to 0.11.4 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 384ff4adb..ca889a739 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.11.3' +VERSION = '0.11.4' AUTHOR = 'Codaone Oy' __version__ = VERSION From 51093868f85fc11b951002cef7b3e3f8cd5f8a75 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 29 Apr 2019 00:22:44 +0500 Subject: [PATCH 1421/1846] Add example logging.ini May be used in case someone needs to log RPC exchange on the node side. --- tests/node_config/logging.ini | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tests/node_config/logging.ini diff --git a/tests/node_config/logging.ini b/tests/node_config/logging.ini new file mode 100644 index 000000000..1aedcc1e5 --- /dev/null +++ b/tests/node_config/logging.ini @@ -0,0 +1,20 @@ +[log.console_appender.stderr] +stream=std_error + +#[log.file_appender.p2p] +#filename=logs/p2p/p2p.log + +[logger.default] +level=debug +appenders=stderr + +#[logger.p2p] +#level=debug +#appenders=stderr +# + + +#[logger.rpc] +#level=debug +#appenders=stderr +# From 799549ed1cfdcc17fb94a4225fd97e22c74ec515 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 9 May 2019 19:25:03 +0500 Subject: [PATCH 1422/1846] Add check for correct asset symbol --- tests/test_prepared_testnet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_prepared_testnet.py b/tests/test_prepared_testnet.py index b27bf2adf..51912f0f5 100644 --- a/tests/test_prepared_testnet.py +++ b/tests/test_prepared_testnet.py @@ -26,8 +26,10 @@ def test_worker_balance(bitshares, accounts): def test_asset_base(bitshares, assets): a = Asset('MYBASE', full=True, bitshares_instance=bitshares) assert a['dynamic_asset_data']['current_supply'] > 1000 + assert a.symbol == 'MYBASE' def test_asset_quote(bitshares, assets): a = Asset('MYQUOTE', full=True, bitshares_instance=bitshares) assert a['dynamic_asset_data']['current_supply'] > 1000 + assert a.symbol == 'MYQUOTE' From e283a695caa726fca3c7c40e23842cffa52fa7d2 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 18 May 2019 21:45:46 +0500 Subject: [PATCH 1423/1846] Add initial tests for Staggered Orders --- tests/conftest.py | 4 + tests/strategies/staggered_orders/conftest.py | 389 ++++++ .../staggered_orders/test_pybitshares.py | 8 + .../test_staggered_orders_complex.py | 1044 +++++++++++++++++ .../test_staggered_orders_highlevel.py | 556 +++++++++ .../test_staggered_orders_init.py | 36 + .../test_staggered_orders_lowlevel.py | 44 + .../test_staggered_orders_unittests.py | 44 + 8 files changed, 2125 insertions(+) create mode 100644 tests/strategies/staggered_orders/conftest.py create mode 100644 tests/strategies/staggered_orders/test_pybitshares.py create mode 100644 tests/strategies/staggered_orders/test_staggered_orders_complex.py create mode 100644 tests/strategies/staggered_orders/test_staggered_orders_highlevel.py create mode 100644 tests/strategies/staggered_orders/test_staggered_orders_init.py create mode 100644 tests/strategies/staggered_orders/test_staggered_orders_lowlevel.py create mode 100644 tests/strategies/staggered_orders/test_staggered_orders_unittests.py diff --git a/tests/conftest.py b/tests/conftest.py index 96ad1d99b..e7148418b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -123,6 +123,10 @@ def _create_asset(asset, precision): @pytest.fixture(scope='session') def issue_asset(bitshares): """ Issue asset shares to specified account + + :param str asset: asset symbol to issue + :param float amount: amount to issue + :param str to: account name to receive new shares """ def _issue_asset(asset, amount, to): diff --git a/tests/strategies/staggered_orders/conftest.py b/tests/strategies/staggered_orders/conftest.py new file mode 100644 index 000000000..4066d73f8 --- /dev/null +++ b/tests/strategies/staggered_orders/conftest.py @@ -0,0 +1,389 @@ +import pytest +import copy +import time +import tempfile +import os +import logging + +from bitshares.amount import Amount + +from dexbot.strategies.staggered_orders import Strategy + +log = logging.getLogger("dexbot") + +MODES = ['mountain', 'valley', 'neutral', 'buy_slope', 'sell_slope'] + + +@pytest.fixture(scope='session') +def assets(create_asset): + """ Create some assets with different precision + """ + create_asset('BASEA', 3) + create_asset('QUOTEA', 8) + create_asset('BASEB', 8) + create_asset('QUOTEB', 3) + + +@pytest.fixture(scope='module') +def base_account(assets, prepare_account): + """ Factory to generate random account with pre-defined balances + """ + + def func(): + account = prepare_account({'BASEA': 10000, 'QUOTEA': 100, 'BASEB': 10000, 'QUOTEB': 100, 'TEST': 1000}) + return account + + return func + + +@pytest.fixture +def account(base_account): + """ Prepare worker account with some balance + """ + return base_account() + + +@pytest.fixture +def account_only_base(assets, prepare_account): + """ Prepare worker account with only BASE assets balance + """ + account = prepare_account({'BASEA': 1000, 'BASEB': 1000, 'TEST': 1000}) + return account + + +@pytest.fixture +def account_1_sat(assets, prepare_account): + """ Prepare worker account to simulate XXX/BTC trading near zero prices + """ + account = prepare_account({'BASEB': 0.02, 'QUOTEB': 10000000, 'TEST': 1000}) + return account + + +@pytest.fixture(scope='session') +def so_worker_name(): + """ Fixture to share Staggered Orders worker name + """ + return 'so-worker' + + +@pytest.fixture(params=[('QUOTEA', 'BASEA'), ('QUOTEB', 'BASEB')]) +def config(request, bitshares, account, so_worker_name): + """ Define worker's config with variable assets + + This fixture should be function-scoped to use new fresh bitshares account for each test + """ + worker_name = so_worker_name + config = { + 'node': '{}'.format(bitshares.rpc.url), + 'workers': { + worker_name: { + 'account': '{}'.format(account), + 'market': '{}/{}'.format(request.param[0], request.param[1]), + 'module': 'dexbot.strategies.staggered_orders', + 'mode': 'valley', + 'center_price': 100.0, + 'center_price_dynamic': False, + 'fee_asset': 'TEST', + 'lower_bound': 90.0, + 'spread': 2.0, + 'increment': 1.0, + 'upper_bound': 110.0, + 'operational_depth': 10, + } + }, + } + return config + + +@pytest.fixture(params=MODES) +def config_variable_modes(request, config, so_worker_name): + """ Test config which tests all modes + """ + worker_name = so_worker_name + config = copy.deepcopy(config) + config['workers'][worker_name]['mode'] = request.param + return config + + +@pytest.fixture +def config_only_base(config, so_worker_name, account_only_base): + """ Config which uses an account with only BASE asset + """ + worker_name = so_worker_name + config = copy.deepcopy(config) + config['workers'][worker_name]['account'] = account_only_base + return config + + +@pytest.fixture +def config_1_sat(so_worker_name, bitshares, account_1_sat): + """ Config to set up a worker on market with center price around 1 sats + """ + worker_name = so_worker_name + config = { + 'node': '{}'.format(bitshares.rpc.url), + 'workers': { + worker_name: { + 'account': '{}'.format(account_1_sat), + 'market': 'QUOTEB/BASEB', + 'module': 'dexbot.strategies.staggered_orders', + 'mode': 'valley', + 'center_price': 0.00000001, + 'center_price_dynamic': False, + 'fee_asset': 'TEST', + 'lower_bound': 0.000000002, + 'spread': 30.0, + 'increment': 10.0, + 'upper_bound': 0.00000002, + 'operational_depth': 10, + } + }, + } + return config + + +@pytest.fixture +def base_worker(bitshares, so_worker_name, storage_db): + worker_name = so_worker_name + workers = [] + + def _base_worker(config): + worker = Strategy(config=config, name=worker_name, bitshares_instance=bitshares) + # Set market center price to avoid calling of maintain_strategy() + worker.market_center_price = worker.worker['center_price'] + log.info('Initialized {} on account {}'.format(worker_name, worker.account.name)) + workers.append(worker) + return worker + + yield _base_worker + + # We need to make sure no orders left after test finished + for worker in workers: + worker.cancel_all_orders() + worker.bitshares.txbuffer.clear() + worker.bitshares.bundle = False + + +@pytest.fixture(scope='session') +def storage_db(): + """ Prepare custom sqlite database to not mess with main one + """ + from dexbot.storage import sqlDataBaseFile + + fd, sqlDataBaseFile = tempfile.mkstemp() # noqa: F811 + yield + os.unlink(sqlDataBaseFile) + + +@pytest.fixture +def worker(base_worker, config): + """ Worker to test in single mode (for methods which not required to be tested against all modes) + """ + worker = base_worker(config) + return worker + + +@pytest.fixture +def worker2(base_worker, config_variable_modes): + """ Worker to test all modes + """ + worker = base_worker(config_variable_modes) + return worker + + +@pytest.fixture +def init_empty_balances(worker, bitshares): + # Defaults are None, which breaks place_virtual_xxx_order() + worker.quote_balance = Amount(0, worker.market['quote']['symbol'], bitshares_instance=bitshares) + worker.base_balance = Amount(0, worker.market['base']['symbol'], bitshares_instance=bitshares) + + +@pytest.fixture +def orders1(worker, bitshares, init_empty_balances): + """ Place 1 buy+sell real order, and 1 buy+sell virtual orders with prices outside of the range. + + Note: this fixture don't calls refresh.xxx() intentionally! + """ + # Make sure there are no orders + worker.cancel_all_orders() + # Prices outside of the range + buy_price = 1 # price for test_refresh_balances() + sell_price = worker.upper_bound + 1 + # Place real orders + worker.place_market_buy_order(10, buy_price) + worker.place_market_sell_order(10, sell_price) + # Place virtual orders + worker.place_virtual_buy_order(10, buy_price) + worker.place_virtual_sell_order(10, sell_price) + yield worker + # Remove orders on teardown + worker.cancel_all_orders() + worker.virtual_orders = [] + # Need to wait until trxs will be included into block because several consequent runs of tests which uses this + # fixture will cause identical cancel trxs, which is not allowed by the node + time.sleep(1.1) + + +@pytest.fixture +def orders2(worker): + """ Place buy+sell real orders near center price + """ + worker.cancel_all_orders() + buy_price = worker.market_center_price - 1 + sell_price = worker.market_center_price + 1 + # Place real orders + worker.place_market_buy_order(1, buy_price) + worker.place_market_sell_order(1, sell_price) + worker.refresh_orders() + worker.refresh_balances() + yield worker + worker.cancel_all_orders() + worker.virtual_orders = [] + time.sleep(1.1) + + +@pytest.fixture +def orders3(worker): + """ Place buy+sell virtual orders near center price + """ + worker.cancel_all_orders() + worker.refresh_balances() + buy_price = worker.market_center_price - 1 + sell_price = worker.market_center_price + 1 + # Place virtual orders + worker.place_virtual_buy_order(1, buy_price) + worker.place_virtual_sell_order(1, sell_price) + worker.refresh_orders() + yield worker + worker.virtual_orders = [] + + +@pytest.fixture +def orders4(worker, orders1): + """ Just wrap orders1, but refresh balances in addition + """ + worker.refresh_balances() + yield orders1 + + +@pytest.fixture +def orders5(worker2): + """ Place buy+sell virtual orders at some distance from center price, and + buy+sell real orders at 1 order distance from center + """ + worker = worker2 + + worker.cancel_all_orders() + worker.refresh_balances() + + # Virtual orders outside of operational depth + buy_price = worker.market_center_price / (1 + worker.increment) ** (worker.operational_depth * 2) + sell_price = worker.market_center_price * (1 + worker.increment) ** (worker.operational_depth * 2) + worker.place_virtual_buy_order(1, buy_price) + worker.place_virtual_sell_order(1, sell_price) + + # Virtual orders within operational depth + buy_price = worker.market_center_price / (1 + worker.increment) ** (worker.operational_depth // 2) + sell_price = worker.market_center_price * (1 + worker.increment) ** (worker.operational_depth // 2) + worker.place_virtual_buy_order(1, buy_price) + worker.place_virtual_sell_order(1, sell_price) + + # Real orders outside of operational depth + buy_price = worker.market_center_price / (1 + worker.increment) ** (worker.operational_depth + 2) + sell_price = worker.market_center_price * (1 + worker.increment) ** (worker.operational_depth + 2) + worker.place_market_buy_order(1, buy_price) + worker.place_market_sell_order(1, sell_price) + + # Real orders at 2 increment distance from the center + buy_price = worker.market_center_price / (1 + worker.increment) ** 2 + sell_price = worker.market_center_price * (1 + worker.increment) ** 2 + worker.place_market_buy_order(1, buy_price) + worker.place_market_sell_order(1, sell_price) + + worker.refresh_orders() + yield worker + worker.virtual_orders = [] + worker.cancel_all_orders() + time.sleep(1.1) + + +@pytest.fixture +def partially_filled_order(worker): + """ Create partially filled order + """ + worker.cancel_all_orders() + order = worker.place_market_buy_order(100, 1, returnOrderId=True) + worker.place_market_sell_order(20, 1) + worker.refresh_balances() + # refresh order + order = worker.get_order(order) + yield order + worker.cancel_all_orders() + time.sleep(1.1) + + +@pytest.fixture(scope='session') +def increase_until_allocated(): + """ Run increase_order_sizes() until funds are allocated + + :param Strategy worker: worker instance + """ + + def func(worker): + buy_increased = False + sell_increased = False + + while not buy_increased or not sell_increased: + worker.refresh_orders() + worker.refresh_balances(use_cached_orders=True) + buy_increased = worker.increase_order_sizes('base', worker.base_balance, worker.buy_orders) + sell_increased = worker.increase_order_sizes('quote', worker.quote_balance, worker.sell_orders) + worker.refresh_orders() + log.info('Increase done') + + return func + + +@pytest.fixture(scope='session') +def maintain_until_allocated(): + """ Run maintain_strategy() on a specific worker until funds are allocated + + :param Strategy worker: worker instance + """ + + def func(worker): + # Speed up a little + worker.min_check_interval = 0.01 + worker.current_check_interval = worker.min_check_interval + while True: + worker.maintain_strategy() + if not worker.current_check_interval == worker.min_check_interval: + # Use "if" statement instead of putting this into a "while" to avoid waiting max_check_interval on last + # run + break + time.sleep(worker.min_check_interval) + log.info('Allocation done') + + return func + + +@pytest.fixture +def do_initial_allocation(maintain_until_allocated): + """ Run maintain_strategy() to make an initial allocation of funds + + :param Strategy worker: initialized worker + :param str mode: SO mode (valley, mountain etc) + """ + + def func(worker, mode): + worker.mode = mode + worker.cancel_all_orders() + maintain_until_allocated(worker) + worker.refresh_orders() + worker.refresh_balances(use_cached_orders=True) + worker.current_check_interval = 0 + log.info('Initial allocation done') + + return worker + + return func diff --git a/tests/strategies/staggered_orders/test_pybitshares.py b/tests/strategies/staggered_orders/test_pybitshares.py new file mode 100644 index 000000000..4f5a54eab --- /dev/null +++ b/tests/strategies/staggered_orders/test_pybitshares.py @@ -0,0 +1,8 @@ +def test_correct_asset_names(orders1): + """ Test for https://github.com/bitshares/python-bitshares/issues/239 + """ + worker = orders1 + worker.account.refresh() + orders = worker.account.openorders + symbols = ['BASEA', 'BASEB', 'QUOTEA', 'QUOTEB'] + assert orders[0]['base']['asset']['symbol'] in symbols diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py new file mode 100644 index 000000000..81d862d90 --- /dev/null +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -0,0 +1,1044 @@ +import logging +import pytest +import math + +from datetime import datetime +from bitshares.account import Account +from bitshares.amount import Amount + +# Turn on debug for dexbot logger +log = logging.getLogger("dexbot") +log.setLevel(logging.DEBUG) + +MODES = ['mountain', 'valley', 'neutral', 'buy_slope', 'sell_slope'] + + +def get_spread(worker): + """ Get actual spread on SO worker + + :param Strategy worker: an active worker instance + """ + if worker.buy_orders: + highest_buy_price = worker.buy_orders[0].get('price') + else: + return float('Inf') + + if worker.sell_orders: + lowest_sell_price = worker.sell_orders[0].get('price') + # Invert the sell price to BASE so it can be used in comparison + lowest_sell_price = lowest_sell_price ** -1 + else: + return float('Inf') + + return (lowest_sell_price / highest_buy_price) - 1 + + +################### +# Most complex methods which depends on high-level methods +################### + + +def test_maintain_strategy_manual_cp_empty_market(worker): + """ On empty market, center price should be set to manual CP + """ + worker.cancel_all_orders() + # Undefine market_center_price + worker.market_center_price = None + # Workaround for https://github.com/Codaone/DEXBot/issues/566 + worker.last_check = datetime(2000, 1, 1) + worker.maintain_strategy() + assert worker.market_center_price == worker.center_price + + +def test_maintain_strategy_no_manual_cp_empty_market(worker): + """ Strategy should not work on empty market if no manual CP was set + """ + worker.cancel_all_orders() + # Undefine market_center_price + worker.market_center_price = None + worker.center_price = None + # Workaround for https://github.com/Codaone/DEXBot/issues/566 + worker.last_check = datetime(2000, 1, 1) + worker.maintain_strategy() + assert worker.market_center_price is None + + +@pytest.mark.xfail(reason='https://github.com/Codaone/DEXBot/issues/575') +@pytest.mark.parametrize('mode', MODES) +def test_maintain_strategy_basic(mode, worker, do_initial_allocation): + """ Check if intial orders placement is correct + """ + worker = do_initial_allocation(worker, mode) + + # Check target spread is reached + assert worker.actual_spread < worker.target_spread + worker.increment + + # Check number of orders + price = worker.center_price * math.sqrt(1 + worker.target_spread) + sell_orders_count = worker.calc_sell_orders_count(price, worker.upper_bound) + assert len(worker.sell_orders) == sell_orders_count + + price = worker.center_price / math.sqrt(1 + worker.target_spread) + buy_orders_count = worker.calc_buy_orders_count(price, worker.lower_bound) + assert len(worker.buy_orders) == buy_orders_count + + # Make sure balances are allocated after full maintenance + # Unallocated balances are less than closest order amount + assert worker.base_balance['amount'] < worker.buy_orders[0]['base']['amount'] + assert worker.quote_balance['amount'] < worker.sell_orders[0]['base']['amount'] + + # Test how ranges are covered + # Expect furthest order price to be less than increment x2 + assert worker.buy_orders[-1]['price'] < worker.lower_bound * (1 + worker.increment * 2) + assert worker.sell_orders[-1]['price'] ** -1 > worker.upper_bound / (1 + worker.increment * 2) + + +@pytest.mark.xfail(reason='https://github.com/Codaone/DEXBot/issues/575') +@pytest.mark.parametrize('mode', MODES) +def test_maintain_strategy_one_sided(mode, base_worker, config_only_base, do_initial_allocation): + """ Test for one-sided start (buy only) + """ + worker = base_worker(config_only_base) + do_initial_allocation(worker, mode) + + # Check target spread is reached + assert worker.actual_spread < worker.target_spread + worker.increment + + # Check number of orders + price = worker.center_price / math.sqrt(1 + worker.target_spread) + buy_orders_count = worker.calc_buy_orders_count(price, worker.lower_bound) + assert len(worker.buy_orders) == buy_orders_count + + # Make sure balances are allocated after full maintenance + # Unallocated balances are less than closest order amount + assert worker.base_balance['amount'] < worker.buy_orders[0]['base']['amount'] + + # Test how ranges are covered + # Expect furthest order price to be less than increment x2 + assert worker.buy_orders[-1]['price'] < worker.lower_bound * (1 + worker.increment * 2) + + +@pytest.mark.xfail(reason='https://github.com/Codaone/DEXBot/issues/575') +def test_maintain_strategy_1sat(base_worker, config_1_sat, do_initial_allocation): + worker = base_worker(config_1_sat) + do_initial_allocation(worker, worker.mode) + + # Check target spread is reached + assert worker.actual_spread < worker.target_spread + worker.increment + + # Check number of orders + price = worker.center_price * math.sqrt(1 + worker.target_spread) + sell_orders_count = worker.calc_sell_orders_count(price, worker.upper_bound) + assert len(worker.sell_orders) == sell_orders_count + + price = worker.center_price / math.sqrt(1 + worker.target_spread) + buy_orders_count = worker.calc_buy_orders_count(price, worker.lower_bound) + assert len(worker.buy_orders) == buy_orders_count + + # Make sure balances are allocated after full maintenance + # Unallocated balances are less than closest order amount + assert worker.base_balance['amount'] < worker.buy_orders[0]['base']['amount'] + assert worker.quote_balance['amount'] < worker.sell_orders[0]['base']['amount'] + + # Test how ranges are covered + # Expect furthest order price to be less than increment x2 + assert worker.buy_orders[-1]['price'] < worker.lower_bound * (1 + worker.increment * 2) + assert worker.sell_orders[-1]['price'] ** -1 > worker.upper_bound / (1 + worker.increment * 2) + + +# Combine each mode with base and quote +@pytest.mark.parametrize('asset', ['base', 'quote']) +@pytest.mark.parametrize('mode', MODES) +def test_maintain_strategy_fallback_logic(asset, mode, worker, do_initial_allocation): + """ Check fallback logic: when spread is not reached, furthest order should be cancelled to make free funds to + close spread + """ + do_initial_allocation(worker, worker.mode) + # TODO: strategy must turn off bootstrapping once target spread is reached + worker.bootstrapping = False + + if asset == 'base': + worker.cancel_orders_wrapper(worker.buy_orders[0]) + amount = worker.balance(worker.market['base']['symbol']) + worker.bitshares.reserve(amount, account=worker.account) + elif asset == 'quote': + worker.cancel_orders_wrapper(worker.sell_orders[0]) + amount = worker.balance(worker.market['quote']['symbol']) + worker.bitshares.reserve(amount, account=worker.account) + + worker.refresh_orders() + spread_before = get_spread(worker) + assert spread_before > worker.target_spread + worker.increment + + for i in range(0, 6): + worker.maintain_strategy() + + worker.refresh_orders() + spread_after = get_spread(worker) + assert spread_after <= worker.target_spread + worker.increment + + +def test_increase_order_sizes_valley_basic(worker, do_initial_allocation, issue_asset, increase_until_allocated): + """ Test increases in valley mode when all orders are equal (new allocation round). + """ + do_initial_allocation(worker, 'valley') + # Double worker's balance + issue_asset(worker.market['base']['symbol'], worker.base_total_balance, worker.account.name) + issue_asset(worker.market['quote']['symbol'], worker.quote_total_balance, worker.account.name) + + increase_until_allocated(worker) + + # All orders must be equal-sized + for order in worker.buy_orders: + assert order['base']['amount'] == worker.buy_orders[0]['base']['amount'] + for order in worker.sell_orders: + assert order['base']['amount'] == worker.sell_orders[0]['base']['amount'] + + +def test_increase_order_sizes_valley_direction(worker, do_initial_allocation, issue_asset, increase_until_allocated): + """ Test increase direction in valley mode: new allocation round must be started from closest order. + + Buy side, amounts in BASE: + + 100 100 100 100 100 + 100 100 100 100 115 + 100 100 100 115 115 + 100 100 115 115 115 + """ + do_initial_allocation(worker, 'valley') + + # Add balance to increase several orders + increase_factor = max(1 + worker.increment, worker.min_increase_factor) + to_issue = worker.buy_orders[0]['base']['amount'] * (increase_factor - 1) * 3 + issue_asset(worker.market['base']['symbol'], to_issue, worker.account.name) + to_issue = worker.sell_orders[0]['base']['amount'] * (increase_factor - 1) * 3 + issue_asset(worker.market['quote']['symbol'], to_issue, worker.account.name) + + increase_until_allocated(worker) + + for order in worker.buy_orders: + assert order['base']['amount'] <= worker.buy_orders[0]['base']['amount'] + for order in worker.sell_orders: + assert order['base']['amount'] <= worker.sell_orders[0]['base']['amount'] + + +def test_increase_order_sizes_valley_transit_from_mountain(worker, do_initial_allocation, issue_asset): + """ Transition from mountain to valley + + Buy side, amounts in BASE, increase should be like this: + + 70 80 90 100 + 80 80 90 100 + 80 90 90 100 + 90 90 90 100 + """ + # Set up mountain + do_initial_allocation(worker, 'mountain') + # Switch to valley + worker.mode = 'valley' + # Add balance to increase several orders + to_issue = worker.buy_orders[0]['base']['amount'] * 10 + issue_asset(worker.market['base']['symbol'], to_issue, worker.account.name) + + for _ in range(0, 6): + previous_buy_orders = worker.buy_orders + worker.refresh_balances() + worker.increase_order_sizes('base', worker.base_balance, previous_buy_orders) + worker.refresh_orders() + + for i in range(-1, -6, -1): + if ( + previous_buy_orders[i]['base']['amount'] < previous_buy_orders[i - 1]['base']['amount'] + and previous_buy_orders[i - 1]['base']['amount'] - previous_buy_orders[i]['base']['amount'] + > previous_buy_orders[i]['base']['amount'] * worker.increment / 2 + ): + # Expect increased order if closer order is bigger than further + assert worker.buy_orders[i]['base']['amount'] > previous_buy_orders[i]['base']['amount'] + # Only one check at a time + break + + +def test_increase_order_sizes_valley_smaller_closest_orders(worker, do_initial_allocation, increase_until_allocated): + """ Test increase when closest-to-center orders are less than further orders. Normal situation when initial sides + are imbalanced and several orders were filled. + + Buy side, amounts in BASE: + + 100 100 100 10 10 10
+ """ + worker = do_initial_allocation(worker, 'valley') + increase_until_allocated(worker) + + # Cancel several closest orders + num_orders_to_cancel = 3 + num_orders_before = len(worker.own_orders) + worker.cancel_orders_wrapper(worker.buy_orders[:num_orders_to_cancel]) + worker.cancel_orders_wrapper(worker.sell_orders[:num_orders_to_cancel]) + worker.refresh_orders() + worker.refresh_balances() + + # Place limited orders + initial_base = worker.buy_orders[0]['base']['amount'] + initial_quote = worker.sell_orders[0]['base']['amount'] + base_limit = initial_base / 2 + quote_limit = initial_quote / 2 + for _ in range(0, num_orders_to_cancel): + worker.place_closer_order('base', worker.buy_orders[0], own_asset_limit=base_limit) + worker.place_closer_order('quote', worker.sell_orders[0], own_asset_limit=quote_limit) + worker.refresh_orders() + + increase_until_allocated(worker) + + # Number of orders should be the same + num_orders_after = len(worker.own_orders) + assert num_orders_before == num_orders_after + + # New closest orders amount should be equal to initial ones + assert worker.buy_orders[0]['base']['amount'] == initial_base + assert worker.sell_orders[0]['base']['amount'] == initial_quote + + +@pytest.mark.xfail(reason='https://github.com/Codaone/DEXBot/issues/444') +def test_increase_order_sizes_valley_imbalaced_small_further(worker, do_initial_allocation, increase_until_allocated): + """ + TODO: test stub for https://github.com/Codaone/DEXBot/pull/484 + + Buy side, amounts in BASE: + + 5 5 5 100 100 10 10 10
+ + Should be: + + 10 10 10 100 100 10 10 10
+ """ + worker = do_initial_allocation(worker, 'valley') + + # Cancel several closest orders + num_orders_to_cancel = 3 + worker.cancel_orders_wrapper(worker.buy_orders[:num_orders_to_cancel]) + # Cancel furthest orders + worker.cancel_orders_wrapper(worker.buy_orders[-num_orders_to_cancel:]) + worker.refresh_orders() + worker.refresh_balances() + + # Place limited orders + initial_base = worker.buy_orders[0]['base']['amount'] + base_limit = initial_base / 2 + for i in range(0, num_orders_to_cancel): + # Place smaller closer order + worker.place_closer_order('base', worker.buy_orders[0], own_asset_limit=base_limit) + # place_further_order() doesn't have own_asset_limit, so do own calculation + further_order = worker.place_further_order('base', worker.buy_orders[-1], place_order=False) + # Place smaller further order + to_buy = base_limit / further_order['price'] + worker.place_market_buy_order(to_buy, further_order['price']) + worker.refresh_orders() + + # Drop excess balance, the goal is to keep balance to only increase furthest orders + amount = Amount( + base_limit * num_orders_to_cancel, worker.market['base']['symbol'], bitshares_instance=worker.bitshares + ) + worker.bitshares.reserve(amount, account=worker.account) + + increase_until_allocated(worker) + + for i in range(1, num_orders_to_cancel): + assert worker.buy_orders[-i]['base']['amount'] == worker.buy_orders[i - 1]['base']['amount'] + + +@pytest.mark.xfail(reason='https://github.com/Codaone/DEXBot/issues/586') +def test_increase_order_sizes_valley_closest_order(worker, do_initial_allocation, issue_asset): + """ Should test proper calculation of closest order: order should not be less that min_increase_factor + """ + worker = do_initial_allocation(worker, 'valley') + + # Add balance to increase 2 orders + increase_factor = max(1 + worker.increment, worker.min_increase_factor) + to_issue = worker.buy_orders[0]['base']['amount'] * (increase_factor - 1) * 2 + issue_asset(worker.market['base']['symbol'], to_issue, worker.account.name) + + previous_buy_orders = worker.buy_orders + worker.refresh_balances() + worker.increase_order_sizes('base', worker.base_balance, previous_buy_orders) + worker.refresh_orders() + + assert worker.buy_orders[0]['base']['amount'] - previous_buy_orders[0]['base']['amount'] >= previous_buy_orders[0][ + 'base' + ]['amount'] * (increase_factor - 1) + + +def test_increase_order_sizes_mountain_basic(worker, do_initial_allocation, issue_asset, increase_until_allocated): + """ Test increases in mountain mode when all orders are equal (new allocation round). New orders should be equal in + their "quote" + """ + do_initial_allocation(worker, 'mountain') + increase_until_allocated(worker) + + # Double worker's balance + issue_asset(worker.market['base']['symbol'], worker.base_total_balance, worker.account.name) + issue_asset(worker.market['quote']['symbol'], worker.quote_total_balance, worker.account.name) + + increase_until_allocated(worker) + + # All orders must be equal-sized in their quote, accept slight error + for order in worker.buy_orders: + assert order['quote']['amount'] == pytest.approx( + worker.buy_orders[0]['quote']['amount'], rel=(1 ** -worker.market['quote']['precision']) + ) + for order in worker.sell_orders: + assert order['quote']['amount'] == pytest.approx( + worker.sell_orders[0]['quote']['amount'], rel=(1 ** -worker.market['base']['precision']) + ) + + +def test_increase_order_sizes_mountain_direction(worker, do_initial_allocation, issue_asset): + """ Test increase direction in mountain mode + + Buy side, amounts in QUOTE: + + 10 10 10 10 10 + 15 10 10 10 10 + 15 15 10 10 10 + 15 15 15 10 10 + """ + do_initial_allocation(worker, 'mountain') + worker.mode = 'mountain' + + # Double worker's balance + issue_asset(worker.market['base']['symbol'], worker.base_total_balance, worker.account.name) + + for _ in range(0, 6): + previous_buy_orders = worker.buy_orders + worker.refresh_balances() + worker.increase_order_sizes('base', worker.base_balance, previous_buy_orders) + worker.refresh_orders() + + for i in range(-1, -6, -1): + if ( + previous_buy_orders[i]['quote']['amount'] > previous_buy_orders[i - 1]['quote']['amount'] + and previous_buy_orders[i]['quote']['amount'] - previous_buy_orders[i - 1]['quote']['amount'] + > previous_buy_orders[i - 1]['quote']['amount'] * worker.increment / 2 + ): + # Expect increased order if further order is bigger than closer + assert worker.buy_orders[i - 1]['quote']['amount'] > previous_buy_orders[i - 1]['quote']['amount'] + # Only one check at a time + break + + +@pytest.mark.xfail(reason='https://github.com/Codaone/DEXBot/issues/585') +def test_increase_order_sizes_mountain_furthest_order(worker, do_initial_allocation, issue_asset): + """ Should test proper calculation of furthest order: try to maximize, don't allow too small increase + """ + do_initial_allocation(worker, 'mountain') + worker.mode = 'mountain' + + # Add balance to increase 2 orders + increase_factor = max(1 + worker.increment, worker.min_increase_factor) + to_issue = worker.buy_orders[0]['base']['amount'] * (increase_factor - 1) * 2 + issue_asset(worker.market['base']['symbol'], to_issue, worker.account.name) + + previous_buy_orders = worker.buy_orders + worker.refresh_balances() + worker.increase_order_sizes('base', worker.base_balance, previous_buy_orders) + worker.refresh_orders() + + assert worker.buy_orders[-1]['base']['amount'] - previous_buy_orders[-1]['base']['amount'] >= previous_buy_orders[ + -1 + ]['base']['amount'] * (increase_factor - 1) + + +def test_increase_order_sizes_mountain_imbalanced(worker, do_initial_allocation): + """ Test situation when sides was imbalances, several orders filled on opposite side. + This also tests transition from vally to mountain. + + Buy side, amounts in QUOTE: + + 100 100 100 10 10 10 + 100 100 100 20 10 10 + 100 100 100 20 20 10 + """ + do_initial_allocation(worker, 'mountain') + worker.mode = 'mountain' + + # Cancel several closest orders + num_orders_to_cancel = 3 + worker.cancel_orders_wrapper(worker.buy_orders[:num_orders_to_cancel]) + worker.refresh_orders() + worker.refresh_balances() + + # Place limited orders + initial_base = worker.buy_orders[0]['base']['amount'] + base_limit = initial_base / 2 + # Add own_asset_limit only for first new order + worker.place_closer_order('base', worker.buy_orders[0], own_asset_limit=base_limit) + for _ in range(1, num_orders_to_cancel): + worker.place_closer_order('base', worker.buy_orders[0]) + worker.refresh_orders() + + for _ in range(0, num_orders_to_cancel): + previous_buy_orders = worker.buy_orders + worker.refresh_balances() + worker.increase_order_sizes('base', worker.base_balance, previous_buy_orders) + worker.refresh_orders() + + for order in worker.buy_orders: + order_index = worker.buy_orders.index(order) + + if ( + previous_buy_orders[order_index]['quote']['amount'] + < previous_buy_orders[order_index + 1]['quote']['amount'] + and previous_buy_orders[order_index + 1]['base']['amount'] + - previous_buy_orders[order_index]['base']['amount'] + > previous_buy_orders[order_index]['base']['amount'] * worker.increment / 2 + ): + # If order before increase was smaller than further order, expect to see it increased + assert order['quote']['amount'] > previous_buy_orders[order_index]['quote']['amount'] + break + + +def test_increase_order_sizes_neutral_basic(worker, do_initial_allocation, issue_asset, increase_until_allocated): + """ Test increases in neutral mode when all orders are equal (new allocation round) + """ + do_initial_allocation(worker, 'neutral') + increase_until_allocated(worker) + + # Double worker's balance + issue_asset(worker.market['base']['symbol'], worker.base_total_balance, worker.account.name) + issue_asset(worker.market['quote']['symbol'], worker.quote_total_balance, worker.account.name) + + increase_until_allocated(worker) + + for index, order in enumerate(worker.buy_orders): + if index == 0: + continue + # Assume amounts are equal within some tolerance + assert order['base']['amount'] == pytest.approx( + worker.buy_orders[index - 1]['base']['amount'] / math.sqrt(1 + worker.increment), + rel=(1 ** -worker.market['base']['precision']), + ) + for index, order in enumerate(worker.sell_orders): + if index == 0: + continue + assert order['base']['amount'] == pytest.approx( + worker.sell_orders[index - 1]['base']['amount'] / math.sqrt(1 + worker.increment), + rel=(1 ** -worker.market['quote']['precision']), + ) + + +def test_increase_order_sizes_neutral_direction(worker, do_initial_allocation, issue_asset, increase_until_allocated): + """ Test increase direction in neutral mode: new allocation round must be started from closest order. + + Buy side, amounts in BASE: + + 100 100 100 100 100 + 100 100 100 100 115 + 100 100 100 114 115 + 100 100 113 114 115 + """ + do_initial_allocation(worker, 'neutral') + + # Add balance to increase several orders + increase_factor = max(1 + worker.increment, worker.min_increase_factor) + to_issue = worker.buy_orders[0]['base']['amount'] * (increase_factor - 1) * 3 + issue_asset(worker.market['base']['symbol'], to_issue, worker.account.name) + to_issue = worker.sell_orders[0]['base']['amount'] * (increase_factor - 1) * 3 + issue_asset(worker.market['quote']['symbol'], to_issue, worker.account.name) + + increase_until_allocated(worker) + + for order in worker.buy_orders: + assert order['base']['amount'] <= worker.buy_orders[0]['base']['amount'] + for order in worker.sell_orders: + assert order['base']['amount'] <= worker.sell_orders[0]['base']['amount'] + + +def test_increase_order_sizes_neutral_transit_from_mountain(worker, do_initial_allocation, issue_asset): + """ Transition from mountain to neutral + + Buy side, amounts in BASE, increase should be like this: + + 70 80 90 100 + 80 80 90 100 + 80 90 90 100 + 90 90 90 100 + """ + # Set up mountain + do_initial_allocation(worker, 'mountain') + # Switch to neutral + worker.mode = 'neutral' + # Add balance to increase several orders + to_issue = worker.buy_orders[0]['base']['amount'] * 10 + issue_asset(worker.market['base']['symbol'], to_issue, worker.account.name) + + for _ in range(0, 6): + previous_buy_orders = worker.buy_orders + worker.refresh_balances() + worker.increase_order_sizes('base', worker.base_balance, previous_buy_orders) + worker.refresh_orders() + + for i in range(-1, -6, -1): + if ( + previous_buy_orders[i]['base']['amount'] < previous_buy_orders[i - 1]['base']['amount'] + and previous_buy_orders[i - 1]['base']['amount'] - previous_buy_orders[i]['base']['amount'] + > previous_buy_orders[i]['base']['amount'] * worker.increment / 2 + ): + # Expect increased order if closer order is bigger than further + assert worker.buy_orders[i]['base']['amount'] > previous_buy_orders[i]['base']['amount'] + # Only one check at a time + break + + +@pytest.mark.xfail(reason='Closest order failed to increase up to initial balance, fp/rounding issue') +def test_increase_order_sizes_neutral_smaller_closest_orders(worker, do_initial_allocation, increase_until_allocated): + """ Test increase when closest-to-center orders are less than further orders. Normal situation when initial sides + are imbalanced and several orders were filled. + + Buy side, amounts in BASE: + + 100 100 100 10 10 10
+ """ + worker = do_initial_allocation(worker, 'neutral') + increase_until_allocated(worker) + + initial_base = worker.buy_orders[0]['base']['amount'] + initial_quote = worker.sell_orders[0]['base']['amount'] + + # Cancel several closest orders + num_orders_to_cancel = 3 + worker.cancel_orders_wrapper(worker.buy_orders[:num_orders_to_cancel]) + worker.cancel_orders_wrapper(worker.sell_orders[:num_orders_to_cancel]) + worker.refresh_orders() + worker.refresh_balances() + + # Place limited orders + base_limit = initial_base / 2 + quote_limit = initial_quote / 2 + worker.place_closer_order('base', worker.buy_orders[0], own_asset_limit=base_limit) + worker.place_closer_order('quote', worker.sell_orders[0], own_asset_limit=quote_limit) + worker.refresh_orders() + for i in range(1, num_orders_to_cancel): + worker.place_closer_order('base', worker.buy_orders[0]) + worker.place_closer_order('quote', worker.sell_orders[0]) + worker.refresh_orders() + + increase_until_allocated(worker) + + # New closest orders amount should be equal to initial ones + assert worker.buy_orders[0]['base']['amount'] == initial_base + assert worker.sell_orders[0]['base']['amount'] == initial_quote + + +@pytest.mark.xfail(reason='https://github.com/Codaone/DEXBot/issues/444') +def test_increase_order_sizes_neutral_imbalaced_small_further(worker, do_initial_allocation, increase_until_allocated): + """ + TODO: test stub for https://github.com/Codaone/DEXBot/pull/484 + + Buy side, amounts in BASE: + + 5 5 5 100 100 10 10 10
+ + Should be: + + 10 10 10 100 100 10 10 10
+ """ + worker = do_initial_allocation(worker, 'neutral') + increase_until_allocated(worker) + + # Cancel several closest orders + num_orders_to_cancel = 3 + worker.cancel_orders_wrapper(worker.buy_orders[:num_orders_to_cancel]) + # Cancel furthest orders + worker.cancel_orders_wrapper(worker.buy_orders[-num_orders_to_cancel:]) + worker.refresh_orders() + worker.refresh_balances() + + # Place limited orders + initial_base = worker.buy_orders[0]['base']['amount'] + base_limit = initial_base / 2 + for i in range(0, num_orders_to_cancel): + worker.place_closer_order('base', worker.buy_orders[0], own_asset_limit=base_limit) + # place_further_order() doesn't have own_asset_limit, so do own calculation + further_order = worker.place_further_order('base', worker.buy_orders[-1], place_order=False) + worker.place_market_buy_order(base_limit / further_order['price'], further_order['price']) + worker.refresh_orders() + + # Drop excess balance, the goal is to keep balance to only increase furthest orders + amount = Amount( + base_limit * num_orders_to_cancel, worker.market['base']['symbol'], bitshares_instance=worker.bitshares + ) + worker.bitshares.reserve(amount, account=worker.account) + + increase_until_allocated(worker) + + for i in range(1, num_orders_to_cancel): + # TODO: this is a simple check without precise calculation + # We're roughly checking that new furthest orders are not exceed new closest orders + assert worker.buy_orders[-i]['base']['amount'] < worker.buy_orders[i - 1]['base']['amount'] + + +@pytest.mark.xfail(reason='https://github.com/Codaone/DEXBot/issues/586') +def test_increase_order_sizes_neutral_closest_order(worker, do_initial_allocation, issue_asset): + """ Should test proper calculation of closest order: order should not be less that min_increase_factor + """ + worker = do_initial_allocation(worker, 'neutral') + + # Add balance to increase 2 orders + increase_factor = max(1 + worker.increment, worker.min_increase_factor) + to_issue = worker.buy_orders[0]['base']['amount'] * (increase_factor - 1) * 2 + issue_asset(worker.market['base']['symbol'], to_issue, worker.account.name) + + previous_buy_orders = worker.buy_orders + worker.refresh_balances() + worker.increase_order_sizes('base', worker.base_balance, previous_buy_orders) + worker.refresh_orders() + + assert worker.buy_orders[0]['base']['amount'] - previous_buy_orders[0]['base']['amount'] >= previous_buy_orders[0][ + 'base' + ]['amount'] * (increase_factor - 1) + + +def test_increase_order_sizes_buy_slope(worker, do_initial_allocation, issue_asset, increase_until_allocated): + """ Check correct orders sizes on both sides + """ + do_initial_allocation(worker, 'buy_slope') + + # Double worker's balance + issue_asset(worker.market['base']['symbol'], worker.base_total_balance, worker.account.name) + issue_asset(worker.market['quote']['symbol'], worker.quote_total_balance, worker.account.name) + + increase_until_allocated(worker) + + for order in worker.buy_orders: + # All buy orders must be equal-sized in BASE + assert order['base']['amount'] == worker.buy_orders[0]['base']['amount'] + for order in worker.sell_orders: + # Sell orders are equal-sized in BASE asset + assert order['quote']['amount'] == pytest.approx( + worker.sell_orders[0]['quote']['amount'], rel=(1 ** -worker.market['base']['precision']) + ) + + +def test_increase_order_sizes_sell_slope(worker, do_initial_allocation, issue_asset, increase_until_allocated): + """ Check correct orders sizes on both sides + """ + do_initial_allocation(worker, 'sell_slope') + + # Double worker's balance + issue_asset(worker.market['base']['symbol'], worker.base_total_balance, worker.account.name) + issue_asset(worker.market['quote']['symbol'], worker.quote_total_balance, worker.account.name) + + increase_until_allocated(worker) + + for order in worker.buy_orders: + # All buy orders must be equal-sized in market QUOTE + assert order['quote']['amount'] == pytest.approx( + worker.buy_orders[0]['quote']['amount'], rel=(1 ** -worker.market['quote']['precision']) + ) + + for order in worker.sell_orders: + # All sell orders must be equal-sized in market QUOTE + assert order['base']['amount'] == worker.sell_orders[0]['base']['amount'] + + +# Note: no other tests for slope modes because they are combined modes. If valley and mountain are ok, so slopes too + + +def test_allocate_asset_basic(worker): + """ Check that free balance is shrinking after each allocation and spread is decreasing + """ + + worker.calculate_asset_thresholds() + worker.refresh_balances() + spread_after = get_spread(worker) + + # Allocate asset until target spread will be reached + while spread_after >= worker.target_spread + worker.increment: + free_base = worker.base_balance + free_quote = worker.quote_balance + spread_before = get_spread(worker) + + worker.allocate_asset('base', free_base) + worker.allocate_asset('quote', free_quote) + worker.refresh_orders() + worker.refresh_balances(use_cached_orders=True) + spread_after = get_spread(worker) + + # Update whistory of balance changes + worker.base_balance_history.append(worker.base_balance['amount']) + worker.quote_balance_history.append(worker.quote_balance['amount']) + if len(worker.base_balance_history) > 3: + del worker.base_balance_history[0] + del worker.quote_balance_history[0] + + # Free balance is shrinking after each allocation + assert worker.base_balance < free_base or worker.quote_balance < free_quote + + # Actual spread is decreasing + assert spread_after < spread_before + + +def test_allocate_asset_replace_closest_partial_order(worker, do_initial_allocation, base_account, issue_asset): + """ Test that partially filled order is replaced when target spread is not reached, before placing closer order + """ + do_initial_allocation(worker, worker.mode) + additional_account = base_account() + + # Sell some quote from another account to make PF order on buy side + price = worker.buy_orders[0]['price'] / 1.01 + amount = worker.buy_orders[0]['quote']['amount'] * (1 - worker.partial_fill_threshold * 1.1) + worker.market.sell(price, amount, account=additional_account) + + # Fill sell order + price = worker.sell_orders[0]['price'] ** -1 * 1.01 + amount = worker.sell_orders[0]['base']['amount'] + worker.market.buy(price, amount, account=additional_account) + + # Expect replaced closest buy order + worker.refresh_orders() + worker.refresh_balances(use_cached_orders=True) + worker.allocate_asset('base', worker.base_balance) + worker.refresh_orders() + assert worker.buy_orders[0]['base']['amount'] == worker.buy_orders[0]['for_sale']['amount'] + + +def test_allocate_asset_replace_partially_filled_orders( + worker, do_initial_allocation, base_account, issue_asset, maintain_until_allocated +): + """ Check replacement of partially filled orders on both sides. Simple check. + """ + do_initial_allocation(worker, worker.mode) + # TODO: automatically turn off bootstrapping after target spread is closed? + worker.bootstrapping = False + additional_account = base_account() + + # Partially fill closest orders + price = worker.buy_orders[0]['price'] + amount = worker.buy_orders[0]['quote']['amount'] / 2 + log.debug('Filling {} @ {}'.format(amount, price)) + worker.market.sell(price, amount, account=additional_account) + price = worker.sell_orders[0]['price'] ** -1 + amount = worker.sell_orders[0]['base']['amount'] / 2 + log.debug('Filling {} @ {}'.format(amount, price)) + worker.market.buy(price, amount, account=additional_account) + + # Add some balance to worker + to_issue = worker.buy_orders[0]['base']['amount'] + issue_asset(worker.market['base']['symbol'], to_issue, worker.account.name) + to_issue = worker.sell_orders[0]['base']['amount'] + issue_asset(worker.market['quote']['symbol'], to_issue, worker.account.name) + + maintain_until_allocated(worker) + worker.refresh_orders() + assert worker.buy_orders[0]['base']['amount'] == worker.buy_orders[0]['for_sale']['amount'] + assert worker.sell_orders[0]['base']['amount'] == worker.sell_orders[0]['for_sale']['amount'] + + +def test_allocate_asset_increase_orders(worker, do_initial_allocation, maintain_until_allocated, issue_asset): + """ Add balance, expect increased orders + """ + do_initial_allocation(worker, worker.mode) + order_ids = [order['id'] for order in worker.own_orders] + balance_in_orders_before = worker.get_allocated_assets(order_ids) + to_issue = worker.buy_orders[0]['base']['amount'] * 3 + issue_asset(worker.market['base']['symbol'], to_issue, worker.account.name) + to_issue = worker.sell_orders[0]['base']['amount'] * 3 + issue_asset(worker.market['quote']['symbol'], to_issue, worker.account.name) + # Use maintain_strategy() here for simplicity + maintain_until_allocated(worker) + order_ids = [order['id'] for order in worker.own_orders] + balance_in_orders_after = worker.get_allocated_assets(order_ids) + assert balance_in_orders_after['base'] > balance_in_orders_before['base'] + assert balance_in_orders_after['quote'] > balance_in_orders_before['quote'] + + +@pytest.mark.xfail(reason='https://github.com/Codaone/DEXBot/issues/588') +def test_allocate_asset_dust_order(worker, do_initial_allocation, maintain_until_allocated, base_account): + """ Make dust order, check if it canceled and closer opposite order placed + """ + do_initial_allocation(worker, worker.mode) + num_sell_orders_before = len(worker.sell_orders) + num_buy_orders_before = len(worker.buy_orders) + additional_account = base_account() + # Partially fill order from another account + sell_price = worker.buy_orders[0]['price'] / 1.01 + sell_amount = worker.buy_orders[0]['quote']['amount'] * (1 - worker.partial_fill_threshold) * 1.1 + worker.market.sell(sell_price, sell_amount, account=additional_account) + worker.refresh_balances() + worker.refresh_orders() + worker.allocate_asset('quote', worker.quote_balance) + worker.refresh_orders() + num_sell_orders_after = len(worker.sell_orders) + num_buy_orders_after = len(worker.buy_orders) + + assert num_buy_orders_before - num_buy_orders_after == 1 + assert num_sell_orders_after - num_sell_orders_before == 1 + + +@pytest.mark.xfail(reason='https://github.com/Codaone/DEXBot/issues/587') +def test_allocate_asset_dust_order_increase(worker, do_initial_allocation, base_account, issue_asset): + """ Test for https://github.com/Codaone/DEXBot/issues/587 + """ + do_initial_allocation(worker, worker.mode) + additional_account = base_account() + num_buy_orders_before = len(worker.buy_orders) + + # Make closest sell order small enough to be a most likely candidate for increase + worker.cancel_orders_wrapper(worker.sell_orders[0]) + worker.refresh_orders() + worker.refresh_balances() + worker.place_closer_order( + 'quote', worker.sell_orders[0], own_asset_limit=(worker.sell_orders[0]['base']['amount'] / 100) + ) + worker.refresh_orders() + # Additional balance to overcome reservation + to_issue = worker.sell_orders[1]['base']['amount'] + issue_asset(worker.market['quote']['symbol'], to_issue, worker.account.name) + # Partially fill order from another account + buy_price = worker.sell_orders[0]['price'] ** -1 * 1.01 + buy_amount = worker.sell_orders[0]['base']['amount'] * (1 - worker.partial_fill_threshold) * 1.1 + log.debug('{}, {}'.format(buy_price, buy_amount)) + worker.market.buy(buy_price, buy_amount, account=additional_account) + + # PF fill sell order should be replaced without errors + worker.maintain_strategy() + worker.refresh_orders() + num_buy_orders_after = len(worker.buy_orders) + assert num_buy_orders_before - num_buy_orders_after == 1 + + +@pytest.mark.xfail(reason='https://github.com/Codaone/DEXBot/issues/588') +def test_allocate_asset_filled_orders(worker, do_initial_allocation, base_account): + """ Fill an order and check if opposite order placed + """ + do_initial_allocation(worker, worker.mode) + # TODO: automatically turn off bootstrapping after target spread is closed? + worker.bootstrapping = False + additional_account = base_account() + num_sell_orders_before = len(worker.sell_orders) + + # Fill sell order + price = worker.buy_orders[0]['price'] + amount = worker.buy_orders[0]['quote']['amount'] + worker.market.sell(price, amount, account=additional_account) + worker.refresh_balances() + worker.refresh_orders() + worker.allocate_asset('quote', worker.quote_balance) + worker.refresh_orders() + num_sell_orders_after = len(worker.sell_orders) + assert num_sell_orders_after - num_sell_orders_before == 1 + + +@pytest.mark.parametrize('mode', MODES) +def test_allocate_asset_limiting_on_sell_side(mode, worker, do_initial_allocation, base_account): + """ Check order size limiting when placing closer order on side which is bigger (using funds obtained from filled + orders on side which is smaller) + """ + do_initial_allocation(worker, worker.mode) + # TODO: automatically turn off bootstrapping after target spread is closed? + worker.bootstrapping = False + additional_account = base_account() + + # Fill several orders + num_orders_to_fill = 4 + for i in range(0, num_orders_to_fill): + price = worker.buy_orders[i]['price'] + amount = worker.buy_orders[i]['quote']['amount'] * 1.01 + log.debug('Filling {} @ {}'.format(amount, price)) + worker.market.sell(price, amount, account=additional_account) + + # Cancel unmatched dust + account = Account(additional_account, bitshares_instance=worker.bitshares) + ids = [order['id'] for order in account.openorders if 'id' in order] + worker.bitshares.cancel(ids, account=additional_account) + + # Allocate asset until target spread will be reached + worker.refresh_orders() + worker.refresh_balances(use_cached_orders=True) + spread_after = get_spread(worker) + counter = 0 + while spread_after >= worker.target_spread + worker.increment: + worker.allocate_asset('base', worker.base_balance) + worker.allocate_asset('quote', worker.quote_balance) + worker.refresh_orders() + worker.refresh_balances(use_cached_orders=True) + spread_after = get_spread(worker) + counter += 1 + # Counter is for preventing infinity loop + assert counter < 20 + + # Check 2 closest orders to match mode + if worker.mode == 'valley' or worker.mode == 'sell_slope': + assert worker.sell_orders[0]['base']['amount'] == worker.sell_orders[1]['base']['amount'] + elif worker.mode == 'mountain' or worker.mode == 'buy_slope': + assert worker.sell_orders[0]['quote']['amount'] == pytest.approx( + worker.sell_orders[1]['quote']['amount'], rel=(1 ** -worker.market['base']['precision']) + ) + elif worker.mode == 'neutral': + assert worker.sell_orders[0]['base']['amount'] == pytest.approx( + worker.sell_orders[1]['base']['amount'] * math.sqrt(1 + worker.increment), + rel=(1 ** -worker.market['quote']['precision']), + ) + + +@pytest.mark.parametrize('mode', MODES) +def test_allocate_asset_limiting_on_buy_side(mode, worker, do_initial_allocation, base_account, issue_asset): + """ Check order size limiting when placing closer order on side which is bigger (using funds obtained from filled + orders on side which is smaller) + """ + worker.center_price = 1 + worker.lower_bound = 0.4 + worker.upper_bound = 1.4 + do_initial_allocation(worker, worker.mode) + # TODO: automatically turn off bootstrapping after target spread is closed? + worker.bootstrapping = False + additional_account = base_account() + + # Fill several orders + num_orders_to_fill = 5 + for i in range(0, num_orders_to_fill): + price = worker.sell_orders[i]['price'] ** -1 + amount = worker.sell_orders[i]['base']['amount'] * 1.01 + log.debug('Filling {} @ {}'.format(amount, price)) + worker.market.buy(price, amount, account=additional_account) + + # Cancel unmatched dust + account = Account(additional_account, bitshares_instance=worker.bitshares) + ids = [order['id'] for order in account.openorders if 'id' in order] + worker.bitshares.cancel(ids, account=additional_account) + + # Allocate asset until target spread will be reached + worker.refresh_orders() + worker.refresh_balances(use_cached_orders=True) + spread_after = get_spread(worker) + counter = 0 + while spread_after >= worker.target_spread + worker.increment: + worker.allocate_asset('base', worker.base_balance) + worker.allocate_asset('quote', worker.quote_balance) + worker.refresh_orders() + worker.refresh_balances(use_cached_orders=True) + spread_after = get_spread(worker) + counter += 1 + # Counter is for preventing infinity loop + assert counter < 20 + + # Check 2 closest orders to match mode + if worker.mode == 'valley' or worker.mode == 'buy_slope': + assert worker.buy_orders[0]['base']['amount'] == worker.buy_orders[1]['base']['amount'] + elif worker.mode == 'mountain' or worker.mode == 'sell_slope': + assert worker.buy_orders[0]['quote']['amount'] == pytest.approx( + worker.buy_orders[1]['quote']['amount'], rel=(1 ** -worker.market['base']['precision']) + ) + elif worker.mode == 'neutral': + assert worker.buy_orders[0]['base']['amount'] == pytest.approx( + worker.buy_orders[1]['base']['amount'] * math.sqrt(1 + worker.increment), + rel=(1 ** -worker.market['quote']['precision']), + ) + + +def test_tick(worker): + """ Check tick counter increment + """ + counter_before = worker.counter + worker.tick('foo') + counter_after = worker.counter + assert counter_after - counter_before == 1 diff --git a/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py b/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py new file mode 100644 index 000000000..1a35fc5f8 --- /dev/null +++ b/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py @@ -0,0 +1,556 @@ +import logging +import math +import pytest + +from dexbot.strategies.staggered_orders import VirtualOrder + +# Turn on debug for dexbot logger +logger = logging.getLogger("dexbot") +logger.setLevel(logging.DEBUG) + + +################### +# Higher-level methods which depends on lower-level methods +################### + + +def test_refresh_balances(orders1): + """ Check if balance refresh works + """ + worker = orders1 + worker.refresh_balances() + balance = worker.count_asset() + + assert worker.base_balance['amount'] > 0 + assert worker.quote_balance['amount'] > 0 + assert worker.base_total_balance == balance['base'] + assert worker.quote_total_balance == balance['quote'] + + +def test_refresh_orders(orders1): + """ Make sure orders refresh is working + + Note: this test doesn't checks orders sorting + """ + worker = orders1 + worker.refresh_orders() + assert worker.virtual_buy_orders[0]['base']['amount'] == 10 + assert worker.virtual_sell_orders[0]['base']['amount'] == 10 + assert worker.real_buy_orders[0]['base']['amount'] == 10 + assert worker.real_sell_orders[0]['base']['amount'] == 10 + assert len(worker.sell_orders) == 2 + assert len(worker.buy_orders) == 2 + + +def test_check_min_order_size(worker): + """ Make sure our orders are always match minimal allowed size + """ + worker.calculate_min_amounts() + if worker.order_min_quote > worker.order_min_base: + # Limiting asset is QUOTE + # Intentionally pass amount 2 times lower than minimum, the function should return increased amount + corrected_amount = worker.check_min_order_size(worker.order_min_quote / 2, 1) + assert corrected_amount == worker.order_min_quote + else: + # Limiting precision is BASE, at price=1 amounts are the same, so pass 2 times lower amount + corrected_amount = worker.check_min_order_size(worker.order_min_base / 2, 1) + assert corrected_amount >= worker.order_min_quote + + # Place/cancel real order to ensure no errors from the node + worker.place_market_sell_order(corrected_amount, 1, returnOrderId=False) + worker.cancel_all_orders() + + +def test_remove_outside_orders(orders1): + """ All orders in orders1 fixture are outside of the range, so remove_outside_orders() should cancel all + """ + worker = orders1 + worker.refresh_orders() + assert worker.remove_outside_orders(worker.sell_orders, worker.buy_orders) + assert len(worker.sell_orders) == 0 + assert len(worker.buy_orders) == 0 + + +def test_restore_virtual_orders(orders2): + """ Very basic test, checks if number of virtual orders at least 2 + """ + worker = orders2 + worker.restore_virtual_orders() + assert len(worker.virtual_orders) >= 2 + + +def test_replace_real_order_with_virtual(orders2): + """ Try to replace 2 furthest orders with virtual, then compare difference + """ + worker = orders2 + worker.virtual_orders = [] + num_orders_before = len(worker.real_buy_orders) + len(worker.real_sell_orders) + worker.replace_real_order_with_virtual(worker.real_buy_orders[-1]) + worker.replace_real_order_with_virtual(worker.real_sell_orders[-1]) + worker.refresh_orders() + num_orders_after = len(worker.real_buy_orders) + len(worker.real_sell_orders) + assert num_orders_before - num_orders_after == 2 + assert len(worker.virtual_orders) == 2 + + +def test_replace_virtual_order_with_real(orders3): + """ Try to replace 2 furthest virtual orders with real orders + """ + worker = orders3 + num_orders_before = len(worker.virtual_orders) + num_real_orders_before = len(worker.own_orders) + assert worker.replace_virtual_order_with_real(worker.virtual_buy_orders[-1]) + assert worker.replace_virtual_order_with_real(worker.virtual_sell_orders[-1]) + num_orders_after = len(worker.virtual_orders) + num_real_orders_after = len(worker.own_orders) + assert num_orders_before - num_orders_after == 2 + assert num_real_orders_after - num_real_orders_before == 2 + + +def test_store_profit_estimation_data(worker, storage_db): + """ Check if storing of profit estimation data works + """ + worker.refresh_balances() + worker.store_profit_estimation_data(force=True) + account = worker.worker.get('account') + data = worker.get_recent_balance_entry(account, worker.worker_name, worker.base_asset, worker.quote_asset) + assert data.center_price == worker.market_center_price + assert data.base_total == worker.base_total_balance + assert data.quote_total == worker.quote_total_balance + + +def test_check_partial_fill(worker, partially_filled_order): + """ Test that check_partial_fill() can detect partially filled order + """ + is_not_partially_filled = worker.check_partial_fill(partially_filled_order, fill_threshold=0) + assert not is_not_partially_filled + is_not_partially_filled = worker.check_partial_fill(partially_filled_order, fill_threshold=90) + assert is_not_partially_filled + + +def test_replace_partially_filled_order(worker, partially_filled_order): + """ Test if replace_partially_filled_order() do correct replacement + """ + worker.replace_partially_filled_order(partially_filled_order) + new_order = worker.own_orders[0] + assert new_order['base']['amount'] == new_order['for_sale']['amount'] + + +def test_place_lowest_buy_order(worker2): + """ Check if placement of lowest buy order works in general + """ + worker = worker2 + worker.refresh_balances() + worker.place_lowest_buy_order(worker.base_balance) + worker.refresh_orders() + + # Expect furthest order price to be less than increment x2 + assert worker.buy_orders[-1]['price'] < worker.lower_bound * (1 + worker.increment * 2) + + +def test_place_highest_sell_order(worker2): + """ Check if placement of highest sell order works in general + """ + worker = worker2 + worker.refresh_balances() + worker.place_highest_sell_order(worker.quote_balance) + worker.refresh_orders() + + # Expect furthest order price to be less than increment x2 + assert worker.sell_orders[-1]['price'] ** -1 > worker.upper_bound / (1 + worker.increment * 2) + + +@pytest.mark.parametrize('asset', ['base', 'quote']) +def test_place_closer_order_real_or_virtual(orders5, asset): + """ Closer order may be real or virtual, depending on distance from the center and operational_depth + + 1. Closer order within operational depth must be real + 2. Closer order outside of operational depth must be virtual if previous order is virtual + 3. Closer order outside of operational depth must be real if previous order is real + """ + worker = orders5 + if asset == 'base': + virtual_outside = worker.virtual_buy_orders[-1] + virtual_within = worker.virtual_buy_orders[0] + real_outside = worker.real_buy_orders[-1] + real_within = worker.real_buy_orders[0] + elif asset == 'quote': + virtual_outside = worker.virtual_sell_orders[-1] + virtual_within = worker.virtual_sell_orders[0] + real_outside = worker.real_sell_orders[-1] + real_within = worker.real_sell_orders[0] + + closer_order = worker.place_closer_order(asset, virtual_outside, place_order=True) + assert isinstance( + closer_order, VirtualOrder + ), "Closer order outside of operational depth must be virtual if previous order is virtual" + + # When self.returnOrderId is True, place_market_xxx_order() will return bool + closer_order = worker.place_closer_order(asset, virtual_within, place_order=True) + assert closer_order, "Closer order within operational depth must be real" + + closer_order = worker.place_closer_order(asset, real_outside, place_order=True) + assert closer_order, "Closer order outside of operational depth must be real if previous order is real" + + closer_order = worker.place_closer_order(asset, real_within, place_order=True) + assert closer_order, "Closer order within operational depth must be real" + + +@pytest.mark.xfail(reason='https://github.com/bitshares/python-bitshares/issues/227') +@pytest.mark.parametrize('asset', ['base', 'quote']) +def test_place_closer_order_price_amount(orders5, asset): + """ Test that closer order price and amounts are correct + """ + worker = orders5 + + if asset == 'base': + order = worker.buy_orders[0] + elif asset == 'quote': + order = worker.sell_orders[0] + + worker.returnOrderId = True + closer_order = worker.place_closer_order(asset, order, place_order=True) + + # Test for correct price + assert closer_order['price'] == order['price'] * (1 + worker.increment) + + # Test for correct amount + if ( + worker.mode == 'mountain' + or (worker.mode == 'buy_slope' and asset == 'quote') + or (worker.mode == 'sell_slope' and asset == 'base') + ): + assert closer_order['quote']['amount'] == order['quote']['amount'] + elif ( + worker.mode == 'valley' + or (worker.mode == 'buy_slope' and asset == 'base') + or (worker.mode == 'sell_slope' and asset == 'quote') + ): + assert closer_order['base']['amount'] == order['base']['amount'] + elif worker.mode == 'neutral': + assert closer_order['base']['amount'] == order['base']['amount'] * math.sqrt(1 + worker.increment) + + +@pytest.mark.xfail(reason='https://github.com/bitshares/python-bitshares/issues/227') +@pytest.mark.parametrize('asset', ['base', 'quote']) +def test_place_closer_order_no_place_order(orders5, asset): + """ Test place_closer_order() with place_order=False kwarg + """ + worker = orders5 + + if asset == 'base': + order = worker.buy_orders[0] + elif asset == 'quote': + order = worker.sell_orders[0] + + closer_order = worker.place_closer_order(asset, order, place_order=False) + worker.place_closer_order(asset, order, place_order=True) + worker.refresh_orders() + + if asset == 'base': + real_order = worker.buy_orders[0] + price = real_order['price'] + amount = real_order['quote']['amount'] + elif asset == 'quote': + real_order = worker.sell_orders[0] + price = real_order['price'] ** -1 + amount = real_order['base']['amount'] + + assert closer_order['price'] == price + assert closer_order['amount'] == amount + + +@pytest.mark.parametrize('asset', ['base', 'quote']) +def test_place_closer_order_allow_partial_hard_limit(orders2, asset): + """ Test place_closer_order with allow_partial=True when avail balance is less than minimal allowed order size + """ + worker = orders2 + + if asset == 'base': + order = worker.buy_orders[0] + price = order['price'] + # Pretend we have balance smaller than hard limit + worker.base_balance['amount'] = worker.check_min_order_size(0, price) / 2 + elif asset == 'quote': + order = worker.sell_orders[0] + price = order['price'] ** -1 + worker.quote_balance['amount'] = worker.check_min_order_size(0, price) / 2 + + num_orders_before = len(worker.own_orders) + worker.place_closer_order(asset, order, place_order=True, allow_partial=True) + num_orders_after = len(worker.own_orders) + # Expect that order was not placed + assert num_orders_before == num_orders_after + + +@pytest.mark.parametrize('asset', ['base', 'quote']) +def test_place_closer_order_allow_partial_soft_limit(orders2, asset): + """ Test place_closer_order with allow_partial=True when avail balance is less than self.partial_fill_threshold + restriction + """ + worker = orders2 + + if asset == 'base': + order = worker.buy_orders[0] + # Pretend we have balance smaller than soft limit + worker.base_balance['amount'] = order['base']['amount'] * worker.partial_fill_threshold / 1.1 + elif asset == 'quote': + order = worker.sell_orders[0] + worker.quote_balance['amount'] = order['base']['amount'] * worker.partial_fill_threshold / 1.1 + + num_orders_before = len(worker.own_orders) + worker.place_closer_order(asset, order, place_order=True, allow_partial=True) + num_orders_after = len(worker.own_orders) + # Expect that order was not placed + assert num_orders_before == num_orders_after + + +@pytest.mark.parametrize('asset', ['base', 'quote']) +def test_place_closer_order_allow_partial(orders2, asset): + """ Test place_closer_order with allow_partial=True when avail balance is more than self.partial_fill_threshold + restriction (enough for partial order) + """ + worker = orders2 + + if asset == 'base': + order = worker.buy_orders[0] + worker.base_balance['amount'] = order['base']['amount'] * worker.partial_fill_threshold * 2 + elif asset == 'quote': + order = worker.sell_orders[0] + worker.quote_balance['amount'] = order['base']['amount'] * worker.partial_fill_threshold * 2 + + num_orders_before = len(worker.own_orders) + worker.place_closer_order(asset, order, place_order=True, allow_partial=True) + num_orders_after = len(worker.own_orders) + # Expect order placed + assert num_orders_after - num_orders_before == 1 + + +@pytest.mark.parametrize('asset', ['base', 'quote']) +def test_place_closer_order_not_allow_partial(orders2, asset): + """ Test place_closer_order with allow_partial=False + """ + worker = orders2 + + if asset == 'base': + order = worker.buy_orders[0] + worker.base_balance['amount'] = order['base']['amount'] * worker.partial_fill_threshold * 2 + elif asset == 'quote': + order = worker.sell_orders[0] + worker.quote_balance['amount'] = order['base']['amount'] * worker.partial_fill_threshold * 2 + + num_orders_before = len(worker.own_orders) + worker.place_closer_order(asset, order, place_order=True, allow_partial=False) + num_orders_after = len(worker.own_orders) + # Expect that order was not placed + assert num_orders_before == num_orders_after + + +@pytest.mark.parametrize('asset', ['base', 'quote']) +def test_place_closer_order_own_asset_limit(orders5, asset): + """ Place closer order with own_asset_limit, test that amount of a new order is matching limit + """ + worker = orders5 + + if asset == 'base': + order = worker.buy_orders[0] + elif asset == 'quote': + order = worker.sell_orders[0] + + limit = order['base']['amount'] / 2 + + worker.returnOrderId = True + closer_order = worker.place_closer_order(asset, order, place_order=True, own_asset_limit=limit) + assert closer_order['base']['amount'] == limit + + +@pytest.mark.parametrize('asset', ['base', 'quote']) +def test_place_closer_order_opposite_asset_limit(orders5, asset): + """ Place closer order with opposite_asset_limit, test that amount of a new order is matching limit + """ + worker = orders5 + + if asset == 'base': + order = worker.buy_orders[0] + elif asset == 'quote': + order = worker.sell_orders[0] + + limit = order['quote']['amount'] / 2 + + worker.returnOrderId = True + closer_order = worker.place_closer_order(asset, order, place_order=True, opposite_asset_limit=limit) + assert closer_order['quote']['amount'] == limit + + +@pytest.mark.parametrize('asset', ['base', 'quote']) +def test_place_closer_order_instant_fill_disabled(orders5, asset): + """ When instant fill is disabled, new order should not cross lowest ask or highest bid + """ + worker = orders5 + + if asset == 'base': + order = worker.buy_orders[0] + elif asset == 'quote': + order = worker.sell_orders[0] + + worker.is_instant_fill_enabled = False + # Bump increment so hish that closer order will inevitably cross an opposite one + worker.increment = 100 + result = worker.place_closer_order(asset, order, place_order=True) + assert result is None + + +@pytest.mark.parametrize('asset', ['base', 'quote']) +def test_place_further_order_real_or_virtual(orders5, asset): + """ Further order may be real or virtual, depending on distance from the center and operational_depth + + 1. Further order within operational depth must be real + 2. Further order within operational depth must be virtual if virtual=True was given + 2. Further order outside of operational depth must be virtual + """ + worker = orders5 + if asset == 'base': + real_outside = worker.real_buy_orders[-1] + real_within = worker.real_buy_orders[0] + elif asset == 'quote': + real_outside = worker.real_sell_orders[-1] + real_within = worker.real_sell_orders[0] + + further_order = worker.place_further_order(asset, real_within, place_order=True) + assert further_order, "Further order within operational depth must be real" + + further_order = worker.place_further_order(asset, real_within, place_order=True, virtual=True) + assert isinstance( + further_order, VirtualOrder + ), "Further order within operational depth must be virtual if virtual=True was given" + + further_order = worker.place_further_order(asset, real_outside, place_order=True) + assert isinstance(further_order, VirtualOrder), "Further order outside of operational depth must be virtual" + + +@pytest.mark.xfail(reason='https://github.com/bitshares/python-bitshares/issues/227') +@pytest.mark.parametrize('asset', ['base', 'quote']) +def test_place_further_order_price_amount(orders5, asset): + """ Test that further order price and amounts are correct + """ + worker = orders5 + + if asset == 'base': + order = worker.buy_orders[0] + elif asset == 'quote': + order = worker.sell_orders[0] + + worker.returnOrderId = True + further_order = worker.place_further_order(asset, order, place_order=True) + + # Test for correct price + assert further_order['price'] == order['price'] / (1 + worker.increment) + + # Test for correct amount + if ( + worker.mode == 'mountain' + or (worker.mode == 'buy_slope' and asset == 'quote') + or (worker.mode == 'sell_slope' and asset == 'base') + ): + assert further_order['quote']['amount'] == order['quote']['amount'] + elif ( + worker.mode == 'valley' + or (worker.mode == 'buy_slope' and asset == 'base') + or (worker.mode == 'sell_slope' and asset == 'quote') + ): + assert further_order['base']['amount'] == order['base']['amount'] + elif worker.mode == 'neutral': + assert further_order['base']['amount'] == order['base']['amount'] / math.sqrt(1 + worker.increment) + + +@pytest.mark.xfail(reason='https://github.com/bitshares/python-bitshares/issues/227') +@pytest.mark.parametrize('asset', ['base', 'quote']) +def test_place_further_order_no_place_order(orders5, asset): + """ Test place_further_order() with place_order=False kwarg + """ + worker = orders5 + + if asset == 'base': + order = worker.buy_orders[0] + elif asset == 'quote': + order = worker.sell_orders[0] + + further_order = worker.place_further_order(asset, order, place_order=False) + # Place real order to compare with + worker.place_further_order(asset, order, place_order=True) + worker.refresh_orders() + + if asset == 'base': + real_order = worker.buy_orders[1] + price = real_order['price'] + amount = real_order['quote']['amount'] + elif asset == 'quote': + real_order = worker.sell_orders[1] + price = real_order['price'] ** -1 + amount = real_order['base']['amount'] + + assert further_order['price'] == price + assert further_order['amount'] == amount + + +@pytest.mark.parametrize('asset', ['base', 'quote']) +def test_place_further_order_not_allow_partial(orders2, asset): + """ Test place_further_order with allow_partial=False + """ + worker = orders2 + + if asset == 'base': + order = worker.buy_orders[0] + worker.base_balance['amount'] = order['base']['amount'] / 2 + elif asset == 'quote': + order = worker.sell_orders[0] + worker.quote_balance['amount'] = order['base']['amount'] / 2 + + num_orders_before = len(worker.own_orders) + worker.place_further_order(asset, order, place_order=True, allow_partial=False) + num_orders_after = len(worker.own_orders) + # Expect that order was not placed + assert num_orders_before == num_orders_after + + +@pytest.mark.parametrize('asset', ['base', 'quote']) +def test_place_further_order_allow_partial_hard_limit(orders2, asset): + """ Test place_further_order with allow_partial=True when avail balance is less than minimal allowed order size + """ + worker = orders2 + + if asset == 'base': + order = worker.buy_orders[0] + price = order['price'] + # Pretend we have balance smaller than hard limit + worker.base_balance['amount'] = worker.check_min_order_size(0, price) / 2 + elif asset == 'quote': + order = worker.sell_orders[0] + price = order['price'] ** -1 + worker.quote_balance['amount'] = worker.check_min_order_size(0, price) / 2 + + num_orders_before = len(worker.own_orders) + worker.place_further_order(asset, order, place_order=True, allow_partial=True) + num_orders_after = len(worker.own_orders) + # Expect that order was not placed + assert num_orders_before == num_orders_after + + +@pytest.mark.parametrize('asset', ['base', 'quote']) +def test_place_further_order_allow_partial(orders2, asset): + """ Test place_further_order with allow_partial=True + """ + worker = orders2 + + if asset == 'base': + order = worker.buy_orders[0] + worker.base_balance['amount'] = order['base']['amount'] / 2 + elif asset == 'quote': + order = worker.sell_orders[0] + worker.quote_balance['amount'] = order['base']['amount'] / 2 + + num_orders_before = len(worker.own_orders) + worker.place_closer_order(asset, order, place_order=True, allow_partial=True) + num_orders_after = len(worker.own_orders) + # Expect order placed + assert num_orders_after - num_orders_before == 1 diff --git a/tests/strategies/staggered_orders/test_staggered_orders_init.py b/tests/strategies/staggered_orders/test_staggered_orders_init.py new file mode 100644 index 000000000..01679c301 --- /dev/null +++ b/tests/strategies/staggered_orders/test_staggered_orders_init.py @@ -0,0 +1,36 @@ +import copy +import logging +import pytest + +from dexbot.strategies.staggered_orders import Strategy + +# Turn on debug for dexbot logger +logger = logging.getLogger("dexbot") +logger.setLevel(logging.DEBUG) + + +################### +# __init__ tests here +################### + + +@pytest.mark.parametrize('spread, increment', [(1, 2), pytest.param(2, 2, marks=pytest.mark.xfail(reason="bug"))]) +def test_spread_increment_check(bitshares, config, so_worker_name, spread, increment): + """ Spread must be greater than increment + """ + worker_name = so_worker_name + incorrect_config = copy.deepcopy(config) + incorrect_config['workers'][worker_name]['spread'] = spread + incorrect_config['workers'][worker_name]['increment'] = increment + worker = Strategy(config=incorrect_config, name=worker_name, bitshares_instance=bitshares) + assert worker.disabled + + +def test_min_operational_depth(bitshares, config, so_worker_name): + """ Operational depth should not be too small + """ + worker_name = so_worker_name + incorrect_config = copy.deepcopy(config) + incorrect_config['workers'][worker_name]['operational_depth'] = 1 + worker = Strategy(config=incorrect_config, name=worker_name, bitshares_instance=bitshares) + assert worker.disabled diff --git a/tests/strategies/staggered_orders/test_staggered_orders_lowlevel.py b/tests/strategies/staggered_orders/test_staggered_orders_lowlevel.py new file mode 100644 index 000000000..589119515 --- /dev/null +++ b/tests/strategies/staggered_orders/test_staggered_orders_lowlevel.py @@ -0,0 +1,44 @@ +import logging + +# Turn on debug for dexbot logger +logger = logging.getLogger("dexbot") +logger.setLevel(logging.DEBUG) + + +################### +# Lower-level methods used by higher-level methods +################### + + +def test_cancel_orders_wrapper(orders4): + worker = orders4 + + # test real order + orders = worker.own_orders + before = len(orders) + worker.cancel_orders_wrapper(orders[0]) + after = len(worker.own_orders) + assert before - after == 1 + # test virtual order + before = len(worker.virtual_orders) + worker.cancel_orders_wrapper(worker.virtual_orders[0]) + after = len(worker.virtual_orders) + assert before - after == 1 + + +def test_place_virtual_buy_order(worker, init_empty_balances): + worker.place_virtual_buy_order(100, 1) + assert len(worker.virtual_orders) == 1 + assert worker.virtual_orders[0]['base']['amount'] == 100 + assert worker.virtual_orders[0]['for_sale']['amount'] == 100 + assert worker.virtual_orders[0]['quote']['amount'] == 100 + assert worker.virtual_orders[0]['price'] == 1 + + +def test_place_virtual_sell_order(worker, init_empty_balances): + worker.place_virtual_sell_order(100, 1) + assert len(worker.virtual_orders) == 1 + assert worker.virtual_orders[0]['base']['amount'] == 100 + assert worker.virtual_orders[0]['for_sale']['amount'] == 100 + assert worker.virtual_orders[0]['quote']['amount'] == 100 + assert worker.virtual_orders[0]['price'] == 1 diff --git a/tests/strategies/staggered_orders/test_staggered_orders_unittests.py b/tests/strategies/staggered_orders/test_staggered_orders_unittests.py new file mode 100644 index 000000000..ce2b27f48 --- /dev/null +++ b/tests/strategies/staggered_orders/test_staggered_orders_unittests.py @@ -0,0 +1,44 @@ +import logging + +# Turn on debug for dexbot logger +logger = logging.getLogger("dexbot") +logger.setLevel(logging.DEBUG) + + +################### +# Methods which not depends on other methods at all, can be tested separately +################### + + +def test_log_maintenance_time(worker): + """ Should just not fail + """ + worker.log_maintenance_time() + + +def test_calculate_min_amounts(worker): + """ Min amounts should be greater than assets precision + """ + worker.calculate_min_amounts() + assert worker.order_min_base > 10 ** -worker.market['base']['precision'] + assert worker.order_min_quote > 10 ** -worker.market['quote']['precision'] + + +def test_calculate_asset_thresholds(worker): + """ Check asset threshold + + Todo: https://github.com/Codaone/DEXBot/issues/554 + """ + worker.calculate_asset_thresholds() + assert worker.base_asset_threshold > 0 + assert worker.quote_asset_threshold > 0 + + +def test_calc_buy_orders_count(worker): + worker.increment = 0.01 + assert worker.calc_buy_orders_count(100, 90) == 11 + + +def test_calc_sell_orders_count(worker): + worker.increment = 0.01 + assert worker.calc_sell_orders_count(90, 100) == 11 From 785f71f06f1caf1d666fd3dae42aefa85ca91bd4 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Mon, 20 May 2019 22:41:16 -0700 Subject: [PATCH 1424/1846] move orders log to base.py and remove reference to write_order_log in RO --- dexbot/orderengines/bitshares_engine.py | 4 ---- dexbot/strategies/base.py | 2 ++ dexbot/strategies/relative_orders.py | 2 -- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index 54bebcd53..45ddf5d58 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -111,10 +111,6 @@ def __init__(self, # buy/sell actions will return order id by default self.returnOrderId = 'head' - self.orders_log = logging.LoggerAdapter( - logging.getLogger('dexbot.orders_log'), {} - ) - def _callbackPlaceFillOrders(self, d): """ This method distinguishes notifications caused by Matched orders from those caused by placed orders """ diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 0b342220e..60ae7a2b9 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -189,6 +189,8 @@ def __init__(self, } ) + self.orders_log = logging.LoggerAdapter(logging.getLogger('dexbot.orders_log'), {}) + def pause(self): """ Pause the worker diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index e7aa8adf4..8722084a5 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -538,8 +538,6 @@ def check_orders(self, *args, **kwargs): self.log.debug('Could not found order on the market, it was filled, expired or cancelled') # Write a trade log entry only when we are not using custom expiration because we cannot # distinguish an expired order from filled - if not self.is_custom_expiration: - self.write_order_log(self.worker_name, order) elif self.is_reset_on_partial_fill: # Detect partially filled orders; # on fresh order 'for_sale' is always equal to ['base']['amount'] From 5eef74d7b8e4b60ff34fe1a1c31b771e5ece2c98 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 21 May 2019 09:22:06 +0300 Subject: [PATCH 1425/1846] Change one letter variables to more descriptive --- dexbot/config_validator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/config_validator.py b/dexbot/config_validator.py index 4eaa659f2..1d5fd1af7 100644 --- a/dexbot/config_validator.py +++ b/dexbot/config_validator.py @@ -39,8 +39,8 @@ def validate_private_key(self, account, private_key): if not private_key: # Check if the account is already in the database account_ids = wallet.getAccounts() - accounts = [Account(id, bitshares_instance=self.bitshares) for id in account_ids] - if any(account == a['name'] for a in accounts): + accounts = [Account(account_id, bitshares_instance=self.bitshares) for account_id in account_ids] + if any(account == account['name'] for account in accounts): return True return False @@ -51,7 +51,7 @@ def validate_private_key(self, account, private_key): # Load all accounts with corresponding public key from the blockchain account_ids = wallet.getAccountsFromPublicKey(pubkey) - account_names = [Account(id, bitshares_instance=self.bitshares).name for id in account_ids] + account_names = [Account(account_id, bitshares_instance=self.bitshares).name for account_id in account_ids] if account in account_names: return True From 6b50be61567f310288e8af23a47a353f369248ba Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 21 May 2019 09:31:20 +0300 Subject: [PATCH 1426/1846] Change dexbot version number to 0.11.5 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index ca889a739..0b76bd2c2 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.11.4' +VERSION = '0.11.5' AUTHOR = 'Codaone Oy' __version__ = VERSION From 78ff9808ea53f198d5fd975bf9f471621b2b5ad7 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 21 May 2019 12:17:46 +0500 Subject: [PATCH 1427/1846] Add git as dependency in Dockerfile Now we have git+https:// in requirements.txt, so need to have git. Closes: #599 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5b77bcd8a..4fea97610 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ RUN set -xe ;\ apt-get update ;\ # Prepare dependencies apt-get install -y --no-install-recommends gcc make libssl-dev python3-pip python3-dev python3-setuptools \ - python3-async whiptail ;\ + python3-async whiptail git ;\ apt-get clean ;\ rm -rf /var/lib/apt/lists/* From 3fcbc1d139d822db0cdc5f618008cf1cbb39c7e8 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 21 May 2019 14:42:18 +0500 Subject: [PATCH 1428/1846] Remove tests/__init__.py Modules insides tests/ should not be importable --- tests/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 From 2c994a5675a530f217d0153783945d4e95f1c0f8 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 21 May 2019 15:22:22 +0500 Subject: [PATCH 1429/1846] Pin versions of bitshares libs See #574 for details --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index c8f9d70f1..b3c855714 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,8 +8,8 @@ requests>=2.20.0 yarl==1.1.0 ccxt==1.17.434 pywaves==0.8.20 -git+https://github.com/Codaone/python-bitshares.git#egg=bitshares -git+https://github.com/Codaone/python-graphenelib.git#egg=graphenelib +git+https://github.com/Codaone/python-bitshares.git@0c9bf5ef3808572b7a10dfc32615906083e6b8ff#egg=bitshares +git+https://github.com/Codaone/python-graphenelib.git@ba3702a25498f56a8ade8b855b812e7ec00311e2#egg=graphenelib uptick==0.2.1 ruamel.yaml>=0.15.37 appdirs>=1.4.3 From 824926854e6298845f3a08f832b6df4777d2003e Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 21 May 2019 15:47:20 +0500 Subject: [PATCH 1430/1846] Prevent bandit warning In this case we're using `random` not for security but just for random number. --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 96ad1d99b..ce00b3f55 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -162,7 +162,7 @@ def unused_account(bitshares): def _unused_account(): _range = 100000 while True: - account = 'worker-{}'.format(random.randint(1, _range)) + account = 'worker-{}'.format(random.randint(1, _range)) # nosec try: Account(account, bitshares_instance=bitshares) except AccountDoesNotExistsException: From abb8e3b7c5e9c4a1ef30fb3f367b74b5b96da9d9 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 21 May 2019 15:49:03 +0500 Subject: [PATCH 1431/1846] Prevent bandit from arguing on assert statements Closes: #571 --- .gitignore | 3 +++ bandit.yml | 1 + 2 files changed, 4 insertions(+) create mode 100644 bandit.yml diff --git a/.gitignore b/.gitignore index b45009909..28f506e8a 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,6 @@ archive # Keep readthedocs config !.readthedocs.yml + +# Bandit config +!bandit.yml diff --git a/bandit.yml b/bandit.yml new file mode 100644 index 000000000..75d550c32 --- /dev/null +++ b/bandit.yml @@ -0,0 +1 @@ +skips: ['B101'] From 8351214459d9e86a32278946b2a5ae48ccc76584 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 21 May 2019 17:15:38 +0500 Subject: [PATCH 1432/1846] Add .remarkrc Default codacy remarklint settings are pretty strict, use relaxed settings. --- .remarkrc | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .remarkrc diff --git a/.remarkrc b/.remarkrc new file mode 100644 index 000000000..eebb33612 --- /dev/null +++ b/.remarkrc @@ -0,0 +1,7 @@ +{ + "plugins": [ + "remark-preset-lint-recommended", + ["remark-lint-list-item-indent", "space"], + ["remark-lint-fenced-code-flag", false] + ] +} From f0db6bdef608cc76049b4f9f8d6b664cc4eb44db Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Tue, 21 May 2019 15:37:59 -0700 Subject: [PATCH 1433/1846] remove old order log comment --- dexbot/strategies/relative_orders.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 8722084a5..60b36b30f 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -536,8 +536,6 @@ def check_orders(self, *args, **kwargs): if not current_order: need_update = True self.log.debug('Could not found order on the market, it was filled, expired or cancelled') - # Write a trade log entry only when we are not using custom expiration because we cannot - # distinguish an expired order from filled elif self.is_reset_on_partial_fill: # Detect partially filled orders; # on fresh order 'for_sale' is always equal to ['base']['amount'] From 8ebd7010ba76f07df2a524135e558c2d184939b0 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 22 May 2019 08:39:29 +0300 Subject: [PATCH 1434/1846] Change dexbot version number to 0.11.6 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 0b76bd2c2..7bb58e520 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.11.5' +VERSION = '0.11.6' AUTHOR = 'Codaone Oy' __version__ = VERSION From b852d224d7278a5572f375f4e7cd908d33106319 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 22 May 2019 08:56:42 +0300 Subject: [PATCH 1435/1846] Change dexbot version number to 0.11.7 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 7bb58e520..f663ea87b 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.11.6' +VERSION = '0.11.7' AUTHOR = 'Codaone Oy' __version__ = VERSION From 25ebe82e6a56979fc95b58516c4541e46b20e5d2 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 22 May 2019 14:09:11 +0500 Subject: [PATCH 1436/1846] Fix unused variables --- tests/strategies/staggered_orders/conftest.py | 2 +- .../staggered_orders/test_staggered_orders_complex.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/strategies/staggered_orders/conftest.py b/tests/strategies/staggered_orders/conftest.py index 4066d73f8..099892362 100644 --- a/tests/strategies/staggered_orders/conftest.py +++ b/tests/strategies/staggered_orders/conftest.py @@ -170,7 +170,7 @@ def storage_db(): """ from dexbot.storage import sqlDataBaseFile - fd, sqlDataBaseFile = tempfile.mkstemp() # noqa: F811 + _, sqlDataBaseFile = tempfile.mkstemp() # noqa: F811 yield os.unlink(sqlDataBaseFile) diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index 81d862d90..a4f2c64f9 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -170,7 +170,7 @@ def test_maintain_strategy_fallback_logic(asset, mode, worker, do_initial_alloca spread_before = get_spread(worker) assert spread_before > worker.target_spread + worker.increment - for i in range(0, 6): + for _ in range(0, 6): worker.maintain_strategy() worker.refresh_orders() @@ -616,7 +616,7 @@ def test_increase_order_sizes_neutral_smaller_closest_orders(worker, do_initial_ worker.place_closer_order('base', worker.buy_orders[0], own_asset_limit=base_limit) worker.place_closer_order('quote', worker.sell_orders[0], own_asset_limit=quote_limit) worker.refresh_orders() - for i in range(1, num_orders_to_cancel): + for _ in range(1, num_orders_to_cancel): worker.place_closer_order('base', worker.buy_orders[0]) worker.place_closer_order('quote', worker.sell_orders[0]) worker.refresh_orders() From 927a5f08927b4b0d5fe7daebaa93d2ceec1ef324 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 22 May 2019 15:00:59 +0500 Subject: [PATCH 1437/1846] Restore tests/__init__.py to be able to run via `pytest` When tests/__init__.py is present, pytest adds root directory to PYTHONPATH, so tests can use imports `from dexbot.foo import bar`; otherwise, `pip install -e .` is needed. See doc for details: https://docs.pytest.org/en/latest/goodpractices.html#choosing-a-test-layout-import-rules --- tests/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb From 4da0ba9be501dbd916473375e45c3d4cf4a7408c Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 22 May 2019 16:27:01 +0500 Subject: [PATCH 1438/1846] Set shared bitshares instance in tests This is a workaround for https://github.com/bitshares/python-bitshares/issues/234 --- tests/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 822081070..5c810b485 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,7 @@ import random from bitshares import BitShares +from bitshares.instance import set_shared_bitshares_instance from bitshares.genesisbalance import GenesisBalance from bitshares.account import Account from bitshares.asset import Asset @@ -87,6 +88,9 @@ def bitshares_instance(bitshares_testnet): bitshares = BitShares( node='ws://127.0.0.1:{}'.format(bitshares_testnet.service_port), keys=PRIVATE_KEYS, num_retries=-1 ) + # Shared instance allows to avoid any bugs when bitshares_instance is not passed explicitly when instantiating + # objects + set_shared_bitshares_instance(bitshares) # Todo: show chain params when connectiong to unknown network # https://github.com/bitshares/python-bitshares/issues/221 From d8da4bac5607b1c8234eea5a28a2788dd6165c49 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 28 May 2019 08:21:25 +0500 Subject: [PATCH 1439/1846] Fix default value supplied for cli configure select_choice() expect to see only "tag" as default, not ("tag", "value") tuple Closes: #605 --- dexbot/strategies/config_parts/relative_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/config_parts/relative_config.py b/dexbot/strategies/config_parts/relative_config.py index 480dab533..a87fb5165 100644 --- a/dexbot/strategies/config_parts/relative_config.py +++ b/dexbot/strategies/config_parts/relative_config.py @@ -30,7 +30,7 @@ def configure(cls, return_base_config=True): relative_orders_config = [ ConfigElement('external_feed', 'bool', False, 'External price feed', 'Use external reference price instead of center price acquired from the market', None), - ConfigElement('external_price_source', 'choice', EXCHANGES[0], 'External price source', + ConfigElement('external_price_source', 'choice', EXCHANGES[0][0], 'External price source', 'The bot will try to get price information from this source', EXCHANGES), ConfigElement('amount', 'float', 1, 'Amount', 'Fixed order size, expressed in quote asset, unless "relative order size" selected', From ebc00748bdc5ba5ed77b62a65c88b8e269622114 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Thu, 30 May 2019 21:54:55 +0300 Subject: [PATCH 1440/1846] 611, Use own last trade as basis for new center price --- dexbot/cli.py | 8 ++++---- .../strategies/config_parts/relative_config.py | 2 ++ dexbot/strategies/relative_orders.py | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index 3951c0d13..0dc0cec35 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -14,10 +14,10 @@ unlock, configfile ) -from .worker import WorkerInfrastructure -from .cli_conf import configure_dexbot, dexbot_service_running -from . import errors -from . import helper +from dexbot.worker import WorkerInfrastructure +from dexbot.cli_conf import configure_dexbot, dexbot_service_running +from dexbot import errors +from dexbot import helper # We need to do this before importing click if "LANG" not in os.environ: diff --git a/dexbot/strategies/config_parts/relative_config.py b/dexbot/strategies/config_parts/relative_config.py index 480dab533..59a26bbba 100644 --- a/dexbot/strategies/config_parts/relative_config.py +++ b/dexbot/strategies/config_parts/relative_config.py @@ -54,6 +54,8 @@ def configure(cls, return_base_config=True): ConfigElement('center_price_depth', 'float', 0, 'Measurement depth', 'Cumulative quote amount from which depth center price will be measured', (0.00000001, 1000000000, 8, '')), + ConfigElement('center_price_from_last_trade', bool, 1, 'Last trade price as new center price', + 'This will make orders move by half the spread at every fill', None), ConfigElement('center_price_offset', 'bool', False, 'Center price offset based on asset balances', 'Automatically adjust orders up or down based on the imbalance of your assets', None), ConfigElement('manual_offset', 'float', 0, 'Manual center price offset', diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 60b36b30f..788c49b89 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -58,10 +58,14 @@ def __init__(self, *args, **kwargs): if self.is_center_price_dynamic: self.center_price = None self.center_price_depth = self.worker.get('center_price_depth', 0) + self.cp_from_last_trade = self.worker.get('cp_from_last_trade') else: # Use manually set center price self.center_price = self.worker["center_price"] + if self.cp_from_last_trade: + self.ontick -= self.tick # Save a few cycles there + self.is_relative_order_size = self.worker.get('relative_order_size', False) self.is_asset_offset = self.worker.get('center_price_offset', False) self.manual_offset = self.worker.get('manual_offset', 0) / 100 @@ -197,6 +201,9 @@ def calculate_order_prices(self): # Calculate with quote amount if given center_price = self.get_market_center_price(quote_amount=self.center_price_depth) + if self.cp_from_last_trade: + center_price = self.get_own_last_trade()['price'] + self.center_price = self.calculate_center_price( center_price, self.is_asset_offset, @@ -583,3 +590,14 @@ def check_orders(self, *args, **kwargs): self.update_gui_profit() self.last_check = datetime.now() + + def get_own_last_trade(self): + """ Returns dict with amounts and price of last trade """ + trade = [x for x in self.account.history(limit=1, only_ops=['fill_order'])][0]['op'][1] + if trade['pays']['asset_id'] == self.market['base']['id']: # Buy order + base = trade['fill_price']['base']['amount'] / 10 ** self.market['base']['precision'] + quote = trade['fill_price']['quote']['amount'] / 10 ** self.market['quote']['precision'] + else: # Sell order + base = trade['fill_price']['quote']['amount'] / 10 ** self.market['base']['precision'] + quote = trade['fill_price']['base']['amount'] / 10 ** self.market['quote']['precision'] + return {'base': base, 'quote': quote, 'price': base / quote} \ No newline at end of file From eaa1e679982a824a25982146b683a31b75993ee4 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Fri, 31 May 2019 20:24:16 +0300 Subject: [PATCH 1441/1846] Error handling and security for edge casef If using center price from last trade, the strategy starts with basing first order pair on market center price. This in case the last own trade was a year ago at 100x price difference. --- dexbot/strategies/relative_orders.py | 38 ++++++++++++++++++---------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 788c49b89..825c01663 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -58,14 +58,11 @@ def __init__(self, *args, **kwargs): if self.is_center_price_dynamic: self.center_price = None self.center_price_depth = self.worker.get('center_price_depth', 0) - self.cp_from_last_trade = self.worker.get('cp_from_last_trade') + self.cp_from_last_trade = self.worker.get('cp_from_last_trade', False) else: # Use manually set center price self.center_price = self.worker["center_price"] - if self.cp_from_last_trade: - self.ontick -= self.tick # Save a few cycles there - self.is_relative_order_size = self.worker.get('relative_order_size', False) self.is_asset_offset = self.worker.get('center_price_offset', False) self.manual_offset = self.worker.get('manual_offset', 0) / 100 @@ -86,8 +83,13 @@ def __init__(self, *args, **kwargs): if self.is_custom_expiration: self.expiration = self.worker.get('expiration_time', self.expiration) + if self.cp_from_last_trade: + self.ontick -= self.tick # Save a few cycles there + self.expiration = 7 * 24 * 60 * 60 # Order expiration before first trade might result in terrible price + self.last_check = datetime.now() self.min_check_interval = 8 + self.first_round = True self.buy_price = None self.sell_price = None @@ -201,8 +203,13 @@ def calculate_order_prices(self): # Calculate with quote amount if given center_price = self.get_market_center_price(quote_amount=self.center_price_depth) - if self.cp_from_last_trade: - center_price = self.get_own_last_trade()['price'] + if self.cp_from_last_trade and not self.first_round: # Using own last trade is bad idea at startup + try: + center_price = self.get_own_last_trade()['price'] + except TypeError: + center_price = self.get_market_center_price() + else: + center_price = self.get_market_center_price() self.center_price = self.calculate_center_price( center_price, @@ -593,11 +600,14 @@ def check_orders(self, *args, **kwargs): def get_own_last_trade(self): """ Returns dict with amounts and price of last trade """ - trade = [x for x in self.account.history(limit=1, only_ops=['fill_order'])][0]['op'][1] - if trade['pays']['asset_id'] == self.market['base']['id']: # Buy order - base = trade['fill_price']['base']['amount'] / 10 ** self.market['base']['precision'] - quote = trade['fill_price']['quote']['amount'] / 10 ** self.market['quote']['precision'] - else: # Sell order - base = trade['fill_price']['quote']['amount'] / 10 ** self.market['base']['precision'] - quote = trade['fill_price']['base']['amount'] / 10 ** self.market['quote']['precision'] - return {'base': base, 'quote': quote, 'price': base / quote} \ No newline at end of file + try: + trade = [x for x in self.account.history(limit=1, only_ops=['fill_order'])][0]['op'][1] + if trade['pays']['asset_id'] == self.market['base']['id']: # Buy order + base = trade['fill_price']['base']['amount'] / 10 ** self.market['base']['precision'] + quote = trade['fill_price']['quote']['amount'] / 10 ** self.market['quote']['precision'] + else: # Sell order + base = trade['fill_price']['quote']['amount'] / 10 ** self.market['base']['precision'] + quote = trade['fill_price']['base']['amount'] / 10 ** self.market['quote']['precision'] + return {'base': base, 'quote': quote, 'price': base / quote} + except Exception: + return False \ No newline at end of file From d691df98fccc7a42b205210f583e5c63ba450627 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Fri, 31 May 2019 22:36:05 +0300 Subject: [PATCH 1442/1846] cp_from_last_trade added to UI but no connection to logic yet --- .../views/ui/forms/relative_orders_widget.ui | 5107 +++++++++-------- 1 file changed, 2716 insertions(+), 2391 deletions(-) diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index cb9566eb1..793514fa3 100644 --- a/dexbot/views/ui/forms/relative_orders_widget.ui +++ b/dexbot/views/ui/forms/relative_orders_widget.ui @@ -6,8 +6,8 @@ 0 0 - 449 - 687 + 583 + 830 @@ -31,1502 +31,1634 @@ Worker Parameters - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Order size - - - amount_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Fixed order size, expressed in quote asset, unless "relative order size" selected - - - ? - - - 5 - - - - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 9 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 8 - - - 1000000000.000000000000000 - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - - - - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Amount is expressed as a percentage of the account balance of quote/base asset - - - ? - - - 5 - - - - - - - - - - Relative order size - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Spread - - - spread_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - The percentage difference between buy and sell - - - ? - - - 5 - - - - - - - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - % - - - 100000.000000000000000 - - - 5.000000000000000 - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 101 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Enable dynamic spread which overrides the spread field - - - ? - - - 5 - - - - - - - - - - Dynamic spread - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Market depth - - - true - - - center_price_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - From which depth will market spread be measured? (QUOTE amount) - - - ? - - - 5 - - - - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 9 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 8 - - - 1000000000.000000000000000 - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - - - - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Dynamic spread factor - - - true - - - center_price_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - How many percent will own spread be compared to market spread? - - - ? - - - 5 - - - - - - - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - % - - - 0.010000000000000 - - - 1000.000000000000000 - - - 1.000000000000000 - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Center price - - - center_price_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Fixed center price expressed in base asset: base/quote - - - ? - - - 5 - - - - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 9 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - false - - - - 0 - 0 - - - - - 170 - 0 - - - - ArrowCursor - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - false - - - false - - - 8 - - - 0.000000000000000 - - - 999999999.998999953269958 - - - - - - - true - - - - 0 - 0 - - - - - 120 - 0 - - - - - - - - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 101 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Estimate the center from closest opposite orders or from a depth - - - ? - - - 5 - - - - - - - - - - - 0 - 0 - - - - Measure center price from market orders - - - true - - - false - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Measurement depth - - - true - - - center_price_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Cumulative quote amount from which depth center price will be measured - - - ? - - - 5 - - - - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 9 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 8 - - - 1000000000.000000000000000 - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - - - - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Automatically adjust orders up or down based on the imbalance of your assets - - - ? - - - 5 - - - - - - - - - - Center price offset based on asset balances - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - - - - Manual center price offset - - - true - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Manually adjust orders up or down. Works independently of other offsets and doesn't override them - - - ? - - - 5 - - - - - - - - - - - 0 - 0 - - - - - 290 - 0 - - - - - 16777215 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - true - - - - 0 - 0 - - - - - 250 - 0 - - - - - 145 - 16777215 - - - - QSlider::groove:horizontal { + + + + 8 + 50 + 120 + 29 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Order size + + + amount_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Fixed order size, expressed in quote asset, unless "relative order size" selected + + + ? + + + 5 + + + + + + + + + 134 + 50 + 322 + 28 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 9 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 8 + + + 1000000000.000000000000000 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + + + + + + + + + 8 + 85 + 120 + 29 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Amount is expressed as a percentage of the account balance of quote/base asset + + + ? + + + 5 + + + + + + + + + 134 + 85 + 147 + 25 + + + + Relative order size + + + + + + 8 + 120 + 120 + 29 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Spread + + + spread_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + The percentage difference between buy and sell + + + ? + + + 5 + + + + + + + + + 134 + 120 + 170 + 28 + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + % + + + 100000.000000000000000 + + + 5.000000000000000 + + + + + + 8 + 155 + 120 + 29 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 101 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Enable dynamic spread which overrides the spread field + + + ? + + + 5 + + + + + + + + + 134 + 155 + 132 + 25 + + + + Dynamic spread + + + + + + 8 + 190 + 120 + 29 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Market depth + + + true + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + From which depth will market spread be measured? (QUOTE amount) + + + ? + + + 5 + + + + + + + + + 134 + 190 + 322 + 28 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 9 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 8 + + + 1000000000.000000000000000 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + + + + + + + + + 8 + 225 + 120 + 29 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Dynamic spread factor + + + true + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + How many percent will own spread be compared to market spread? + + + ? + + + 5 + + + + + + + + + 134 + 225 + 170 + 28 + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + % + + + 0.010000000000000 + + + 1000.000000000000000 + + + 1.000000000000000 + + + + + + 8 + 260 + 120 + 29 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Center price + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Fixed center price expressed in base asset: base/quote + + + ? + + + 5 + + + + + + + + + 134 + 260 + 316 + 28 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 9 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + false + + + + 0 + 0 + + + + + 170 + 0 + + + + ArrowCursor + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + false + + + false + + + 8 + + + 0.000000000000000 + + + 999999999.998999953269958 + + + + + + + true + + + + 0 + 0 + + + + + 120 + 0 + + + + + + + + + + + + + 8 + 295 + 120 + 29 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 101 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Estimate the center from closest opposite orders or from a depth + + + ? + + + 5 + + + + + + + + + 134 + 295 + 298 + 25 + + + + + 0 + 0 + + + + Measure center price from market orders + + + true + + + false + + + + + + 8 + 330 + 120 + 29 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Measurement depth + + + true + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Cumulative quote amount from which depth center price will be measured + + + ? + + + 5 + + + + + + + + + 134 + 330 + 322 + 28 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 9 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 8 + + + 1000000000.000000000000000 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + + + + + + + + + 10 + 470 + 120 + 29 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Automatically adjust orders up or down based on the imbalance of your assets + + + ? + + + 5 + + + + + + + + + 136 + 470 + 312 + 25 + + + + Center price offset based on asset balances + + + + + + 10 + 505 + 120 + 29 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + + + + Manual center price offset + + + true + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Manually adjust orders up or down. Works independently of other offsets and doesn't override them + + + ? + + + 5 + + + + + + + + + 136 + 505 + 290 + 20 + + + + + 0 + 0 + + + + + 290 + 0 + + + + + 16777215 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + + 0 + 0 + + + + + 250 + 0 + + + + + 145 + 16777215 + + + + QSlider::groove:horizontal { border: 1px solid #999999; height: 8px; /* The groove expands to the size of the slider by default. by giving it a height, it has a fixed size */ background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #ffffff, stop:1 #ffffff); @@ -1540,899 +1672,1092 @@ QSlider::handle:horizontal { margin: -2px 0; /* Handle is placed by default on the contents rect of the groove. Expand outside the groove */ border-radius: 3px; } - - - -100 - - - 100 - - - 1 - - - Qt::Horizontal - - - false - - - false - - - QSlider::TicksBelow - - - 20 - - - - - - - - 0 - 0 - - - - - 40 - 16777215 - - - - 0 % - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Reset orders when buy or sell order is partially filled - - - ? - - - 5 - - - - - - - - - - Reset orders on partial fill - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Fill threshold - - - center_price_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Order fill threshold to reset orders - - - ? - - - 5 - - - - - - - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - % - - - 100.000000000000000 - - - 0.000000000000000 - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Reset orders when center price is changed more than threshold - - - ? - - - 5 - - - - - - - - - - Reset orders on center price change - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Price change - - - center_price_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Define center price threshold to react on - - - ? - - - 5 - - - - - - - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - % - - - 100000.000000000000000 - - - 0.000000000000000 - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Override order expiration time to trigger a reset - - - ? - - - 5 - - - - - - - - - - Custom expiration - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Order expiration - - - center_price_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Define custom order expiration time to force orders reset more often, seconds - - - ? - - - 5 - - - - - - - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - 0 - - - 100000000000.000000000000000 - - - 5.000000000000000 - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - External feed - - - amount_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - The bot will try to get price information from this source - - - ? - - - 5 - - - - - - - - - - true - - - - 170 - 16777215 - - - - -1 - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Use external reference price instead of center price acquired from the market - - - ? - - - 5 - - - - - - - - - - Use external source for center price calculation - - - - + + + -100 + + + 100 + + + 1 + + + Qt::Horizontal + + + false + + + false + + + QSlider::TicksBelow + + + 20 + + + + + + + + 0 + 0 + + + + + 40 + 16777215 + + + + 0 % + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + 10 + 540 + 120 + 29 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Reset orders when buy or sell order is partially filled + + + ? + + + 5 + + + + + + + + + 136 + 540 + 197 + 25 + + + + Reset orders on partial fill + + + + + + 10 + 575 + 120 + 29 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Fill threshold + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Order fill threshold to reset orders + + + ? + + + 5 + + + + + + + + + 136 + 575 + 170 + 28 + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + % + + + 100.000000000000000 + + + 0.000000000000000 + + + + + + 10 + 610 + 120 + 29 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Reset orders when center price is changed more than threshold + + + ? + + + 5 + + + + + + + + + 136 + 610 + 263 + 25 + + + + Reset orders on center price change + + + + + + 10 + 645 + 120 + 29 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Price change + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Define center price threshold to react on + + + ? + + + 5 + + + + + + + + + 136 + 645 + 170 + 28 + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + % + + + 100000.000000000000000 + + + 0.000000000000000 + + + + + + 10 + 680 + 120 + 29 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Override order expiration time to trigger a reset + + + ? + + + 5 + + + + + + + + + 136 + 680 + 148 + 25 + + + + Custom expiration + + + + + + 10 + 715 + 120 + 29 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Order expiration + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Define custom order expiration time to force orders reset more often, seconds + + + ? + + + 5 + + + + + + + + + 136 + 717 + 170 + 28 + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 0 + + + 100000000000.000000000000000 + + + 5.000000000000000 + + + + + + 10 + 429 + 120 + 29 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + External feed + + + amount_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + The bot will try to get price information from this source + + + ? + + + 5 + + + + + + + + true + + + + 136 + 429 + 79 + 27 + + + + + 170 + 16777215 + + + + -1 + + + + + + 10 + 394 + 120 + 29 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Use external reference price instead of center price acquired from the market + + + ? + + + 5 + + + + + + + + + 136 + 394 + 335 + 25 + + + + Use external source for center price calculation + + + + + + 8 + 364 + 120 + 29 + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 101 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Use last filled order price as new center price + + + ? + + + 5 + + + + + + + + + 136 + 364 + 335 + 25 + + + + Last filled order price as new center price + + From 49da8a0a3616b983a5aff0846fdf82af86de9832 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Sat, 1 Jun 2019 08:08:53 +0300 Subject: [PATCH 1443/1846] Start to actually use last trade as cp after first round --- dexbot/strategies/relative_orders.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 825c01663..e0c37e877 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -562,6 +562,8 @@ def check_orders(self, *args, **kwargs): # FIXME: Need to write trade operation; possible race condition may occur: while # we're updating order it may be filled further so trade log entry will not # be correct + if need_update: + self.first_round = False # Check center price change when using market center price with reset option on change if self.is_reset_on_price_change and self.is_center_price_dynamic: From a05866f17f2a365165eb54ae02160110437c55a6 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Sat, 1 Jun 2019 18:32:07 +0300 Subject: [PATCH 1444/1846] WIP. Fixes default bool value from 1 to False --- dexbot/strategies/config_parts/relative_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/config_parts/relative_config.py b/dexbot/strategies/config_parts/relative_config.py index 59a26bbba..ba4fdf262 100644 --- a/dexbot/strategies/config_parts/relative_config.py +++ b/dexbot/strategies/config_parts/relative_config.py @@ -54,7 +54,7 @@ def configure(cls, return_base_config=True): ConfigElement('center_price_depth', 'float', 0, 'Measurement depth', 'Cumulative quote amount from which depth center price will be measured', (0.00000001, 1000000000, 8, '')), - ConfigElement('center_price_from_last_trade', bool, 1, 'Last trade price as new center price', + ConfigElement('center_price_from_last_trade', bool, False, 'Last trade price as new center price', 'This will make orders move by half the spread at every fill', None), ConfigElement('center_price_offset', 'bool', False, 'Center price offset based on asset balances', 'Automatically adjust orders up or down based on the imbalance of your assets', None), From cf30f9afe8aaa380b56956c44bd63f54a9f0e055 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Sat, 1 Jun 2019 19:18:12 +0300 Subject: [PATCH 1445/1846] Attempt to make config work again --- dexbot/views/ui/forms/relative_orders_widget.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index 793514fa3..e7a7885af 100644 --- a/dexbot/views/ui/forms/relative_orders_widget.ui +++ b/dexbot/views/ui/forms/relative_orders_widget.ui @@ -2745,7 +2745,7 @@ QSlider::handle:horizontal { - + 136 From 65067d2ec18432af16ea3cbc63a3f6889097dfb7 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 6 Jun 2019 15:17:46 +0500 Subject: [PATCH 1446/1846] Split KOTH to strategy + config --- dexbot/strategies/config_parts/koth_config.py | 88 +++++++++++++++++++ dexbot/strategies/king_of_the_hill.py | 47 +++------- 2 files changed, 100 insertions(+), 35 deletions(-) create mode 100644 dexbot/strategies/config_parts/koth_config.py diff --git a/dexbot/strategies/config_parts/koth_config.py b/dexbot/strategies/config_parts/koth_config.py new file mode 100644 index 000000000..2bd6413b6 --- /dev/null +++ b/dexbot/strategies/config_parts/koth_config.py @@ -0,0 +1,88 @@ +from dexbot.strategies.config_parts.base_config import BaseConfig, ConfigElement + + +class KothConfig(BaseConfig): + @classmethod + def configure(cls, return_base_config=True): + config = [ + ConfigElement( + 'mode', + 'choice', + 'both', + 'Mode', + 'Operational mode', + ([('both', 'Buy + sell'), ('buy', 'Buy only'), ('sell', 'Sell only')]), + ), + ConfigElement( + 'lower_bound', + 'float', + 0, + 'Lower bound', + 'Do not place sell orders lower than this bound', + (0, 10000000, 8, ''), + ), + ConfigElement( + 'upper_bound', + 'float', + 0, + 'Upper bound', + 'Do not place buy orders higher than this bound', + (0, 10000000, 8, ''), + ), + ConfigElement( + 'buy_order_amount', + 'float', + 0, + 'Amount (BASE)', + 'Fixed order size for buy orders, expressed in BASE asset, unless "relative order size"' ' selected', + (0, None, 8, ''), + ), + ConfigElement( + 'sell_order_amount', + 'float', + 0, + 'Amount (QUOTE)', + 'Fixed order size for sell orders, expressed in QUOTE asset, unless "relative order size"' ' selected', + (0, None, 8, ''), + ), + ConfigElement( + 'relative_order_size', + 'bool', + False, + 'Relative order size', + 'Amount is expressed as a percentage of the account balance of quote/base asset', + None, + ), + ConfigElement( + 'buy_order_size_threshold', + 'float', + 0, + 'Ignore smaller buy orders', + 'Ignore buy orders which are smaller than this threshold (BASE). ' + 'If unset, use own order size as a threshold', + (0, None, 8, ''), + ), + ConfigElement( + 'sell_order_size_threshold', + 'float', + 0, + 'Ignore smaller sell orders', + 'Ignore sell orders which are smaller than this threshold (QUOTE). ' + 'If unset, use own order size as a threshold', + (0, None, 8, ''), + ), + ConfigElement( + 'min_order_lifetime', + 'int', + 6, + 'Min order lifetime', + 'Minimum order lifetime before order reset, seconds', + (1, None, ''), + ), + ] + + return BaseConfig.configure(return_base_config) + config + + @classmethod + def configure_details(cls, include_default_tabs=True): + return BaseConfig.configure_details(include_default_tabs) + [] diff --git a/dexbot/strategies/king_of_the_hill.py b/dexbot/strategies/king_of_the_hill.py index 807adc3f9..c018a94df 100644 --- a/dexbot/strategies/king_of_the_hill.py +++ b/dexbot/strategies/king_of_the_hill.py @@ -2,7 +2,9 @@ from datetime import datetime, timedelta # Project imports -from dexbot.strategies.base import StrategyBase, ConfigElement +from dexbot.strategies.base import StrategyBase +from dexbot.strategies.config_parts.koth_config import KothConfig + STRATEGY_NAME = 'King of the Hill' @@ -24,39 +26,11 @@ class Strategy(StrategyBase): @classmethod def configure(cls, return_base_config=True): - return StrategyBase.configure(return_base_config) + [ - ConfigElement('mode', 'choice', 'both', 'Mode', - 'Operational mode', ([ - ('both', 'Buy + sell'), - ('buy', 'Buy only'), - ('sell', 'Sell only')])), - ConfigElement('lower_bound', 'float', 0, 'Lower bound', - 'Do not place sell orders lower than this bound', - (0, 10000000, 8, '')), - ConfigElement('upper_bound', 'float', 0, 'Upper bound', - 'Do not place buy orders higher than this bound', - (0, 10000000, 8, '')), - ConfigElement('buy_order_amount', 'float', 0, 'Amount (BASE)', - 'Fixed order size for buy orders, expressed in BASE asset, unless "relative order size"' - ' selected', (0, None, 8, '')), - ConfigElement('sell_order_amount', 'float', 0, 'Amount (QUOTE)', - 'Fixed order size for sell orders, expressed in QUOTE asset, unless "relative order size"' - ' selected', (0, None, 8, '')), - ConfigElement('relative_order_size', 'bool', False, 'Relative order size', - 'Amount is expressed as a percentage of the account balance of quote/base asset', None), - ConfigElement('buy_order_size_threshold', 'float', 0, 'Ignore smaller buy orders', - 'Ignore buy orders which are smaller than this threshold (BASE). ' - 'If unset, use own order size as a threshold', (0, None, 8, '')), - ConfigElement('sell_order_size_threshold', 'float', 0, 'Ignore smaller sell orders', - 'Ignore sell orders which are smaller than this threshold (QUOTE). ' - 'If unset, use own order size as a threshold', (0, None, 8, '')), - ConfigElement('min_order_lifetime', 'int', 6, 'Min order lifetime', - 'Minimum order lifetime before order reset, seconds', (1, None, '')) - ] + return KothConfig.configure(return_base_config) @classmethod def configure_details(cls, include_default_tabs=True): - return StrategyBase.configure_details(include_default_tabs) + [] + return KothConfig.configure_details(include_default_tabs) def __init__(self, *args, **kwargs): # Initializes StrategyBase class @@ -144,10 +118,13 @@ def check_orders(self): orders_to_delete.append(stored_order['id']) self.place_order(order_type) # Check if someone put order above ours or beaten order was canceled - elif ((order_type == 'buy' and (not self.get_order(self.beaten_buy_order) or - stored_order['price'] < self.buy_price)) or - (order_type == 'sell' and (not self.get_order(self.beaten_sell_order) or - stored_order['price'] ** -1 > self.sell_price))): + elif ( + order_type == 'buy' + and (not self.get_order(self.beaten_buy_order) or stored_order['price'] < self.buy_price) + ) or ( + order_type == 'sell' + and (not self.get_order(self.beaten_sell_order) or stored_order['price'] ** -1 > self.sell_price) + ): self.log.debug('Moving {} order'.format(order_type)) self.cancel_orders(order) orders_to_delete.append(stored_order['id']) From f6fffdcc6be9019dd9f2f5dc2b815000b4a86f37 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 6 Jun 2019 15:22:37 +0500 Subject: [PATCH 1447/1846] Check cancel_orders() return value --- dexbot/strategies/king_of_the_hill.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/dexbot/strategies/king_of_the_hill.py b/dexbot/strategies/king_of_the_hill.py index c018a94df..8f860c038 100644 --- a/dexbot/strategies/king_of_the_hill.py +++ b/dexbot/strategies/king_of_the_hill.py @@ -112,11 +112,10 @@ def check_orders(self): is_partially_filled = self.is_partially_filled(order, threshold=0.8) if is_partially_filled: # If own order filled too much, replace it with new order - # TODO: check cancel_orders() return value after #467 fix self.log.info('Own {} order filled too much, resetting'.format(order_type)) - self.cancel_orders(order) - orders_to_delete.append(stored_order['id']) - self.place_order(order_type) + if self.cancel_orders(order): + orders_to_delete.append(stored_order['id']) + self.place_order(order_type) # Check if someone put order above ours or beaten order was canceled elif ( order_type == 'buy' @@ -126,9 +125,9 @@ def check_orders(self): and (not self.get_order(self.beaten_sell_order) or stored_order['price'] ** -1 > self.sell_price) ): self.log.debug('Moving {} order'.format(order_type)) - self.cancel_orders(order) - orders_to_delete.append(stored_order['id']) - self.place_order(order_type) + if self.cancel_orders(order): + orders_to_delete.append(stored_order['id']) + self.place_order(order_type) # Own order is not there else: self.log.info('Own {} order filled, placing a new one'.format(order_type)) From 0fc1c387ced98965a003b95d06abd82442bf4a5b Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 6 Jun 2019 15:24:43 +0500 Subject: [PATCH 1448/1846] Use new property name to get own orders --- dexbot/strategies/king_of_the_hill.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/king_of_the_hill.py b/dexbot/strategies/king_of_the_hill.py index 8f860c038..b0cdf8368 100644 --- a/dexbot/strategies/king_of_the_hill.py +++ b/dexbot/strategies/king_of_the_hill.py @@ -149,7 +149,7 @@ def calc_order_prices(self): """ # Obtain orderbook orders excluding our orders market_orders = self.get_market_orders(depth=100) - own_orders_ids = [order['id'] for order in self.get_own_orders] + own_orders_ids = [order['id'] for order in self.own_orders] market_orders = [order for order in market_orders if order['id'] not in own_orders_ids] buy_orders = self.filter_buy_orders(market_orders) sell_orders = self.filter_sell_orders(market_orders, invert=True) From 6d58154160e1ef952609b10a8a162b9e01a18ba9 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 6 Jun 2019 15:36:19 +0500 Subject: [PATCH 1449/1846] Add FIXME note --- dexbot/strategies/king_of_the_hill.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dexbot/strategies/king_of_the_hill.py b/dexbot/strategies/king_of_the_hill.py index b0cdf8368..a2d36cb4a 100644 --- a/dexbot/strategies/king_of_the_hill.py +++ b/dexbot/strategies/king_of_the_hill.py @@ -122,6 +122,8 @@ def check_orders(self): and (not self.get_order(self.beaten_buy_order) or stored_order['price'] < self.buy_price) ) or ( order_type == 'sell' + # FIXME: price difference should be compared taking into account asset precisions, or use raw + # amounts and (not self.get_order(self.beaten_sell_order) or stored_order['price'] ** -1 > self.sell_price) ): self.log.debug('Moving {} order'.format(order_type)) From 89d85c0e577f184a24260b61c6ea05c1e9f6ae11 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 6 Jun 2019 15:39:07 +0500 Subject: [PATCH 1450/1846] Move new generic methods into BitsharesOrderEngine --- dexbot/orderengines/bitshares_engine.py | 37 +++++++++++++++++++++++++ dexbot/strategies/base.py | 37 ------------------------- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index 45ddf5d58..1a0f8f35c 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -820,3 +820,40 @@ def get_order(self, order_id, return_none=True): if return_none and order['deleted']: return None return order + + def is_too_small_amounts(self, amount_quote, amount_base): + """ Check whether amounts are within asset precision limits + :param float amount_quote: QUOTE asset amount + :param float amount_base: BASE asset amount + :return: bool True = amounts are too small + False = amounts are within limits + """ + if (amount_quote < 2 * 10 ** -self.market['quote']['precision'] or + amount_base < 2 * 10 ** -self.market['base']['precision']): + return True + + return False + + def is_partially_filled(self, order, threshold=0.3): + """ Checks whether order was partially filled + + :param dict order: Order instance + :param float fill_threshold: Order fill threshold, relative + :return: bool True = Order is filled more than threshold + False = Order is not partially filled + """ + if self.is_buy_order(order): + order_type = 'buy' + price = order['price'] + else: + order_type = 'sell' + price = order['price'] ** -1 + + if order['for_sale']['amount'] != order['base']['amount']: + diff_abs = order['base']['amount'] - order['for_sale']['amount'] + diff_rel = diff_abs / order['base']['amount'] + if diff_rel > threshold: + self.log.debug('Partially filled {} order: {} {} @ {:.8f}, filled: {:.2%}'.format( + order_type, order['base']['amount'], order['base']['symbol'], price, diff_rel)) + return True + return False diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 6b88d8d55..60ae7a2b9 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -191,43 +191,6 @@ def __init__(self, self.orders_log = logging.LoggerAdapter(logging.getLogger('dexbot.orders_log'), {}) - def is_too_small_amounts(self, amount_quote, amount_base): - """ Check whether amounts are within asset precision limits - :param float | amount_quote: QUOTE asset amount - :param float | amount_base: BASE asset amount - :return: bool | True = amounts are too small - False = amounts are within limits - """ - if (amount_quote < 2 * 10 ** -self.market['quote']['precision'] or - amount_base < 2 * 10 ** -self.market['base']['precision']): - return True - - return False - - def is_partially_filled(self, order, threshold=0.3): - """ Checks whether order was partially filled - - :param dict | order: Order instance - :param float | fill_threshold: Order fill threshold, relative - :return: bool | True = Order is filled more than threshold - False = Order is not partially filled - """ - if self.is_buy_order(order): - order_type = 'buy' - price = order['price'] - else: - order_type = 'sell' - price = order['price'] ** -1 - - if order['for_sale']['amount'] != order['base']['amount']: - diff_abs = order['base']['amount'] - order['for_sale']['amount'] - diff_rel = diff_abs / order['base']['amount'] - if diff_rel > threshold: - self.log.debug('Partially filled {} order: {} {} @ {:.8f}, filled: {:.2%}'.format( - order_type, order['base']['amount'], order['base']['symbol'], price, diff_rel)) - return True - return False - def pause(self): """ Pause the worker From 3e50587b266674d4536d5fc124d43e5b7ee719ae Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 6 Jun 2019 19:43:53 +0500 Subject: [PATCH 1451/1846] Fix flake8 warning --- dexbot/controllers/strategy_controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 98d40f5ad..f11513870 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -296,6 +296,7 @@ def validation_errors(self): error_texts.append("Lower bound can't be 0") return error_texts + class KingOfTheHillController(StrategyController): def __init__(self, view, configure, worker_controller, worker_data): From b5433a96815cde8aef5fd8db1addb133d0702615 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 7 Jun 2019 08:53:56 +0500 Subject: [PATCH 1452/1846] Implement in-memory calculation of increased orders Closes: #614 --- dexbot/strategies/staggered_orders.py | 338 ++++++++++++++++++-------- 1 file changed, 231 insertions(+), 107 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 629e5b618..198fd27fa 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -797,112 +797,106 @@ def allocate_asset(self, asset, asset_balance): if self.returnOrderId: self.refresh_orders() - def increase_order_sizes(self, asset, asset_balance, orders): - """ Checks which order should be increased in size and replaces it - with a maximum size order, according to global limits. Logic - depends on mode in question. + def _increase_single_order(self, asset, asset_balance, order, new_order_amount): + """ To avoid code doubling, use this unified function to increase single order + + :param str asset: 'base' or 'quote', depending if checking sell or buy + :param Amount asset_balance: asset balance available for increase + :param order order: order needed to be increased + :param float new_order_amount: BASE or QUOTE amount of a new order (depending on asset) + :return: True = available funds were allocated, cannot allocate remainder + False = not all funds were allocated, can increase more orders next time + :rtype: bool + """ + quote_amount = 0 + base_amount = 0 + price = 0 + order_amount = order['base']['amount'] + order_type = '' + symbol = '' + precision = 0 - Mountain: - Maximize order size as close to center as possible. When all orders are max, the new increase round is - started from the furthest order. + if asset == 'quote': + order_type = 'sell' + symbol = self.market['quote']['symbol'] + precision = self.market['quote']['precision'] + price = order['price'] ** -1 + # New order amount must be at least x2 precision bigger + new_order_amount = max( + new_order_amount, order['base']['amount'] + 2 * 10 ** -self.market['quote']['precision'] + ) + quote_amount = new_order_amount + base_amount = quote_amount * price + elif asset == 'base': + order_type = 'buy' + symbol = self.market['base']['symbol'] + precision = self.market['base']['precision'] + price = order['price'] + # New order amount must be at least x2 precision bigger + new_order_amount = max( + new_order_amount, order['base']['amount'] + 2 * 10 ** -self.market['base']['precision'] + ) + base_amount = new_order_amount + quote_amount = base_amount / price + + needed_balance = new_order_amount - order['for_sale']['amount'] + if asset_balance < needed_balance: + # Balance should be enough to replace partially filled order + self.log.debug( + 'Not enough balance to increase {} order at price {:.8f}: {:.{prec}f}/{:.{prec}f} {}'.format( + order_type, price, asset_balance['amount'], needed_balance, symbol, prec=precision + ) + ) + # Increase finished + return True - Neutral: - Try to flatten everything by increasing order sizes to neutral. When everything is correct, maximize - closest orders and then increase other orders to match that. + self.log.debug( + 'Pre-increasing {} order at price {:.8f} from {:.{prec}f} to {:.{prec}f} {}'.format( + order_type, price, order_amount, new_order_amount, symbol, prec=precision + ) + ) - Valley: - Maximize order sizes as far as possible from center first. When all orders are max, the new increase round - is started from the closest-to-center order. + if asset == 'quote': + order['base']['amount'] = quote_amount + order['for_sale']['amount'] += needed_balance + order['quote']['amount'] = base_amount + asset_balance -= quote_amount - order_amount + elif asset == 'base': + order['base']['amount'] = base_amount + order['for_sale']['amount'] += needed_balance + order['quote']['amount'] = quote_amount + asset_balance -= base_amount - order_amount - Buy slope: - Maximize order size as low as possible. Buy orders maximized as far as possible (same as valley), and sell - orders as close as possible to cp (same as mountain). + # Increase not finished + return False - Sell slope: - Maximize order size as high as possible. Buy orders as close (same as mountain), and sell orders as far as - possible from cp (same as valley). + def _calc_increase(self, asset, asset_balance, orders): + """ Calculate increased order sizes for specified orders with inplace replacement of order amounts. + Only one increase is performed at a time. - :param str | asset: 'base' or 'quote', depending if checking sell or buy - :param Amount | asset_balance: Balance of the account - :param list | orders: List of buy or sell orders - :return bool | True = all available funds was allocated - False = not all funds was allocated, can increase more orders next time + :param str asset: 'base' or 'quote', depending if checking sell or buy + :param Amount asset_balance: Balance of the account + :param list orders: List of buy or sell orders + :return: True = all available funds were allocated + False = not all funds was allocated, can increase more orders next time + :rtype: bool """ - def increase_single_order(asset, order, new_order_amount): - """ To avoid code doubling, use this unified function to increase single order - - :param str | asset: 'base' or 'quote', depending if checking sell or buy - :param order | order: order needed to be increased - :param float | new_order_amount: BASE or QUOTE amount of a new order (depending on asset) - :return bool | True = available funds was allocated, cannot allocate remainder - False = not all funds was allocated, can increase more orders next time - """ - quote_amount = 0 - price = 0 - order_type = '' - order_amount = order['base']['amount'] - - if asset == 'quote': - order_type = 'sell' - price = (order['price'] ** -1) - # New order amount must be at least x2 precision bigger - new_order_amount = max( - new_order_amount, order['base']['amount'] + 2 * 10 ** -self.market['quote']['precision'] - ) - quote_amount = new_order_amount - elif asset == 'base': - order_type = 'buy' - price = order['price'] - # New order amount must be at least x2 precision bigger - new_order_amount = max(new_order_amount, - order['base']['amount'] + 2 * 10 ** -self.market['base']['precision']) - quote_amount = new_order_amount / price - - if asset_balance < new_order_amount - order['for_sale']['amount']: - # Balance should be enough to replace partially filled order - self.log.debug('Not enough balance to increase {} order at price {:.8f}' - .format(order_type, price)) - return True - - self.log.info('Increasing {} order at price {:.8f} from {:.{prec}f} to {:.{prec}f} {}' - .format(order_type, price, order_amount, new_order_amount, symbol, prec=precision)) - self.log.debug('Cancelling {} order in increase_order_sizes(); mode: {}, amount: {}, price: {:.8f}' - .format(order_type, self.mode, order_amount, price)) - self.cancel_orders_wrapper(order) - if asset == 'quote': - if isinstance(order, VirtualOrder): - self.place_virtual_sell_order(quote_amount, price) - else: - self.place_market_sell_order(quote_amount, price) - elif asset == 'base': - if isinstance(order, VirtualOrder): - self.place_virtual_buy_order(quote_amount, price) - else: - self.place_market_buy_order(quote_amount, price) - - # Only one increase at a time. This prevents running more than one increment round simultaneously - return False - - total_balance = 0 - symbol = '' - precision = 0 new_order_amount = 0 furthest_order_bound = 0 + total_balance = 0 if asset == 'quote': total_balance = self.quote_total_balance - symbol = self.market['quote']['symbol'] - precision = self.market['quote']['precision'] elif asset == 'base': total_balance = self.base_total_balance - symbol = self.market['base']['symbol'] - precision = self.market['base']['precision'] # Mountain mode: - if (self.mode == 'mountain' or - (self.mode == 'buy_slope' and asset == 'quote') or - (self.mode == 'sell_slope' and asset == 'base')): + if ( + self.mode == 'mountain' + or (self.mode == 'buy_slope' and asset == 'quote') + or (self.mode == 'sell_slope' and asset == 'base') + ): """ Starting from the furthest order. For each order, see if it is approximately maximum size. If it is, move on to next. @@ -947,8 +941,10 @@ def increase_single_order(asset, order, new_order_amount): further_bound = further_order['base']['amount'] * (1 + self.increment) - if (further_bound > order_amount * (1 + self.increment / 10) < closer_bound and - further_bound - order_amount >= order_amount * self.increment / 2): + if ( + further_bound > order_amount * (1 + self.increment / 10) < closer_bound + and further_bound - order_amount >= order_amount * self.increment / 2 + ): # Calculate new order size and place the order to the market """ To prevent moving liquidity away from center, let new order be no more than `order_amount * increase_factor`. This is for situations when we increasing order on side which was previously @@ -999,11 +995,13 @@ def increase_single_order(asset, order, new_order_amount): self.log.debug('Deactivating max increase mode for mountain mode') self.mountain_max_increase_mode = False - return increase_single_order(asset, order, new_order_amount) + return self._increase_single_order(asset, asset_balance, order, new_order_amount) - elif (self.mode == 'valley' or - (self.mode == 'buy_slope' and asset == 'base') or - (self.mode == 'sell_slope' and asset == 'quote')): + elif ( + self.mode == 'valley' + or (self.mode == 'buy_slope' and asset == 'base') + or (self.mode == 'sell_slope' and asset == 'quote') + ): """ Starting from the furthest order, for each order, see if it is approximately maximum size. If it is, move on to next. @@ -1053,9 +1051,11 @@ def increase_single_order(asset, order, new_order_amount): order_amount_normalized = order_amount * (1 + self.increment / 10) need_increase = False - if (order_amount_normalized < further_order_bound and - further_order_bound - order_amount >= order_amount * self.increment / 2 and - order_amount_normalized < closest_order_bound): + if ( + order_amount_normalized < further_order_bound + and further_order_bound - order_amount >= order_amount * self.increment / 2 + and order_amount_normalized < closest_order_bound + ): """ Check whether order amount is less than further order and also less than `closer order + increment`. We need this check to be able to increase closer orders more smoothly. Here is the example: @@ -1079,8 +1079,10 @@ def increase_single_order(asset, order, new_order_amount): # Skip order if new amount is less than current for any reason need_increase = False - elif (order_amount_normalized < closer_order_bound and - closer_order_bound - order_amount >= order_amount * self.increment / 2): + elif ( + order_amount_normalized < closer_order_bound + and closer_order_bound - order_amount >= order_amount * self.increment / 2 + ): """ Check whether order amount is less than closer or order and the diff is more than 50% of one increment. Note: we can use only 50% or less diffs. Bigger will not work. For example, with diff 80% an order may have an actual difference like 30% from closer and 70% from further. @@ -1089,7 +1091,7 @@ def increase_single_order(asset, order, new_order_amount): need_increase = True if need_increase: - return increase_single_order(asset, order, new_order_amount) + return self._increase_single_order(asset, asset_balance, order, new_order_amount) elif self.mode == 'neutral': """ Starting from the furthest order, for each order, see if it is approximately @@ -1146,8 +1148,10 @@ def increase_single_order(asset, order, new_order_amount): need_increase = False order_amount_normalized = order_amount * (1 + self.increment / 10) - if (order_amount_normalized < further_order_bound and - further_order_bound - order_amount >= order_amount * (math.sqrt(1 + self.increment) - 1) / 2): + if ( + order_amount_normalized < further_order_bound + and further_order_bound - order_amount >= order_amount * (math.sqrt(1 + self.increment) - 1) / 2 + ): # Order is less than further order and diff is more than `increment / 2` if is_closest_order: @@ -1167,17 +1171,137 @@ def increase_single_order(asset, order, new_order_amount): new_order_amount = min(order['base']['amount'] * (1 + self.increment), further_order_bound) need_increase = True - elif (order_amount_normalized < closer_order_bound and - closer_order_bound - order_amount >= order_amount * (math.sqrt(1 + self.increment) - 1) / 2): + elif ( + order_amount_normalized < closer_order_bound + and closer_order_bound - order_amount >= order_amount * (math.sqrt(1 + self.increment) - 1) / 2 + ): # Order is less than closer order and diff is more than `increment / 2` new_order_amount = closer_order_bound need_increase = True if need_increase: - return increase_single_order(asset, order, new_order_amount) + return self._increase_single_order(asset, asset_balance, order, new_order_amount) - return None + def increase_order_sizes(self, asset, asset_balance, orders): + """ Checks which order should be increased in size and replaces it + with a maximum size order, according to global limits. Logic + depends on mode in question. + + Mountain: + Maximize order size as close to center as possible. When all orders are max, the new increase round is + started from the furthest order. + + Neutral: + Try to flatten everything by increasing order sizes to neutral. When everything is correct, maximize + closest orders and then increase other orders to match that. + + Valley: + Maximize order sizes as far as possible from center first. When all orders are max, the new increase round + is started from the closest-to-center order. + + Buy slope: + Maximize order size as low as possible. Buy orders maximized as far as possible (same as valley), and sell + orders as close as possible to cp (same as mountain). + + Sell slope: + Maximize order size as high as possible. Buy orders as close (same as mountain), and sell orders as far as + possible from cp (same as valley). + + :param str asset: 'base' or 'quote', depending if checking sell or buy + :param Amount asset_balance: Balance of the account + :param list orders: List of buy or sell orders + :return: True = all available funds were allocated + False = not all funds were allocated, can increase more orders next time + :rtype: bool + """ + + # Create temp order list (copy.deepcopy() doesn't work here) + temp_orders = [] + for order in orders: + tmp_order = { + 'base': {'amount': order['base']['amount']}, + 'quote': {'amount': order['quote']['amount']}, + 'for_sale': {'amount': order['for_sale']['amount']}, + 'price': order['price'], + } + temp_orders.append(tmp_order) + + # Get calculated increased orders + increase_finished = False + while not increase_finished: + increase_finished = self._calc_increase(asset, asset_balance, temp_orders) + + price = 0 + order_type = '' + symbol = '' + opposite_symbol = '' + precision = 0 + opposite_precision = 0 + + if asset == 'quote': + order_type = 'sell' + symbol = self.market['quote']['symbol'] + opposite_symbol = self.market['base']['symbol'] + precision = self.market['quote']['precision'] + opposite_precision = self.market['base']['precision'] + elif asset == 'base': + order_type = 'buy' + symbol = self.market['base']['symbol'] + opposite_symbol = self.market['quote']['symbol'] + precision = self.market['base']['precision'] + opposite_precision = self.market['quote']['precision'] + + # We're iterating in reverse manner to place further orders first + orders = list(reversed(orders)) + temp_orders = list(reversed(temp_orders)) + + for index, order in enumerate(temp_orders): + if order['base']['amount'] != orders[index]['base']['amount']: + price = order['price'] if asset == 'base' else order['price'] ** -1 + old_amount = orders[index]['base']['amount'] + new_amount = order['base']['amount'] + old_opposite_amount = orders[index]['quote']['amount'] + new_opposite_amount = order['quote']['amount'] + self.log.info( + 'Increasing {} order at price {:.8f}, {:.{prec}f} -> {:.{prec}f} {}, ' + '({:.{opposite_prec}f} -> {:.{opposite_prec}f} {})'.format( + order_type, + price, + old_amount, + new_amount, + symbol, + old_opposite_amount, + new_opposite_amount, + opposite_symbol, + prec=precision, + opposite_prec=opposite_precision, + ) + ) + self.log.debug( + 'Cancelling {} order in increase_order_sizes(); mode: {}, amount: {}, price: {:.8f}'.format( + order_type, self.mode, old_amount, price + ) + ) + self.cancel_orders_wrapper(orders[index]) + + if asset == 'quote': + if isinstance(orders[index], VirtualOrder): + self.place_virtual_sell_order(order['base']['amount'], price) + else: + self.place_market_sell_order(order['base']['amount'], price) + elif asset == 'base': + if isinstance(orders[index], VirtualOrder): + self.place_virtual_buy_order(order['quote']['amount'], price) + else: + self.place_market_buy_order(order['quote']['amount'], price) + + # Limit number of operations to send at once + if len(self.bitshares.txbuffer.ops) > 10: + return False + + # All funds were used + return True def check_partial_fill(self, order, fill_threshold=None): """ Checks whether order was partially filled it needs to be replaced From 8142113044f089fea83e0a4204e274d5703cf5a4 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 7 Jun 2019 09:36:18 +0500 Subject: [PATCH 1453/1846] Fix closest order calculation for neutral mode Closest order caclulation was a bit wrong which caused an error with new in-memory caclulation logic. --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 198fd27fa..6318d7998 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1135,11 +1135,11 @@ def _calc_increase(self, asset, asset_balance, orders): new_orders_sum = 0 amount = order_amount - for o in orders: + for _ in orders: new_orders_sum += amount amount = amount / math.sqrt(1 + self.increment) virtual_furthest_order_bound = amount * (total_balance / new_orders_sum) - new_amount = order_amount * (total_balance / new_orders_sum) + new_amount = order_amount * (total_balance / new_orders_sum) / self.min_increase_factor if new_amount > closer_order_bound and virtual_furthest_order_bound > furthest_order_bound: # Maximize order up to max possible amount if we can From ca5eb8a344a50e05800e60aa2cb187b9495324a8 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 7 Jun 2019 10:15:38 +0500 Subject: [PATCH 1454/1846] Fix furthest order calculation for mountain mode --- 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 6318d7998..1ca3603e4 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -976,7 +976,7 @@ def _calc_increase(self, asset, asset_balance, orders): amount = amount * (1 + self.increment) new_orders_sum += amount # To reduce allocation rounds, increase furthest order more - new_order_amount = order_amount * (total_balance / new_orders_sum) * (1 + self.increment) + new_order_amount = order_amount * (total_balance / new_orders_sum) if new_order_amount < closer_bound: """ This is for situations when calculated new_order_amount is not big enough to From 1884606582f13af092fbb8055442764fb2c0f688 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 7 Jun 2019 10:47:08 +0500 Subject: [PATCH 1455/1846] Fix SO tests for new in-memory increases --- .../test_staggered_orders_complex.py | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index a4f2c64f9..f700b0c85 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -236,11 +236,11 @@ def test_increase_order_sizes_valley_transit_from_mountain(worker, do_initial_al do_initial_allocation(worker, 'mountain') # Switch to valley worker.mode = 'valley' - # Add balance to increase several orders - to_issue = worker.buy_orders[0]['base']['amount'] * 10 - issue_asset(worker.market['base']['symbol'], to_issue, worker.account.name) for _ in range(0, 6): + # Add balance to increase ~1 order + to_issue = worker.buy_orders[0]['base']['amount'] + issue_asset(worker.market['base']['symbol'], to_issue, worker.account.name) previous_buy_orders = worker.buy_orders worker.refresh_balances() worker.increase_order_sizes('base', worker.base_balance, previous_buy_orders) @@ -391,7 +391,7 @@ def test_increase_order_sizes_mountain_basic(worker, do_initial_allocation, issu ) -def test_increase_order_sizes_mountain_direction(worker, do_initial_allocation, issue_asset): +def test_increase_order_sizes_mountain_direction(worker, do_initial_allocation, issue_asset, increase_until_allocated): """ Test increase direction in mountain mode Buy side, amounts in QUOTE: @@ -402,12 +402,14 @@ def test_increase_order_sizes_mountain_direction(worker, do_initial_allocation, 15 15 15 10 10 """ do_initial_allocation(worker, 'mountain') + increase_until_allocated(worker) worker.mode = 'mountain' - - # Double worker's balance - issue_asset(worker.market['base']['symbol'], worker.base_total_balance, worker.account.name) + increase_factor = max(1 + worker.increment, worker.min_increase_factor) for _ in range(0, 6): + # Add balance to increase ~1 order + to_issue = worker.buy_orders[0]['base']['amount'] * (increase_factor - 1) + issue_asset(worker.market['base']['symbol'], to_issue, worker.account.name) previous_buy_orders = worker.buy_orders worker.refresh_balances() worker.increase_order_sizes('base', worker.base_balance, previous_buy_orders) @@ -471,29 +473,30 @@ def test_increase_order_sizes_mountain_imbalanced(worker, do_initial_allocation) base_limit = initial_base / 2 # Add own_asset_limit only for first new order worker.place_closer_order('base', worker.buy_orders[0], own_asset_limit=base_limit) + worker.refresh_orders() for _ in range(1, num_orders_to_cancel): worker.place_closer_order('base', worker.buy_orders[0]) worker.refresh_orders() + previous_buy_orders = worker.buy_orders + for _ in range(0, num_orders_to_cancel): - previous_buy_orders = worker.buy_orders worker.refresh_balances() worker.increase_order_sizes('base', worker.base_balance, previous_buy_orders) worker.refresh_orders() - for order in worker.buy_orders: - order_index = worker.buy_orders.index(order) - - if ( - previous_buy_orders[order_index]['quote']['amount'] - < previous_buy_orders[order_index + 1]['quote']['amount'] - and previous_buy_orders[order_index + 1]['base']['amount'] - - previous_buy_orders[order_index]['base']['amount'] - > previous_buy_orders[order_index]['base']['amount'] * worker.increment / 2 - ): - # If order before increase was smaller than further order, expect to see it increased - assert order['quote']['amount'] > previous_buy_orders[order_index]['quote']['amount'] - break + for order_index in range(0, num_orders_to_cancel): + order = worker.buy_orders[order_index] + if ( + previous_buy_orders[order_index]['quote']['amount'] + < previous_buy_orders[order_index + 1]['quote']['amount'] + and previous_buy_orders[order_index + 1]['base']['amount'] + - previous_buy_orders[order_index]['base']['amount'] + > previous_buy_orders[order_index]['base']['amount'] * worker.increment / 2 + ): + # If order before increase was smaller than further order, expect to see it increased + assert order['quote']['amount'] > previous_buy_orders[order_index]['quote']['amount'] + break def test_increase_order_sizes_neutral_basic(worker, do_initial_allocation, issue_asset, increase_until_allocated): From b9257b3538c8b687ac5b2e06d9b91edd40ccbaae Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 4 Feb 2019 01:03:47 +0500 Subject: [PATCH 1456/1846] Fix typo --- 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 1ca3603e4..716c08cf0 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -710,7 +710,7 @@ def allocate_asset(self, asset, asset_balance): # Target spread is reached, let's allocate remaining funds if not self.check_partial_fill(closest_own_order, fill_threshold=0): """ Detect partially filled order on the own side and reserve funds to replace order in case - opposite oreder will be fully filled. + opposite order will be fully filled. """ funds_to_reserve = closest_own_order['base']['amount'] self.log.debug('Partially filled order on own side, reserving funds to replace: ' From cc3f895cdf9846ef5f61ebadbdaeccb74c9e8270 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 4 Feb 2019 00:35:27 +0500 Subject: [PATCH 1457/1846] Refactor orders increase logic Prevent moving liquidity away from center in valley and neutral modes. Closes: #444, #586 --- dexbot/strategies/staggered_orders.py | 68 ++++++++++++------- .../test_staggered_orders_complex.py | 65 ++++++++++-------- 2 files changed, 81 insertions(+), 52 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 716c08cf0..81be4e6e8 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -885,11 +885,17 @@ def _calc_increase(self, asset, asset_balance, orders): new_order_amount = 0 furthest_order_bound = 0 total_balance = 0 + symbol = '' + precision = 0 if asset == 'quote': total_balance = self.quote_total_balance + symbol = self.market['quote']['symbol'] + precision = self.market['quote']['precision'] elif asset == 'base': total_balance = self.base_total_balance + symbol = self.market['base']['symbol'] + precision = self.market['base']['precision'] # Mountain mode: if ( @@ -1016,8 +1022,11 @@ def _calc_increase(self, asset, asset_balance, orders): orders_count = len(orders) orders = list(reversed(orders)) + # To speed up the process, use at least N% increases + increase_factor = max(1 + self.increment, self.min_increase_factor) + closest_order = orders[-1] - closest_order_bound = closest_order['base']['amount'] * (1 + self.increment) + closest_order_bound = closest_order['base']['amount'] * increase_factor for order in orders: order_index = orders.index(order) @@ -1070,8 +1079,6 @@ def _calc_increase(self, asset, asset_balance, orders): """ need_increase = True - # To speed up the process, use at least N% increases - increase_factor = max(1 + self.increment, self.min_increase_factor) # Do not allow to increase more than further order amount new_order_amount = min(closer_order_bound * increase_factor, further_order_bound) @@ -1081,13 +1088,25 @@ def _calc_increase(self, asset, asset_balance, orders): elif ( order_amount_normalized < closer_order_bound - and closer_order_bound - order_amount >= order_amount * self.increment / 2 + and order_amount_normalized < closest_order_bound + and closer_order_bound - order_amount >= order_amount * (math.sqrt(1 + self.increment) - 1) / 2 ): """ Check whether order amount is less than closer or order and the diff is more than 50% of one increment. Note: we can use only 50% or less diffs. Bigger will not work. For example, with diff 80% an order may have an actual difference like 30% from closer and 70% from further. + + Also prevent moving liqudity away from closer-to-center orders. Instead of increasing "80" + orders, increase closer-to-center orders first: + + [80 80 80 100 100 100 60 50 40 40] + [80 80 80 100 100 100 60 50 50 40] + [80 80 80 100 100 100 60 50 50 50] + ... + [80 80 80 100 100 100 60 60 60 60] + ... + [80 80 80 100 100 100 80 80 80 80] """ - new_order_amount = closer_order_bound + new_order_amount = min(closest_order_bound, closer_order_bound) need_increase = True if need_increase: @@ -1109,11 +1128,14 @@ def _calc_increase(self, asset, asset_balance, orders): orders_count = len(orders) orders = list(reversed(orders)) closest_order = orders[-1] - previous_amount = 0 + increase_factor = max(1 + self.increment, self.min_increase_factor) + initial_closest_order_bound = closest_order['base']['amount'] * increase_factor for order in orders: order_index = orders.index(order) + reverse_index = orders_count - order_index order_amount = order['base']['amount'] + closest_order_bound = initial_closest_order_bound if order_index == 0: # This is a furthest order @@ -1129,9 +1151,11 @@ def _calc_increase(self, asset, asset_balance, orders): closer_order = orders[order_index + 1] closer_order_bound = closer_order['base']['amount'] / math.sqrt(1 + self.increment) is_closest_order = False + # What size current order may be based on initial closest order bound + closest_order_bound = initial_closest_order_bound / (math.sqrt(1 + self.increment) ** reverse_index) else: is_closest_order = True - closer_order_bound = order['base']['amount'] * (1 + self.increment) + closer_order_bound = initial_closest_order_bound new_orders_sum = 0 amount = order_amount @@ -1143,6 +1167,7 @@ def _calc_increase(self, asset, asset_balance, orders): if new_amount > closer_order_bound and virtual_furthest_order_bound > furthest_order_bound: # Maximize order up to max possible amount if we can + # New order may be feeling bigger than expected after mountain -> neutral transition, it's ok closer_order_bound = new_amount need_increase = False @@ -1150,34 +1175,29 @@ def _calc_increase(self, asset, asset_balance, orders): if ( order_amount_normalized < further_order_bound + and order_amount_normalized < closest_order_bound and further_order_bound - order_amount >= order_amount * (math.sqrt(1 + self.increment) - 1) / 2 ): # Order is less than further order and diff is more than `increment / 2` - + # Order is also less than previously calculated closest_order_bound if is_closest_order: - new_order_amount = closer_order_bound - need_increase = True + # At first, maximize order as we can + new_order_amount = max(closer_order_bound, further_order_bound) else: - price = closest_order['price'] - amount = closest_order['base']['amount'] - while price > order['price'] * (1 + self.increment / 10): - # Calculate closer order amount based on current closest order - previous_amount = amount - price = price / (1 + self.increment) - amount = amount / math.sqrt(1 + self.increment) - if order_amount_normalized < previous_amount: - # Current order is less than virtually calculated next order - # Do not allow to increase more than further order amount - new_order_amount = min(order['base']['amount'] * (1 + self.increment), further_order_bound) - need_increase = True + # Current order is less than virtually calculated next order (closest_order_bound) + # Do not allow to increase more than further order amount + new_order_amount = min(order['base']['amount'] * increase_factor, further_order_bound) + need_increase = True elif ( order_amount_normalized < closer_order_bound + and order_amount_normalized < closest_order_bound and closer_order_bound - order_amount >= order_amount * (math.sqrt(1 + self.increment) - 1) / 2 ): # Order is less than closer order and diff is more than `increment / 2` - - new_order_amount = closer_order_bound + # Order is also less than virtually calculated closest_order_bound, this prevents moving liquidity + # away from center, see similar code in Valley mode for description + new_order_amount = min(closer_order_bound, closest_order_bound) need_increase = True if need_increase: diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index f700b0c85..7bba306dc 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -298,10 +298,9 @@ def test_increase_order_sizes_valley_smaller_closest_orders(worker, do_initial_a assert worker.sell_orders[0]['base']['amount'] == initial_quote -@pytest.mark.xfail(reason='https://github.com/Codaone/DEXBot/issues/444') def test_increase_order_sizes_valley_imbalaced_small_further(worker, do_initial_allocation, increase_until_allocated): - """ - TODO: test stub for https://github.com/Codaone/DEXBot/pull/484 + """ If furthest orders are smaller than closest, they should be increased first. + See https://github.com/Codaone/DEXBot/issues/444 for details Buy side, amounts in BASE: @@ -334,19 +333,22 @@ def test_increase_order_sizes_valley_imbalaced_small_further(worker, do_initial_ worker.place_market_buy_order(to_buy, further_order['price']) worker.refresh_orders() - # Drop excess balance, the goal is to keep balance to only increase furthest orders - amount = Amount( - base_limit * num_orders_to_cancel, worker.market['base']['symbol'], bitshares_instance=worker.bitshares - ) + # Drop excess balance to only allow to one increase round + worker.refresh_balances() + increase_factor = max(1 + worker.increment, worker.min_increase_factor) + to_keep = base_limit * (increase_factor - 1) * num_orders_to_cancel * 2 * 1.01 + to_drop = worker.base_balance['amount'] - to_keep + amount = Amount(to_drop, worker.market['base']['symbol'], bitshares_instance=worker.bitshares) worker.bitshares.reserve(amount, account=worker.account) increase_until_allocated(worker) for i in range(1, num_orders_to_cancel): - assert worker.buy_orders[-i]['base']['amount'] == worker.buy_orders[i - 1]['base']['amount'] + further_order_amount = worker.buy_orders[-i]['base']['amount'] + closer_order_amount = worker.buy_orders[i - 1]['base']['amount'] + assert further_order_amount == closer_order_amount -@pytest.mark.xfail(reason='https://github.com/Codaone/DEXBot/issues/586') def test_increase_order_sizes_valley_closest_order(worker, do_initial_allocation, issue_asset): """ Should test proper calculation of closest order: order should not be less that min_increase_factor """ @@ -631,10 +633,9 @@ def test_increase_order_sizes_neutral_smaller_closest_orders(worker, do_initial_ assert worker.sell_orders[0]['base']['amount'] == initial_quote -@pytest.mark.xfail(reason='https://github.com/Codaone/DEXBot/issues/444') def test_increase_order_sizes_neutral_imbalaced_small_further(worker, do_initial_allocation, increase_until_allocated): - """ - TODO: test stub for https://github.com/Codaone/DEXBot/pull/484 + """ If furthest orders are smaller than closest, they should be increased first. + See https://github.com/Codaone/DEXBot/issues/444 for details Buy side, amounts in BASE: @@ -645,7 +646,6 @@ def test_increase_order_sizes_neutral_imbalaced_small_further(worker, do_initial 10 10 10 100 100 10 10 10
""" worker = do_initial_allocation(worker, 'neutral') - increase_until_allocated(worker) # Cancel several closest orders num_orders_to_cancel = 3 @@ -658,28 +658,37 @@ def test_increase_order_sizes_neutral_imbalaced_small_further(worker, do_initial # Place limited orders initial_base = worker.buy_orders[0]['base']['amount'] base_limit = initial_base / 2 - for i in range(0, num_orders_to_cancel): - worker.place_closer_order('base', worker.buy_orders[0], own_asset_limit=base_limit) - # place_further_order() doesn't have own_asset_limit, so do own calculation - further_order = worker.place_further_order('base', worker.buy_orders[-1], place_order=False) - worker.place_market_buy_order(base_limit / further_order['price'], further_order['price']) + # Apply limit only for first order + worker.place_closer_order('base', worker.buy_orders[0], own_asset_limit=base_limit) + # place_further_order() doesn't have own_asset_limit, so do own calculation + further_order = worker.place_further_order('base', worker.buy_orders[-1], place_order=False) + worker.place_market_buy_order(base_limit / further_order['price'], further_order['price']) + worker.refresh_orders() + + # Place remainig limited orders + for i in range(1, num_orders_to_cancel): + worker.place_closer_order('base', worker.buy_orders[0]) + worker.place_further_order('base', worker.buy_orders[-1]) worker.refresh_orders() - # Drop excess balance, the goal is to keep balance to only increase furthest orders - amount = Amount( - base_limit * num_orders_to_cancel, worker.market['base']['symbol'], bitshares_instance=worker.bitshares - ) + # Drop excess balance to only allow to one increase round + worker.refresh_balances() + increase_factor = max(1 + worker.increment, worker.min_increase_factor) + to_keep = base_limit * (increase_factor - 1) * num_orders_to_cancel * 2 * 1.1 + to_drop = worker.base_balance['amount'] - to_keep + amount = Amount(to_drop, worker.market['base']['symbol'], bitshares_instance=worker.bitshares) worker.bitshares.reserve(amount, account=worker.account) increase_until_allocated(worker) for i in range(1, num_orders_to_cancel): - # TODO: this is a simple check without precise calculation + # This is a simple check without precise calculation # We're roughly checking that new furthest orders are not exceed new closest orders - assert worker.buy_orders[-i]['base']['amount'] < worker.buy_orders[i - 1]['base']['amount'] + further_order_amount = worker.buy_orders[-i]['base']['amount'] + closer_order_amount = worker.buy_orders[i - 1]['base']['amount'] + assert further_order_amount < closer_order_amount -@pytest.mark.xfail(reason='https://github.com/Codaone/DEXBot/issues/586') def test_increase_order_sizes_neutral_closest_order(worker, do_initial_allocation, issue_asset): """ Should test proper calculation of closest order: order should not be less that min_increase_factor """ @@ -695,9 +704,9 @@ def test_increase_order_sizes_neutral_closest_order(worker, do_initial_allocatio worker.increase_order_sizes('base', worker.base_balance, previous_buy_orders) worker.refresh_orders() - assert worker.buy_orders[0]['base']['amount'] - previous_buy_orders[0]['base']['amount'] >= previous_buy_orders[0][ - 'base' - ]['amount'] * (increase_factor - 1) + assert worker.buy_orders[0]['base']['amount'] - previous_buy_orders[0]['base']['amount'] == pytest.approx( + previous_buy_orders[0]['base']['amount'] * (increase_factor - 1), rel=(1 ** -worker.market['base']['precision']) + ) def test_increase_order_sizes_buy_slope(worker, do_initial_allocation, issue_asset, increase_until_allocated): From 3b35b6ccaa3e2e78720ae7c84ed33149785f26f8 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 20 Feb 2019 00:10:27 +0500 Subject: [PATCH 1458/1846] Update furthest order increase in mountain mode Don't allow too small increase steps in mountain mode. This will reduce number of calculations. Closes: #585 --- dexbot/strategies/staggered_orders.py | 23 ++++++++----------- .../test_staggered_orders_complex.py | 12 +++++----- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 81be4e6e8..18e902d7a 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -970,29 +970,26 @@ def _calc_increase(self, asset, asset_balance, orders): examining furthest order. """ new_order_amount = further_bound + increase_factor = max(1 + self.increment, self.min_increase_factor) if not self.mountain_max_increase_mode: - increase_factor = max(1 + self.increment, self.min_increase_factor) + # Smooth increase for orders between furthest and closest (see docstring example) new_order_amount = min(further_bound, order_amount * increase_factor) if is_least_order: + new_order_amount = order_amount * increase_factor new_orders_sum = 0 amount = order_amount for o in orders: amount = amount * (1 + self.increment) new_orders_sum += amount - # To reduce allocation rounds, increase furthest order more - new_order_amount = order_amount * (total_balance / new_orders_sum) - - if new_order_amount < closer_bound: - """ This is for situations when calculated new_order_amount is not big enough to - allocate all funds. Use partial-increment increase, so we'll got at least one full - increase round. Whether we will just use `new_order_amount = further_bound`, we will - get less than one full allocation round, thus leaving closest-to-center order not - increased. - """ - new_order_amount = closer_bound / (1 + self.increment * 0.2) - else: + # To reduce allocation rounds, increase furthest order more if we can + increased_amount = order_amount * (total_balance / new_orders_sum) + + if increased_amount > new_order_amount: + self.log.debug('Correcting furthest order amount from {:.{prec}f} to: {:.{prec}f} {}' + .format(new_order_amount, increased_amount, symbol, prec=precision)) + new_order_amount = increased_amount # Set bypass flag to not limit next orders self.mountain_max_increase_mode = True self.log.debug('Activating max increase mode for mountain mode') diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index 7bba306dc..f4d448f03 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -429,16 +429,15 @@ def test_increase_order_sizes_mountain_direction(worker, do_initial_allocation, break -@pytest.mark.xfail(reason='https://github.com/Codaone/DEXBot/issues/585') def test_increase_order_sizes_mountain_furthest_order(worker, do_initial_allocation, issue_asset): """ Should test proper calculation of furthest order: try to maximize, don't allow too small increase """ do_initial_allocation(worker, 'mountain') worker.mode = 'mountain' - # Add balance to increase 2 orders + # Add balance to increase ~2 orders increase_factor = max(1 + worker.increment, worker.min_increase_factor) - to_issue = worker.buy_orders[0]['base']['amount'] * (increase_factor - 1) * 2 + to_issue = worker.buy_orders[-1]['base']['amount'] * (increase_factor - 1) * 2.2 issue_asset(worker.market['base']['symbol'], to_issue, worker.account.name) previous_buy_orders = worker.buy_orders @@ -446,9 +445,10 @@ def test_increase_order_sizes_mountain_furthest_order(worker, do_initial_allocat worker.increase_order_sizes('base', worker.base_balance, previous_buy_orders) worker.refresh_orders() - assert worker.buy_orders[-1]['base']['amount'] - previous_buy_orders[-1]['base']['amount'] >= previous_buy_orders[ - -1 - ]['base']['amount'] * (increase_factor - 1) + assert worker.buy_orders[-1]['base']['amount'] - previous_buy_orders[-1]['base']['amount'] == pytest.approx( + previous_buy_orders[-1]['base']['amount'] * (increase_factor - 1), + rel=(1 ** -worker.market['base']['precision']), + ) def test_increase_order_sizes_mountain_imbalanced(worker, do_initial_allocation): From 1a8d9df591d0a79d08a3e4db7eccdee4a27d2868 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 7 Jun 2019 16:03:20 +0500 Subject: [PATCH 1459/1846] Fix closest order increase in valley mode --- 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 18e902d7a..d812a4a47 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1052,7 +1052,7 @@ def _calc_increase(self, asset, asset_balance, orders): new_amount = (total_balance / orders_count) / (1 + self.increment / 100) if furthest_order_bound < new_amount > closer_order_bound: # Maximize order up to max possible amount if we can - closer_order_bound = new_amount + closer_order_bound = closest_order_bound = new_amount order_amount_normalized = order_amount * (1 + self.increment / 10) need_increase = False From 371339833649098f81b36a53ad3b594474ff6ed7 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 7 Jun 2019 16:03:49 +0500 Subject: [PATCH 1460/1846] Fix closest order increase in neutral mode --- dexbot/strategies/staggered_orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index d812a4a47..c07277cd6 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1178,8 +1178,8 @@ def _calc_increase(self, asset, asset_balance, orders): # Order is less than further order and diff is more than `increment / 2` # Order is also less than previously calculated closest_order_bound if is_closest_order: - # At first, maximize order as we can - new_order_amount = max(closer_order_bound, further_order_bound) + # At first, maximize order up to further_order_bound + new_order_amount = min(closer_order_bound, further_order_bound) else: # Current order is less than virtually calculated next order (closest_order_bound) # Do not allow to increase more than further order amount From cbbb7c3ca10fe2b0c64aeb3f394fc13f624bf072 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 7 Jun 2019 16:05:18 +0500 Subject: [PATCH 1461/1846] Reduce min_increase_factor Because we're now calculating increased orders in memory, it's a good idea to reduce min_increase_factor to allow more smooth distribution of profits. --- 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 c07277cd6..61b1d3d1a 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -84,7 +84,7 @@ def __init__(self, *args, **kwargs): self.base_balance = None self.quote_asset_threshold = 0 self.base_asset_threshold = 0 - self.min_increase_factor = 1.15 + self.min_increase_factor = 1.05 self.mountain_max_increase_mode = False # Initial balance history elements should not be equal to avoid immediate bootstrap turn off self.quote_balance_history = [1, 2, 3] From 399052175a8ff5d27dc660bb23ecfb57374255e9 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 7 Jun 2019 16:07:09 +0500 Subject: [PATCH 1462/1846] Fix SO tests Some tests were broken after increase logic related changes, this fixes them. --- .../test_staggered_orders_complex.py | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index f4d448f03..973a7c111 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -207,11 +207,11 @@ def test_increase_order_sizes_valley_direction(worker, do_initial_allocation, is """ do_initial_allocation(worker, 'valley') - # Add balance to increase several orders + # Add balance to increase several orders; 1.01 to mitigate rounding issues increase_factor = max(1 + worker.increment, worker.min_increase_factor) - to_issue = worker.buy_orders[0]['base']['amount'] * (increase_factor - 1) * 3 + to_issue = worker.buy_orders[0]['base']['amount'] * (increase_factor - 1) * 3 * 1.01 issue_asset(worker.market['base']['symbol'], to_issue, worker.account.name) - to_issue = worker.sell_orders[0]['base']['amount'] * (increase_factor - 1) * 3 + to_issue = worker.sell_orders[0]['base']['amount'] * (increase_factor - 1) * 3 * 1.01 issue_asset(worker.market['quote']['symbol'], to_issue, worker.account.name) increase_until_allocated(worker) @@ -293,9 +293,11 @@ def test_increase_order_sizes_valley_smaller_closest_orders(worker, do_initial_a num_orders_after = len(worker.own_orders) assert num_orders_before == num_orders_after - # New closest orders amount should be equal to initial ones - assert worker.buy_orders[0]['base']['amount'] == initial_base - assert worker.sell_orders[0]['base']['amount'] == initial_quote + # New orders amounts should be equal to initial ones + # TODO: this relaxed test checks next closest orders because due to fp calculations closest orders may remain not + # increased + assert worker.buy_orders[1]['base']['amount'] == initial_base + assert worker.sell_orders[1]['base']['amount'] == initial_quote def test_increase_order_sizes_valley_imbalaced_small_further(worker, do_initial_allocation, increase_until_allocated): @@ -333,7 +335,7 @@ def test_increase_order_sizes_valley_imbalaced_small_further(worker, do_initial_ worker.place_market_buy_order(to_buy, further_order['price']) worker.refresh_orders() - # Drop excess balance to only allow to one increase round + # Drop excess balance to only allow one increase round worker.refresh_balances() increase_factor = max(1 + worker.increment, worker.min_increase_factor) to_keep = base_limit * (increase_factor - 1) * num_orders_to_cancel * 2 * 1.01 @@ -364,9 +366,9 @@ def test_increase_order_sizes_valley_closest_order(worker, do_initial_allocation worker.increase_order_sizes('base', worker.base_balance, previous_buy_orders) worker.refresh_orders() - assert worker.buy_orders[0]['base']['amount'] - previous_buy_orders[0]['base']['amount'] >= previous_buy_orders[0][ - 'base' - ]['amount'] * (increase_factor - 1) + assert worker.buy_orders[0]['base']['amount'] - previous_buy_orders[0]['base']['amount'] == pytest.approx( + previous_buy_orders[0]['base']['amount'] * (increase_factor - 1) + ) def test_increase_order_sizes_mountain_basic(worker, do_initial_allocation, issue_asset, increase_until_allocated): @@ -408,9 +410,9 @@ def test_increase_order_sizes_mountain_direction(worker, do_initial_allocation, worker.mode = 'mountain' increase_factor = max(1 + worker.increment, worker.min_increase_factor) - for _ in range(0, 6): + for i in range(-1, -6, -1): # Add balance to increase ~1 order - to_issue = worker.buy_orders[0]['base']['amount'] * (increase_factor - 1) + to_issue = worker.buy_orders[i]['base']['amount'] * (increase_factor - 1) issue_asset(worker.market['base']['symbol'], to_issue, worker.account.name) previous_buy_orders = worker.buy_orders worker.refresh_balances() @@ -429,15 +431,18 @@ def test_increase_order_sizes_mountain_direction(worker, do_initial_allocation, break -def test_increase_order_sizes_mountain_furthest_order(worker, do_initial_allocation, issue_asset): +def test_increase_order_sizes_mountain_furthest_order( + worker, do_initial_allocation, increase_until_allocated, issue_asset +): """ Should test proper calculation of furthest order: try to maximize, don't allow too small increase """ do_initial_allocation(worker, 'mountain') worker.mode = 'mountain' + increase_until_allocated(worker) # Add balance to increase ~2 orders increase_factor = max(1 + worker.increment, worker.min_increase_factor) - to_issue = worker.buy_orders[-1]['base']['amount'] * (increase_factor - 1) * 2.2 + to_issue = worker.buy_orders[-1]['base']['amount'] * (increase_factor - 1) * 2.01 issue_asset(worker.market['base']['symbol'], to_issue, worker.account.name) previous_buy_orders = worker.buy_orders @@ -484,7 +489,7 @@ def test_increase_order_sizes_mountain_imbalanced(worker, do_initial_allocation) for _ in range(0, num_orders_to_cancel): worker.refresh_balances() - worker.increase_order_sizes('base', worker.base_balance, previous_buy_orders) + worker.increase_order_sizes('base', worker.base_balance, worker.buy_orders) worker.refresh_orders() for order_index in range(0, num_orders_to_cancel): @@ -665,16 +670,16 @@ def test_increase_order_sizes_neutral_imbalaced_small_further(worker, do_initial worker.place_market_buy_order(base_limit / further_order['price'], further_order['price']) worker.refresh_orders() - # Place remainig limited orders + # Place remaining limited orders for i in range(1, num_orders_to_cancel): worker.place_closer_order('base', worker.buy_orders[0]) worker.place_further_order('base', worker.buy_orders[-1]) worker.refresh_orders() - # Drop excess balance to only allow to one increase round + # Drop excess balance to only allow one increase round worker.refresh_balances() increase_factor = max(1 + worker.increment, worker.min_increase_factor) - to_keep = base_limit * (increase_factor - 1) * num_orders_to_cancel * 2 * 1.1 + to_keep = base_limit * (increase_factor - 1) * num_orders_to_cancel * 2 to_drop = worker.base_balance['amount'] - to_keep amount = Amount(to_drop, worker.market['base']['symbol'], bitshares_instance=worker.bitshares) worker.bitshares.reserve(amount, account=worker.account) @@ -683,16 +688,19 @@ def test_increase_order_sizes_neutral_imbalaced_small_further(worker, do_initial for i in range(1, num_orders_to_cancel): # This is a simple check without precise calculation - # We're roughly checking that new furthest orders are not exceed new closest orders + # We're roughly checking that new furthest orders are not exceeds new closest orders further_order_amount = worker.buy_orders[-i]['base']['amount'] closer_order_amount = worker.buy_orders[i - 1]['base']['amount'] assert further_order_amount < closer_order_amount -def test_increase_order_sizes_neutral_closest_order(worker, do_initial_allocation, issue_asset): +def test_increase_order_sizes_neutral_closest_order( + worker, do_initial_allocation, increase_until_allocated, issue_asset +): """ Should test proper calculation of closest order: order should not be less that min_increase_factor """ worker = do_initial_allocation(worker, 'neutral') + increase_until_allocated(worker) # Add balance to increase 2 orders increase_factor = max(1 + worker.increment, worker.min_increase_factor) From 7e3f79c4ebde7c20ea02e6f1ba903fbb7ae60e17 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 7 Jun 2019 16:48:09 +0500 Subject: [PATCH 1463/1846] A bit cleaner log message --- 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 61b1d3d1a..317f063a0 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1281,7 +1281,7 @@ def increase_order_sizes(self, asset, asset_balance, orders): old_opposite_amount = orders[index]['quote']['amount'] new_opposite_amount = order['quote']['amount'] self.log.info( - 'Increasing {} order at price {:.8f}, {:.{prec}f} -> {:.{prec}f} {}, ' + 'Increasing {} order at price {:.8f}: {:.{prec}f} -> {:.{prec}f} {} ' '({:.{opposite_prec}f} -> {:.{opposite_prec}f} {})'.format( order_type, price, From ab6a7ff96cc1a9469d4324f436954ee798b676ac Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 7 Jun 2019 17:02:13 +0500 Subject: [PATCH 1464/1846] Enable previosly xfailed test test_increase_order_sizes_neutral_smaller_closest_orders is working if some approximation allowed. --- .../staggered_orders/test_staggered_orders_complex.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index 973a7c111..5a5be5844 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -598,7 +598,6 @@ def test_increase_order_sizes_neutral_transit_from_mountain(worker, do_initial_a break -@pytest.mark.xfail(reason='Closest order failed to increase up to initial balance, fp/rounding issue') def test_increase_order_sizes_neutral_smaller_closest_orders(worker, do_initial_allocation, increase_until_allocated): """ Test increase when closest-to-center orders are less than further orders. Normal situation when initial sides are imbalanced and several orders were filled. @@ -634,8 +633,12 @@ def test_increase_order_sizes_neutral_smaller_closest_orders(worker, do_initial_ increase_until_allocated(worker) # New closest orders amount should be equal to initial ones - assert worker.buy_orders[0]['base']['amount'] == initial_base - assert worker.sell_orders[0]['base']['amount'] == initial_quote + assert worker.buy_orders[0]['base']['amount'] == pytest.approx( + initial_base, rel=(1 ** -worker.market['base']['precision']) + ) + assert worker.sell_orders[0]['base']['amount'] == pytest.approx( + initial_quote, rel=(1 ** -worker.market['quote']['precision']) + ) def test_increase_order_sizes_neutral_imbalaced_small_further(worker, do_initial_allocation, increase_until_allocated): From e0a6143b2dda0f842a62d9656f63d99055da7399 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Fri, 7 Jun 2019 13:57:15 -0700 Subject: [PATCH 1465/1846] update websocket-client to 0.56 to be compatible with graphenelib 1.1.18 requirement --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b3c855714..b38cfb0ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ uptick==0.2.1 ruamel.yaml>=0.15.37 appdirs>=1.4.3 pycryptodomex==3.6.4 -websocket-client==0.54.0 +websocket-client==0.56.0 sdnotify==0.3.2 sqlalchemy==1.3.0 click==7.0 From 4e36bd4f60967a64782d59426762967183b52bef Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Mon, 10 Jun 2019 12:13:43 +0300 Subject: [PATCH 1466/1846] Change dexbot version number to 0.11.8 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index f663ea87b..4491b6655 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.11.7' +VERSION = '0.11.8' AUTHOR = 'Codaone Oy' __version__ = VERSION From 4e4f236521429f565577bd0f0a22701843dab50e Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Mon, 10 Jun 2019 13:15:28 +0300 Subject: [PATCH 1467/1846] Change dexbot version number to 0.11.9 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 4491b6655..634e7e0bb 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.11.8' +VERSION = '0.11.9' AUTHOR = 'Codaone Oy' __version__ = VERSION From c173dbabbea8d950f1d1214874cad8dbe799d969 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 11 Jun 2019 08:57:18 +0300 Subject: [PATCH 1468/1846] Change dexbot version number to 0.11.10 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 634e7e0bb..f83e4bcd3 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.11.9' +VERSION = '0.11.10' AUTHOR = 'Codaone Oy' __version__ = VERSION From 40b7d238a6a9730c7ec86e5597415a0e0baa7e4a Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 11 Jun 2019 12:15:15 +0300 Subject: [PATCH 1469/1846] Change dexbot version number to 0.11.11 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index f83e4bcd3..c55db4ddd 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.11.10' +VERSION = '0.11.11' AUTHOR = 'Codaone Oy' __version__ = VERSION From 6aa13545a5fb863618474efbf007b3c9b3a00c4e Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 12 Jun 2019 09:11:17 +0300 Subject: [PATCH 1470/1846] Fix ConfigElement for Last trade price as center price field --- dexbot/strategies/config_parts/relative_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/config_parts/relative_config.py b/dexbot/strategies/config_parts/relative_config.py index f717fa9a1..512ade064 100644 --- a/dexbot/strategies/config_parts/relative_config.py +++ b/dexbot/strategies/config_parts/relative_config.py @@ -54,7 +54,7 @@ def configure(cls, return_base_config=True): ConfigElement('center_price_depth', 'float', 0, 'Measurement depth', 'Cumulative quote amount from which depth center price will be measured', (0.00000001, 1000000000, 8, '')), - ConfigElement('center_price_from_last_trade', bool, False, 'Last trade price as new center price', + ConfigElement('center_price_from_last_trade', 'bool', False, 'Last trade price as new center price', 'This will make orders move by half the spread at every fill', None), ConfigElement('center_price_offset', 'bool', False, 'Center price offset based on asset balances', 'Automatically adjust orders up or down based on the imbalance of your assets', None), From 2d6d4d148b6100d7a0e0798273e4f72677211063 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 12 Jun 2019 09:12:15 +0300 Subject: [PATCH 1471/1846] Change center price from last trade config name --- 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 e0c37e877..a117de231 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -58,7 +58,7 @@ def __init__(self, *args, **kwargs): if self.is_center_price_dynamic: self.center_price = None self.center_price_depth = self.worker.get('center_price_depth', 0) - self.cp_from_last_trade = self.worker.get('cp_from_last_trade', False) + self.cp_from_last_trade = self.worker.get('center_price_from_last_trade', False) else: # Use manually set center price self.center_price = self.worker["center_price"] From e9089c29e5c6aa7b9dfdea2be16b7a4b5bb26a00 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 12 Jun 2019 09:12:45 +0300 Subject: [PATCH 1472/1846] Fix Relative Orders custom form --- .../views/ui/forms/relative_orders_widget.ui | 4900 ++++++++--------- 1 file changed, 2214 insertions(+), 2686 deletions(-) diff --git a/dexbot/views/ui/forms/relative_orders_widget.ui b/dexbot/views/ui/forms/relative_orders_widget.ui index e7a7885af..0f0d68059 100644 --- a/dexbot/views/ui/forms/relative_orders_widget.ui +++ b/dexbot/views/ui/forms/relative_orders_widget.ui @@ -6,24 +6,15 @@ 0 0 - 583 - 830 + 449 + 714 Form - - 0 - - - 0 - - - 0 - - + 0 @@ -31,1634 +22,1616 @@ Worker Parameters - - - - 8 - 50 - 120 - 29 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Order size - - - amount_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Fixed order size, expressed in quote asset, unless "relative order size" selected - - - ? + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 - 5 - - - - - - - - - 134 - 50 - 322 - 28 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 9 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 8 - - - 1000000000.000000000000000 - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - - - - - - - - - 8 - 85 - 120 - 29 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Amount is expressed as a percentage of the account balance of quote/base asset - - - ? + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Order size + + + amount_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Fixed order size, expressed in quote asset, unless "relative order size" selected + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 9 - 5 - - - - - - - - - 134 - 85 - 147 - 25 - - - - Relative order size - - - - - - 8 - 120 - 120 - 29 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Spread - - - spread_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - The percentage difference between buy and sell - - - ? + 0 + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 8 + + + 1000000000.000000000000000 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + + + + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 - 5 - - - - - - - - - 134 - 120 - 170 - 28 - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - % - - - 100000.000000000000000 - - - 5.000000000000000 - - - - - - 8 - 155 - 120 - 29 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 101 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Enable dynamic spread which overrides the spread field - - - ? + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Amount is expressed as a percentage of the account balance of quote/base asset + + + ? + + + 5 + + + + + + + + + + Relative order size + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 - 5 - - - - - - - - - 134 - 155 - 132 - 25 - - - - Dynamic spread - - - - - - 8 - 190 - 120 - 29 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Market depth - - - true - - - center_price_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - From which depth will market spread be measured? (QUOTE amount) - - - ? + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Spread + + + spread_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + The percentage difference between buy and sell + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + % + + + 100000.000000000000000 + + + 5.000000000000000 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 - 5 - - - - - - - - - 134 - 190 - 322 - 28 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 9 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 8 - - - 1000000000.000000000000000 - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - - - - - - - - - 8 - 225 - 120 - 29 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Dynamic spread factor - - - true - - - center_price_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - How many percent will own spread be compared to market spread? - - - ? + 0 + + + + + Qt::Horizontal + + + + 101 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Enable dynamic spread which overrides the spread field + + + ? + + + 5 + + + + + + + + + + Dynamic spread + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 - 5 - - - - - - - - - 134 - 225 - 170 - 28 - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - % - - - 0.010000000000000 - - - 1000.000000000000000 - - - 1.000000000000000 - - - - - - 8 - 260 - 120 - 29 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Center price - - - center_price_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Fixed center price expressed in base asset: base/quote - - - ? + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Market depth + + + true + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + From which depth will market spread be measured? (QUOTE amount) + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 9 - 5 - - - - - - - - - 134 - 260 - 316 - 28 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 9 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - false - - - - 0 - 0 - - - - - 170 - 0 - - - - ArrowCursor - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - false - - - false - - - 8 - - - 0.000000000000000 - - - 999999999.998999953269958 - - - - - - - true - - - - 0 - 0 - - - - - 120 - 0 - - - - - - - - - - - - - 8 - 295 - 120 - 29 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 101 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Estimate the center from closest opposite orders or from a depth - - - ? + 0 + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 8 + + + 1000000000.000000000000000 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + + + + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 - 5 - - - - - - - - - 134 - 295 - 298 - 25 - - - - - 0 - 0 - - - - Measure center price from market orders - - - true - - - false - - - - - - 8 - 330 - 120 - 29 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Measurement depth - - - true - - - center_price_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Cumulative quote amount from which depth center price will be measured - - - ? + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Dynamic spread factor + + + true + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + How many percent will own spread be compared to market spread? + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + % + + + 0.010000000000000 + + + 1000.000000000000000 + + + 1.000000000000000 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 - 5 - - - - - - - - - 134 - 330 - 322 - 28 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 9 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 8 - - - 1000000000.000000000000000 - - - - - - - - 0 - 0 - - - - - 120 - 0 - - - - - - - - - - - - - 10 - 470 - 120 - 29 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Automatically adjust orders up or down based on the imbalance of your assets - - - ? + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Center price + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Fixed center price expressed in base asset: base/quote + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 9 - 5 - - - - - - - - - 136 - 470 - 312 - 25 - - - - Center price offset based on asset balances - - - - - - 10 - 505 - 120 - 29 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - + 0 + + + + + false + + + + 0 + 0 + + + + + 170 + 0 + + + + ArrowCursor + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + false + + + 8 + + + 0.000000000000000 + + + 999999999.998999953269958 + + + false + + + + + + + true + + + + 0 + 0 + + + + + 120 + 0 + + + + + + + + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 - - - - - Manual center price offset - - - true - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Manually adjust orders up or down. Works independently of other offsets and doesn't override them - - - ? + + 0 + + + + + Qt::Horizontal + + + + 101 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Estimate the center from closest opposite orders or from a depth + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + Measure center price from market orders + + + true + + + false + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 - 5 + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Measurement depth + + + true + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Cumulative quote amount from which depth center price will be measured + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 9 - - - - - - - - 136 - 505 - 290 - 20 - - - - - 0 - 0 - - - - - 290 - 0 - - - - - 16777215 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - true + + 0 + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 8 + + + 1000000000.000000000000000 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + + + + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 - - - 0 - 0 - + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Use last filled order price as new center price + + + ? + + + 5 + + + + + + + + + + Last filled order price as new center price + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 - - - 250 - 0 - + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Use external reference price instead of center price acquired from the market + + + ? + + + 5 + + + + + + + + + + Use external source for center price calculation + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 - - - 145 - 16777215 - + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + External feed + + + amount_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + The bot will try to get price information from this source + + + ? + + + 5 + + + + + + + + + + true + + + + 170 + 16777215 + + + + -1 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 - - QSlider::groove:horizontal { + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Automatically adjust orders up or down based on the imbalance of your assets + + + ? + + + 5 + + + + + + + + + + Center price offset based on asset balances + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + + + + Manual center price offset + + + true + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Manually adjust orders up or down. Works independently of other offsets and doesn't override them + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 290 + 0 + + + + + 16777215 + 16777215 + + + + + 0 + + + + + true + + + + 0 + 0 + + + + + 250 + 0 + + + + + 145 + 16777215 + + + + QSlider::groove:horizontal { border: 1px solid #999999; height: 8px; /* The groove expands to the size of the slider by default. by giving it a height, it has a fixed size */ background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #ffffff, stop:1 #ffffff); @@ -1672,1092 +1645,647 @@ QSlider::handle:horizontal { margin: -2px 0; /* Handle is placed by default on the contents rect of the groove. Expand outside the groove */ border-radius: 3px; } - - - -100 - - - 100 - - - 1 - - - Qt::Horizontal - - - false - - - false - - - QSlider::TicksBelow - - - 20 - - - - - - - - 0 - 0 - - - - - 40 - 16777215 - - - - 0 % - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - 10 - 540 - 120 - 29 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Reset orders when buy or sell order is partially filled - - - ? + + + -100 + + + 100 + + + 1 + + + Qt::Horizontal + + + false + + + false + + + QSlider::TicksBelow + + + 20 + + + + + + + + 0 + 0 + + + + + 40 + 16777215 + + + + 0 % + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 - 5 - - - - - - - - - 136 - 540 - 197 - 25 - - - - Reset orders on partial fill - - - - - - 10 - 575 - 120 - 29 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Fill threshold - - - center_price_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Order fill threshold to reset orders - - - ? + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Reset orders when buy or sell order is partially filled + + + ? + + + 5 + + + + + + + + + + Reset orders on partial fill + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 - 5 - - - - - - - - - 136 - 575 - 170 - 28 - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - % - - - 100.000000000000000 - - - 0.000000000000000 - - - - - - 10 - 610 - 120 - 29 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Reset orders when center price is changed more than threshold - - - ? - - - 5 - - - - - - - - - 136 - 610 - 263 - 25 - - - - Reset orders on center price change - - - - - - 10 - 645 - 120 - 29 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Price change - - - center_price_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Define center price threshold to react on - - - ? - - - 5 - - - - - - - - - 136 - 645 - 170 - 28 - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - % - - - 100000.000000000000000 - - - 0.000000000000000 - - - - - - 10 - 680 - 120 - 29 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Override order expiration time to trigger a reset - - - ? - - - 5 - - - - - - - - - 136 - 680 - 148 - 25 - - - - Custom expiration - - - - - - 10 - 715 - 120 - 29 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Order expiration - - - center_price_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Define custom order expiration time to force orders reset more often, seconds - - - ? + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Fill threshold + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Order fill threshold to reset orders + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + % + + + 100.000000000000000 + + + 0.000000000000000 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 - 5 - - - - - - - - - 136 - 717 - 170 - 28 - - - - - 0 - 0 - - - - - 170 - 0 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - 0 - - - 100000000000.000000000000000 - - - 5.000000000000000 - - - - - - 10 - 429 - 120 - 29 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - External feed - - - amount_input - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - The bot will try to get price information from this source - - - ? + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Reset orders when center price is changed more than threshold + + + ? + + + 5 + + + + + + + + + + Reset orders on center price change + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 - 5 - - - - - - - - true - - - - 136 - 429 - 79 - 27 - - - - - 170 - 16777215 - - - - -1 - - - - - - 10 - 394 - 120 - 29 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Use external reference price instead of center price acquired from the market - - - ? + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Price change + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Define center price threshold to react on + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + % + + + 100000.000000000000000 + + + 0.000000000000000 + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 - 5 - - - - - - - - - 136 - 394 - 335 - 25 - - - - Use external source for center price calculation - - - - - - 8 - 364 - 120 - 29 - - - - - 0 - 0 - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 101 - 20 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - WhatsThisCursor - - - Use last filled order price as new center price - - - ? + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Override order expiration time to trigger a reset + + + ? + + + 5 + + + + + + + + + + Custom expiration + + + + + + + + 0 + 0 + + + + + 120 + 0 + + + + + 120 + 16777215 + + + + + 0 - 5 - - - - - - - - - 136 - 364 - 335 - 25 - - - - Last filled order price as new center price - - + 0 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Order expiration + + + center_price_input + + + + + + + + 0 + 0 + + + + + 75 + true + + + + WhatsThisCursor + + + Define custom order expiration time to force orders reset more often, seconds + + + ? + + + 5 + + + + + + + + + + + 0 + 0 + + + + + 170 + 0 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 0 + + + 100000000000.000000000000000 + + + 5.000000000000000 + + + + From 90b5557595cafb2c2b68bc6f1ab67da0204911c2 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 12 Jun 2019 09:29:06 +0300 Subject: [PATCH 1473/1846] Fix pep8 error Missing new line from end of relative_order.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 a117de231..2a938e956 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -612,4 +612,4 @@ def get_own_last_trade(self): quote = trade['fill_price']['base']['amount'] / 10 ** self.market['quote']['precision'] return {'base': base, 'quote': quote, 'price': base / quote} except Exception: - return False \ No newline at end of file + return False From 561a6ea516a284ef76b36ec2f4930d568c920012 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 12 Jun 2019 13:27:01 +0300 Subject: [PATCH 1474/1846] Change imports in worker_list.py Changed imports to absolute imports as well as cleaned up the imports list. --- dexbot/views/worker_list.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 927750287..bf74b3af4 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -3,20 +3,22 @@ import webbrowser from dexbot import __version__ -from dexbot.qt_queue.queue_dispatcher import ThreadDispatcher + +from dexbot.views.create_worker import CreateWorkerView +from dexbot.views.errors import gui_error +from dexbot.views.layouts.flow_layout import FlowLayout +from dexbot.views.settings import SettingsView +from dexbot.views.ui.worker_list_window_ui import Ui_MainWindow +from dexbot.views.worker_item import WorkerItemWidget from dexbot.qt_queue.idle_queue import idle_add -from .ui.worker_list_window_ui import Ui_MainWindow -from .create_worker import CreateWorkerView -from .settings import SettingsView -from .worker_item import WorkerItemWidget -from .errors import gui_error -from .layouts.flow_layout import FlowLayout - -from PyQt5 import QtGui, QtWidgets +from dexbot.qt_queue.queue_dispatcher import ThreadDispatcher + +from PyQt5.QtGui import QFontDatabase +from PyQt5.QtWidgets import QMainWindow from bitsharesapi.bitsharesnoderpc import BitSharesNodeRPC -class MainView(QtWidgets.QMainWindow, Ui_MainWindow): +class MainView(QMainWindow, Ui_MainWindow): def __init__(self, main_ctrl): super().__init__() @@ -57,7 +59,7 @@ def __init__(self, main_ctrl): ) self.statusbar_updater.start() - QtGui.QFontDatabase.addApplicationFont(":/bot_widget/font/SourceSansPro-Bold.ttf") + QFontDatabase.addApplicationFont(":/bot_widget/font/SourceSansPro-Bold.ttf") def add_worker_widget(self, worker_name): config = self.config.get_worker_config(worker_name) From 8abd4724a9c197d9b1a317da0eeb080eab1d364d Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Sun, 16 Jun 2019 22:03:48 -0700 Subject: [PATCH 1475/1846] fix init variables for #596 --- dexbot/orderengines/bitshares_engine.py | 17 ++++++++++------- dexbot/pricefeeds/bitshares_feed.py | 4 ++-- dexbot/strategies/base.py | 2 ++ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index 45ddf5d58..a4e127dc2 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -44,6 +44,7 @@ def __init__(self, config=None, account=None, market=None, + worker_market=None, fee_asset_symbol=None, bitshares_instance=None, bitshares_bundle=None, @@ -71,9 +72,11 @@ def __init__(self, self.config = Config.get_worker_config_file(name) # Get Bitshares account and market for this worker - self._account = account + self.account = account - self._market = market + self.market = market + + self.worker_market = worker_market # Recheck flag - Tell the strategy to check for updated orders self.recheck_orders = False @@ -183,7 +186,7 @@ def balance(self, asset, fee_reservation=0): :param float | fee_reservation: How much is saved in reserve for the fees :return: Balance of specific asset """ - balance = self._account.balance(asset) + balance = self.account.balance(asset) if fee_reservation > 0: balance['amount'] = balance['amount'] - fee_reservation @@ -686,7 +689,7 @@ def account(self): :return: object | Account """ - return self._account + return self.account @property def balances(self): @@ -694,7 +697,7 @@ def balances(self): :return: Balances in list where each asset is in their own Amount object """ - return self._account.balances + return self.account.balances def get_own_orders(self, refresh=True): """ Return the account's open orders in the current market @@ -709,7 +712,7 @@ def get_own_orders(self, refresh=True): self.account.refresh() for order in self.account.openorders: - if self.worker["market"] == order.market and self.account.openorders: + if self.worker_market == order.market and self.account.openorders: orders.append(order) return orders @@ -747,7 +750,7 @@ def market(self): # TODO: property, also in price feed, need to consider inheritance priority """ Return the market object as :class:`bitshares.market.Market` """ - return self._market + return self.market @staticmethod def get_updated_limit_order(limit_order): diff --git a/dexbot/pricefeeds/bitshares_feed.py b/dexbot/pricefeeds/bitshares_feed.py index 020291af6..3dee43aca 100644 --- a/dexbot/pricefeeds/bitshares_feed.py +++ b/dexbot/pricefeeds/bitshares_feed.py @@ -19,7 +19,7 @@ def __init__(self, market, bitshares_instance=None): - self._market = market + self.market = market self.ticker = self.market.ticker self.disabled = False # flag for suppress errors @@ -339,7 +339,7 @@ def get_market_spread(self, quote_amount=0, base_amount=0): def market(self): """ Return the market object as :class:`bitshares.market.Market` """ - return self._market + return self.market @staticmethod def sort_orders_by_price(orders, sort='DESC'): diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 60ae7a2b9..5c4733c37 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -189,6 +189,8 @@ def __init__(self, } ) + self.worker_market = self.worker["market"] + self.orders_log = logging.LoggerAdapter(logging.getLogger('dexbot.orders_log'), {}) def pause(self): From 74e6f26faac39a959c13d1dcd63e4dac08dc96ca Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Mon, 17 Jun 2019 13:17:32 +0300 Subject: [PATCH 1476/1846] Remove properties from bitshares_engine.py and bitshares_feed.py Since changing the _market -> market and _account to account was made these properties conflicted with the variable names thus removed. --- dexbot/orderengines/bitshares_engine.py | 16 ---------------- dexbot/pricefeeds/bitshares_feed.py | 6 ------ 2 files changed, 22 deletions(-) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index a4e127dc2..7ba1366ce 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -682,15 +682,6 @@ def retry_action(self, action, *args, **kwargs): else: raise - @property - def account(self): - """ Return the full account as :class:`bitshares.account.Account` object! - Can be refreshed by using ``x.refresh()`` - - :return: object | Account - """ - return self.account - @property def balances(self): """ Returns all the balances of the account assigned for the worker. @@ -745,13 +736,6 @@ def own_orders(self): """ return self.get_own_orders() - @property - def market(self): - # TODO: property, also in price feed, need to consider inheritance priority - """ Return the market object as :class:`bitshares.market.Market` - """ - return self.market - @staticmethod def get_updated_limit_order(limit_order): """ Returns a modified limit_order so that when passed to Order class, diff --git a/dexbot/pricefeeds/bitshares_feed.py b/dexbot/pricefeeds/bitshares_feed.py index 3dee43aca..2e273cdaf 100644 --- a/dexbot/pricefeeds/bitshares_feed.py +++ b/dexbot/pricefeeds/bitshares_feed.py @@ -335,12 +335,6 @@ def get_market_spread(self, quote_amount=0, base_amount=0): return ask / bid - 1 - @property - def market(self): - """ Return the market object as :class:`bitshares.market.Market` - """ - return self.market - @staticmethod def sort_orders_by_price(orders, sort='DESC'): """ Return list of orders sorted ascending or descending by price From 7a9ef624e72949ffce2060abc41ebc5e58b566b5 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Mon, 17 Jun 2019 13:22:40 +0300 Subject: [PATCH 1477/1846] Change dexbot version number to 0.11.12 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index c55db4ddd..44125bfbe 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.11.11' +VERSION = '0.11.12' AUTHOR = 'Codaone Oy' __version__ = VERSION From 0ad5f84933b9b41ac2e7df27fc67dea1eed6ee83 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 18 Jun 2019 14:45:36 +0300 Subject: [PATCH 1478/1846] Change worker_list_window.ui Add `Unlock wallet` button --- dexbot/views/ui/worker_list_window.ui | 91 ++++++++++++++------------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/dexbot/views/ui/worker_list_window.ui b/dexbot/views/ui/worker_list_window.ui index b93d0b20e..6c1b91160 100644 --- a/dexbot/views/ui/worker_list_window.ui +++ b/dexbot/views/ui/worker_list_window.ui @@ -64,9 +64,6 @@ QScrollBar { Qt::ScrollBarAsNeeded - - QAbstractScrollArea::AdjustToContents - true @@ -88,12 +85,12 @@ QScrollBar { 0 - - -7 - color: white; + + -7 + @@ -179,16 +176,7 @@ QScrollBar { 0 - - 0 - - - 0 - - - 0 - - + 0 @@ -200,30 +188,52 @@ QScrollBar { - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - + + + + + 0 + 0 + + + + + 100 + 0 + + + + + 250 + 25 + + + + PointingHandCursor + + + border: 0px; background-color: #5581e8; width: 250px; height: 20px; border-radius: 10px; color: #ffffff; + + + Unlock wallet + + + -1 + + + Qt::Vertical + + QSizePolicy::Expanding + 20 - 60 + 20 @@ -251,15 +261,15 @@ QScrollBar { PointingHandCursor - - -1 - border: 0px; background-color: #3A6257; width: 250px; height: 20px; border-radius: 10px; color: #ffffff; Add worker + + -1 + @@ -295,16 +305,7 @@ QScrollBar { 0 - - 0 - - - 0 - - - 0 - - + 0 From 6db2fbe21731d45932492a2d03a62cd09171155a Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 18 Jun 2019 14:47:19 +0300 Subject: [PATCH 1479/1846] Change main_controller's bitshares_intance to null on initialization Main controller can now be created without Bitshares instance, so that the main view can be handled without connection to Bitshares. --- dexbot/controllers/main_controller.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index ebe422e5a..ad76a3b53 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -13,9 +13,8 @@ class MainController: - def __init__(self, bitshares_instance, config): - self.bitshares_instance = bitshares_instance - set_shared_bitshares_instance(bitshares_instance) + def __init__(self, config): + self.bitshares_instance = None self.config = config self.worker_manager = None From d6866a92d2938cddaa312e8714fbb30fa0d1d878 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 18 Jun 2019 14:49:14 +0300 Subject: [PATCH 1480/1846] Add functions to main_controller for Bitshares instance creation new_bitshares_instance() = Creates new instance set_bitshares_instance() = Sets the given instance for the controller --- dexbot/controllers/main_controller.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index ad76a3b53..5d0ff0673 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -8,6 +8,7 @@ from dexbot.views.errors import PyQtHandler from appdirs import user_data_dir +from bitshares.bitshares import BitShares from bitshares.instance import set_shared_bitshares_instance @@ -40,6 +41,24 @@ def __init__(self, config): # Initialize folders initialize_data_folders() + def set_bitshares_instance(self, bitshares_instance): + """ Set bitshares instance + + :param bitshares_instance: A bitshares instance + """ + self.bitshares_instance = bitshares_instance + set_shared_bitshares_instance(bitshares_instance) + + def new_bitshares_instance(self, node, retries=-1, expiration=60): + """ Create bitshares instance + + :param retries: Number of retries to connect, -1 default to infinity + :param expiration: Delay in seconds until transactions are supposed to expire + :param list node: Node or a list of nodes + """ + self.bitshares_instance = BitShares(node, num_retries=retries, expiration=expiration) + set_shared_bitshares_instance(self.bitshares_instance) + def set_info_handler(self, handler): self.pyqt_handler.set_info_handler(handler) From 466506b868306ce3fe0e6f890d4cb884a4cd056a Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 18 Jun 2019 14:57:42 +0300 Subject: [PATCH 1481/1846] Change imports to absolute path on create_wallet.py and unlock_wallet.py --- dexbot/views/create_wallet.py | 10 +++++----- dexbot/views/unlock_wallet.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/dexbot/views/create_wallet.py b/dexbot/views/create_wallet.py index 7ae192837..87a90d52d 100644 --- a/dexbot/views/create_wallet.py +++ b/dexbot/views/create_wallet.py @@ -1,11 +1,11 @@ -from .ui.create_wallet_window_ui import Ui_Dialog -from .notice import NoticeDialog -from .errors import gui_error +from dexbot.views.ui.create_wallet_window_ui import Ui_Dialog +from dexbot.views.notice import NoticeDialog +from dexbot.views.errors import gui_error -from PyQt5 import QtWidgets +from PyQt5.QtWidgets import QDialog -class CreateWalletView(QtWidgets.QDialog, Ui_Dialog): +class CreateWalletView(QDialog, Ui_Dialog): def __init__(self, controller): self.controller = controller diff --git a/dexbot/views/unlock_wallet.py b/dexbot/views/unlock_wallet.py index 4eb892691..f2ce65d05 100644 --- a/dexbot/views/unlock_wallet.py +++ b/dexbot/views/unlock_wallet.py @@ -1,11 +1,11 @@ -from .ui.unlock_wallet_window_ui import Ui_Dialog -from .notice import NoticeDialog -from .errors import gui_error +from dexbot.views.ui.unlock_wallet_window_ui import Ui_Dialog +from dexbot.views.notice import NoticeDialog +from dexbot.views.errors import gui_error -from PyQt5 import QtWidgets +from PyQt5.QtWidgets import QDialog -class UnlockWalletView(QtWidgets.QDialog, Ui_Dialog): +class UnlockWalletView(QDialog, Ui_Dialog): def __init__(self, controller): self.controller = controller From 5517f4670913414e875ed8bbdadc9fed47652eac Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 18 Jun 2019 15:06:04 +0300 Subject: [PATCH 1482/1846] Rename main_ctrl to main_controller --- dexbot/views/worker_list.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index bf74b3af4..28215bccf 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -20,19 +20,19 @@ class MainView(QMainWindow, Ui_MainWindow): - def __init__(self, main_ctrl): + def __init__(self, main_controller): super().__init__() self.setupUi(self) - self.main_ctrl = main_ctrl + self.main_controller = main_controller - self.config = main_ctrl.config + self.config = main_controller.config self.max_workers = 10 self.num_of_workers = 0 self.worker_widgets = {} self.closing = False self.statusbar_updater = None self.statusbar_updater_first_run = True - self.main_ctrl.set_info_handler(self.set_worker_status) + self.main_controller.set_info_handler(self.set_worker_status) self.layout = FlowLayout(self.scrollAreaContent) self.add_worker_button.clicked.connect(lambda: self.handle_add_worker()) @@ -63,7 +63,7 @@ def __init__(self, main_ctrl): def add_worker_widget(self, worker_name): config = self.config.get_worker_config(worker_name) - widget = WorkerItemWidget(worker_name, config, self.main_ctrl, self) + widget = WorkerItemWidget(worker_name, config, self.main_controller, self) widget.setFixedSize(widget.frameSize()) self.layout.addWidget(widget) self.worker_widgets[worker_name] = widget @@ -86,13 +86,13 @@ def change_worker_widget_name(self, old_worker_name, new_worker_name): @gui_error def handle_add_worker(self): - create_worker_dialog = CreateWorkerView(self.main_ctrl.bitshares_instance) + create_worker_dialog = CreateWorkerView(self.main_controller.bitshares_instance) return_value = create_worker_dialog.exec_() # User clicked save if return_value == 1: worker_name = create_worker_dialog.worker_name - self.main_ctrl.create_worker(worker_name) + self.main_controller.create_worker(worker_name) self.config.add_worker_config(worker_name, create_worker_dialog.worker_data) self.add_worker_widget(worker_name) From 00fd951ac5f3b0c9fdf5ee7dc9d6f6fc04be049a Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 19 Jun 2019 07:53:48 +0300 Subject: [PATCH 1483/1846] Rename variable statusbar_updater -> status_bar_updater --- dexbot/views/worker_list.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 28215bccf..42959d51f 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -30,7 +30,7 @@ def __init__(self, main_controller): self.num_of_workers = 0 self.worker_widgets = {} self.closing = False - self.statusbar_updater = None + self.status_bar_updater = None self.statusbar_updater_first_run = True self.main_controller.set_info_handler(self.set_worker_status) self.layout = FlowLayout(self.scrollAreaContent) @@ -128,8 +128,8 @@ def customEvent(self, event): def closeEvent(self, event): self.closing = True self.status_bar.showMessage("Closing app...") - if self.statusbar_updater and self.statusbar_updater.is_alive(): - self.statusbar_updater.join() + if self.status_bar_updater and self.status_bar_updater.is_alive(): + self.status_bar_updater.join() def _update_statusbar_message(self): while not self.closing: From 81bc56d121124101fca570ad9811e222e9dfc9e2 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 19 Jun 2019 07:55:08 +0300 Subject: [PATCH 1484/1846] Move wallet unlock inside the main view --- dexbot/gui.py | 30 ++++++------------ dexbot/views/worker_list.py | 61 ++++++++++++++++++++++++++----------- 2 files changed, 53 insertions(+), 38 deletions(-) diff --git a/dexbot/gui.py b/dexbot/gui.py index b4a7b18d0..900342b7a 100644 --- a/dexbot/gui.py +++ b/dexbot/gui.py @@ -3,35 +3,25 @@ from dexbot.config import Config from dexbot.controllers.main_controller import MainController 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 from PyQt5.QtWidgets import QApplication -from bitshares import BitShares class App(QApplication): def __init__(self, sys_argv): super(App, self).__init__(sys_argv) + # Init config config = Config() - bitshares_instance = BitShares(config['node'], num_retries=-1, expiration=60) - - # Wallet unlock - unlock_ctrl = WalletController(bitshares_instance) - if unlock_ctrl.wallet_created(): - unlock_view = UnlockWalletView(unlock_ctrl) - else: - unlock_view = CreateWalletView(unlock_ctrl) - - if unlock_view.exec_(): - bitshares_instance = unlock_ctrl.bitshares - self.main_ctrl = MainController(bitshares_instance, config) - self.main_view = MainView(self.main_ctrl) - self.main_view.show() - else: - sys.exit() + + # Init main controller + self.main_controller = MainController(config) + + # Init main view + self.main_view = MainView(self.main_controller) + + # Show main view + self.main_view.show() def main(): diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 42959d51f..e39903b62 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -3,12 +3,15 @@ import webbrowser from dexbot import __version__ - +from dexbot.config import Config +from dexbot.controllers.wallet_controller import WalletController +from dexbot.views.create_wallet import CreateWalletView from dexbot.views.create_worker import CreateWorkerView from dexbot.views.errors import gui_error from dexbot.views.layouts.flow_layout import FlowLayout from dexbot.views.settings import SettingsView from dexbot.views.ui.worker_list_window_ui import Ui_MainWindow +from dexbot.views.unlock_wallet import UnlockWalletView from dexbot.views.worker_item import WorkerItemWidget from dexbot.qt_queue.idle_queue import idle_add from dexbot.qt_queue.queue_dispatcher import ThreadDispatcher @@ -34,32 +37,54 @@ def __init__(self, main_controller): self.statusbar_updater_first_run = True self.main_controller.set_info_handler(self.set_worker_status) self.layout = FlowLayout(self.scrollAreaContent) + self.dispatcher = None self.add_worker_button.clicked.connect(lambda: self.handle_add_worker()) self.settings_button.clicked.connect(lambda: self.handle_open_settings()) self.help_button.clicked.connect(lambda: self.handle_open_documentation()) + self.unlock_wallet_button.clicked.connect(lambda: self.handle_login()) - # Load worker widgets from config file - workers = self.config.workers_data - for worker_name in workers: - self.add_worker_widget(worker_name) + # Hide certain buttons by default until login success + self.add_worker_button.hide() + + self.status_bar.showMessage(self.get_statusbar_message()) - # Limit the max amount of workers so that the performance isn't greatly affected - if self.num_of_workers >= self.max_workers: - self.add_worker_button.setEnabled(False) - break + QFontDatabase.addApplicationFont(":/bot_widget/font/SourceSansPro-Bold.ttf") - # Dispatcher polls for events from the workers that are used to change the ui - self.dispatcher = ThreadDispatcher(self) - self.dispatcher.start() + def handle_login(self): + """ This function handles login to the wallet. To avoid lag when creating - self.status_bar.showMessage("ver {} - Node delay: - ms".format(__version__)) - self.statusbar_updater = Thread( - target=self._update_statusbar_message - ) - self.statusbar_updater.start() + """ + self.main_controller.new_bitshares_instance(self.config['node']) + wallet_controller = WalletController(self.main_controller.bitshares_instance) - QFontDatabase.addApplicationFont(":/bot_widget/font/SourceSansPro-Bold.ttf") + if wallet_controller.wallet_created(): + unlock_view = UnlockWalletView(wallet_controller) + else: + unlock_view = CreateWalletView(wallet_controller) + + if unlock_view.exec_(): + # Hide button once successful wallet creation / login + self.unlock_wallet_button.hide() + self.add_worker_button.show() + + # Load worker widgets from config file + workers = self.config.workers_data + 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 + if self.num_of_workers >= self.max_workers: + self.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() + + self.status_bar.showMessage("ver {} - Node delay: - ms".format(__version__)) + self.status_bar_updater = Thread(target=self._update_statusbar_message) + self.status_bar_updater.start() def add_worker_widget(self, worker_name): config = self.config.get_worker_config(worker_name) From 4c6dd636a33aecdfe7d735b6b663367d6c4af13a Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 19 Jun 2019 07:55:38 +0300 Subject: [PATCH 1485/1846] Change worker_list.py Reloads the config when saving settings --- dexbot/views/worker_list.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index e39903b62..2908a1269 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -127,6 +127,10 @@ def handle_open_settings(self): settings_dialog = SettingsView() settings_dialog.exec_() + # Reinitialize config after closing the settings window + self.config = Config() + self.main_controller.config = self.config + @staticmethod def handle_open_documentation(): webbrowser.open('https://github.com/Codaone/DEXBot/wiki') From b32733b47ebf4336879e5671b0d639dc2abbdb64 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 19 Jun 2019 08:06:52 +0300 Subject: [PATCH 1486/1846] Change default status to show only version number Getting the status message using `get_status_message()` connects to default node which might have very high latency which lags the start up. --- 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 2908a1269..3e9c943bf 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -47,7 +47,7 @@ def __init__(self, main_controller): # Hide certain buttons by default until login success self.add_worker_button.hide() - self.status_bar.showMessage(self.get_statusbar_message()) + self.status_bar.showMessage("ver {} - Node disconnected".format(__version__)) QFontDatabase.addApplicationFont(":/bot_widget/font/SourceSansPro-Bold.ttf") From 4bb80dc548880ddb57f937f0f99e8d3901669aae Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Wed, 19 Jun 2019 17:57:48 -0700 Subject: [PATCH 1487/1846] move properties up and rename _market, _account --- dexbot/orderengines/bitshares_engine.py | 133 ++++++++++++++---------- dexbot/strategies/base.py | 48 ++------- 2 files changed, 84 insertions(+), 97 deletions(-) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index 7ba1366ce..ecd304e14 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -42,9 +42,8 @@ class BitsharesOrderEngine(Storage, Events): def __init__(self, name, config=None, - account=None, - market=None, - worker_market=None, + _account=None, + _market=None, fee_asset_symbol=None, bitshares_instance=None, bitshares_bundle=None, @@ -71,12 +70,8 @@ def __init__(self, else: self.config = Config.get_worker_config_file(name) - # Get Bitshares account and market for this worker - self.account = account - - self.market = market - - self.worker_market = worker_market + self._market = _market + self._account = _account # Recheck flag - Tell the strategy to check for updated orders self.recheck_orders = False @@ -84,23 +79,13 @@ def __init__(self, # Count of orders to be fetched from the API self.fetch_depth = 8 - # Set fee asset - fee_asset_symbol = fee_asset_symbol - - if fee_asset_symbol: - try: - self.fee_asset = Asset(fee_asset_symbol, bitshares_instance=self.bitshares) - except bitshares.exceptions.AssetDoesNotExistsException: - self.fee_asset = Asset('1.3.0', bitshares_instance=self.bitshares) - else: - # If there is no fee asset, use BTS - self.fee_asset = Asset('1.3.0', bitshares_instance=self.bitshares) + self.fee_asset = fee_asset_symbol # CER cache self.core_exchange_rate = None # Ticker - self.ticker = self.market.ticker + self.ticker = self._market.ticker # Settings for bitshares instance self.bitshares.bundle = bitshares_bundle @@ -130,7 +115,7 @@ def _cancel_orders(self, orders): try: self.retry_action( self.bitshares.cancel, - orders, account=self.account, fee_asset=self.fee_asset['id'] + orders, account=self._account, fee_asset=self.fee_asset['id'] ) except bitsharesapi.exceptions.UnhandledRPCError as exception: if str(exception).startswith('Assert Exception: maybe_found != nullptr: Unable to find Object'): @@ -186,7 +171,7 @@ def balance(self, asset, fee_reservation=0): :param float | fee_reservation: How much is saved in reserve for the fees :return: Balance of specific asset """ - balance = self.account.balance(asset) + balance = self._account.balance(asset) if fee_reservation > 0: balance['amount'] = balance['amount'] - fee_reservation @@ -194,10 +179,10 @@ def balance(self, asset, fee_reservation=0): return balance def calculate_order_data(self, order, amount, price): - quote_asset = Amount(amount, self.market['quote']['symbol'], bitshares_instance=self.bitshares) + quote_asset = Amount(amount, self._market['quote']['symbol'], bitshares_instance=self.bitshares) order['quote'] = quote_asset order['price'] = price - base_asset = Amount(amount * price, self.market['base']['symbol'], bitshares_instance=self.bitshares) + base_asset = Amount(amount * price, self._market['base']['symbol'], bitshares_instance=self.bitshares) order['base'] = base_asset return order @@ -282,8 +267,8 @@ def count_asset(self, order_ids=None, return_asset=False): """ quote = 0 base = 0 - quote_asset = self.market['quote']['id'] - base_asset = self.market['base']['id'] + quote_asset = self._market['quote']['id'] + base_asset = self._market['base']['id'] # Total balance calculation for balance in self.balances: @@ -320,8 +305,8 @@ def get_allocated_assets(self, order_ids=None, return_asset=False): quote = 0 base = 0 - quote_asset = self.market['quote']['id'] - base_asset = self.market['base']['id'] + 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) @@ -378,7 +363,7 @@ def get_market_orders(self, depth=1, updated=True): remainders and not just initial amounts :return: Returns a list of orders or None """ - orders = self.bitshares.rpc.get_limit_orders(self.market['base']['id'], self.market['quote']['id'], depth) + orders = self.bitshares.rpc.get_limit_orders(self._market['base']['id'], self._market['quote']['id'], depth) if updated: orders = [self.get_updated_limit_order(o) for o in orders] orders = [Order(o, bitshares_instance=self.bitshares) for o in orders] @@ -454,7 +439,7 @@ def get_updated_order(self, order_id): # At first, try to look up own orders. This prevents RPC calls whether requested order is own order order = None - for limit_order in self.account['limit_orders']: + for limit_order in self._account['limit_orders']: if order_id == limit_order['id']: order = limit_order break @@ -487,7 +472,7 @@ def is_buy_order(self, order): :return bool """ # Check if the order is buy order, by comparing asset symbol of the order and the market - if order['base']['symbol'] == self.market['base']['symbol']: + if order['base']['symbol'] == self._market['base']['symbol']: return True else: return False @@ -497,14 +482,14 @@ def is_current_market(self, base_asset_id, quote_asset_id): :return: bool: True = Current market, False = Not current market """ - if quote_asset_id == self.market['quote']['id']: - if base_asset_id == self.market['base']['id']: + if quote_asset_id == self._market['quote']['id']: + if base_asset_id == self._market['base']['id']: return True return False # Todo: Should we return true if market is opposite? - if quote_asset_id == self.market['base']['id']: - if base_asset_id == self.market['quote']['id']: + if quote_asset_id == self._market['base']['id']: + if base_asset_id == self._market['quote']['id']: return True return False @@ -517,7 +502,7 @@ def is_sell_order(self, order): :return bool """ # Check if the order is sell order, by comparing asset symbol of the order and the market - if order['base']['symbol'] == self.market['quote']['symbol']: + if order['base']['symbol'] == self._market['quote']['symbol']: return True else: return False @@ -532,8 +517,8 @@ def place_market_buy_order(self, amount, price, return_none=False, *args, **kwar :param kwargs: :return: """ - symbol = self.market['base']['symbol'] - precision = self.market['base']['precision'] + symbol = self._market['base']['symbol'] + precision = self._market['base']['precision'] base_amount = truncate(price * amount, precision) return_order_id = kwargs.pop('returnOrderId', self.returnOrderId) @@ -544,7 +529,7 @@ def place_market_buy_order(self, amount, price, return_none=False, *args, **kwar return None # Make sure we have enough balance for the order - if return_order_id and self.balance(self.market['base']) < base_amount: + if return_order_id and self.balance(self._market['base']) < base_amount: self.log.critical("Insufficient buy balance, needed {} {}".format(base_amount, symbol)) self.disabled = True return None @@ -554,10 +539,10 @@ def place_market_buy_order(self, amount, price, return_none=False, *args, **kwar # Place the order buy_transaction = self.retry_action( - self.market.buy, + self._market.buy, price, - Amount(amount=amount, asset=self.market["quote"], bitshares_instance=self.bitshares), - account=self.account.name, + Amount(amount=amount, asset=self._market["quote"], bitshares_instance=self.bitshares), + account=self._account.name, expiration=self.expiration, returnOrderId=return_order_id, fee_asset=self.fee_asset['id'], @@ -588,8 +573,8 @@ def place_market_sell_order(self, amount, price, return_none=False, invert=False :param kwargs: :return: """ - symbol = self.market['quote']['symbol'] - precision = self.market['quote']['precision'] + symbol = self._market['quote']['symbol'] + precision = self._market['quote']['precision'] quote_amount = truncate(amount, precision) return_order_id = kwargs.pop('returnOrderId', self.returnOrderId) @@ -600,7 +585,7 @@ def place_market_sell_order(self, amount, price, return_none=False, invert=False return None # Make sure we have enough balance for the order - if return_order_id and self.balance(self.market['quote']) < quote_amount: + if return_order_id and self.balance(self._market['quote']) < quote_amount: self.log.critical("Insufficient sell balance, needed {} {}".format(amount, symbol)) self.disabled = True return None @@ -610,10 +595,10 @@ def place_market_sell_order(self, amount, price, return_none=False, invert=False # Place the order sell_transaction = self.retry_action( - self.market.sell, + self._market.sell, price, - Amount(amount=amount, asset=self.market["quote"]), - account=self.account.name, + Amount(amount=amount, asset=self._market["quote"]), + account=self._account.name, expiration=self.expiration, returnOrderId=return_order_id, fee_asset=self.fee_asset['id'], @@ -654,7 +639,7 @@ def retry_action(self, action, *args, **kwargs): tries += 1 self.log.warning("Ignoring: '{}'".format(str(exception))) self.bitshares.txbuffer.clear() - self.account.refresh() + self._account.refresh() time.sleep(2) elif "now <= trx.expiration" in str(exception): # Usually loss of sync to blockchain if tries > MAX_TRIES: @@ -688,7 +673,7 @@ def balances(self): :return: Balances in list where each asset is in their own Amount object """ - return self.account.balances + return self._account.balances def get_own_orders(self, refresh=True): """ Return the account's open orders in the current market @@ -700,10 +685,11 @@ def get_own_orders(self, refresh=True): # Refresh account data if refresh: - self.account.refresh() + self._account.refresh() - for order in self.account.openorders: - if self.worker_market == order.market and self.account.openorders: + for order in self._account.openorders: + worker_market = self._market.get_string('/') + if worker_market == order.market and self._account.openorders: orders.append(order) return orders @@ -716,14 +702,49 @@ def get_all_own_orders(self, refresh=True): """ # Refresh account data if refresh: - self.account.refresh() + self._account.refresh() orders = [] - for order in self.account.openorders: + for order in self._account.openorders: orders.append(order) return orders + @property + def account(self): + """ Return the full account as :class:`bitshares.account.Account` object! + Can be refreshed by using ``x.refresh()`` + + :return: object | Account + """ + return self._account + + @property + def market(self): + """ Return the market object as :class:`bitshares.market.Market` + """ + return self._market + + @property + def base_asset(self): + return self._market.get_string('/').split('/')[1] + +# return self.worker['market'].split('/')[1] + + @property + def quote_asset(self): + return self._market.get_string('/').split('/')[0] + +# return self.worker['market'].split('/')[0] + + @property + def balances(self): + """ Returns all the balances of the account assigned for the worker. + + :return: Balances in list where each asset is in their own Amount object + """ + return self._account.balances + @property def all_own_orders(self): """ Return the worker's open orders in all markets diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 5c4733c37..52b459364 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -11,9 +11,9 @@ from dexbot.pricefeeds.bitshares_feed import BitsharesPriceFeed import bitshares.exceptions -from bitshares.account import Account -from bitshares.amount import Asset from bitshares.instance import shared_bitshares_instance +from bitshares.amount import Asset +from bitshares.account import Account from bitshares.market import Market from events import Events @@ -137,17 +137,16 @@ def __init__(self, # Get worker's parameters from the config self.worker = config["workers"][name] - # Get Bitshares account and market for this worker - self._account = Account(self.worker["account"], full=True, bitshares_instance=self.bitshares) - - self._market = Market(config["workers"][name]["market"], bitshares_instance=self.bitshares) - # Recheck flag - Tell the strategy to check for updated orders self.recheck_orders = False # Count of orders to be fetched from the API self.fetch_depth = 8 + # Get Bitshares account and market for this worker + self._account = Account(self.worker["account"], full=True, bitshares_instance=self.bitshares) + self._market = Market(config["workers"][name]["market"], bitshares_instance=self.bitshares) + # Set fee asset fee_asset_symbol = self.worker.get('fee_asset') @@ -164,7 +163,7 @@ def __init__(self, self.core_exchange_rate = None # Ticker - self.ticker = self.market.ticker + self.ticker = self._market.ticker # Settings for bitshares instance self.bitshares.bundle = bool(self.worker.get("bundle", False)) @@ -189,8 +188,6 @@ def __init__(self, } ) - self.worker_market = self.worker["market"] - self.orders_log = logging.LoggerAdapter(logging.getLogger('dexbot.orders_log'), {}) def pause(self): @@ -284,37 +281,6 @@ def calc_profit(self): return profit - @property - def account(self): - """ Return the full account as :class:`bitshares.account.Account` object! - Can be refreshed by using ``x.refresh()`` - - :return: object | Account - """ - return self._account - - @property - def balances(self): - """ Returns all the balances of the account assigned for the worker. - - :return: Balances in list where each asset is in their own Amount object - """ - return self._account.balances - - @property - def base_asset(self): - return self.worker['market'].split('/')[1] - - @property - def quote_asset(self): - return self.worker['market'].split('/')[0] - - @property - def market(self): - """ Return the market object as :class:`bitshares.market.Market` - """ - return self._market - @staticmethod def purge_all_local_worker_data(worker_name): """ Removes worker's data and orders from local sqlite database From e265cbcb5582e2b416a6378098fdd9835ae41c75 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Thu, 20 Jun 2019 09:27:41 +0300 Subject: [PATCH 1488/1846] Remove bad nodes from default node list There was quite many bad nodes that didn't respond so removed them to make the default list smaller. --- dexbot/config.py | 44 -------------------------------------------- 1 file changed, 44 deletions(-) diff --git a/dexbot/config.py b/dexbot/config.py index b5ac113d2..65ee74116 100644 --- a/dexbot/config.py +++ b/dexbot/config.py @@ -172,67 +172,23 @@ def node_list(self): "wss://eu.openledger.info/ws", "wss://bitshares.openledger.info/ws", "wss://dexnode.net/ws", - "wss://japan.bitshares.apasia.tech/ws", - "wss://bitshares-api.wancloud.io/ws", "wss://openledger.hk/ws", - "wss://bitshares.apasia.tech/ws", - "wss://bitshares.crypto.fans/ws", "wss://kc-us-dex.xeldal.com/ws", - "wss://api.bts.blckchnd.com", - "wss://btsza.co.za:8091/ws", - "wss://bitshares.dacplay.org/ws", - "wss://bit.btsabc.org/ws", - "wss://bts.ai.la/ws", "wss://ws.gdex.top", "wss://na.openledger.info/ws", - "wss://node.btscharts.com/ws", - "wss://status200.bitshares.apasia.tech/ws", - "wss://new-york.bitshares.apasia.tech/ws", - "wss://dallas.bitshares.apasia.tech/ws", - "wss://chicago.bitshares.apasia.tech/ws", - "wss://atlanta.bitshares.apasia.tech/ws", - "wss://us-la.bitshares.apasia.tech/ws", - "wss://seattle.bitshares.apasia.tech/ws", - "wss://miami.bitshares.apasia.tech/ws", - "wss://valley.bitshares.apasia.tech/ws", - "wss://canada6.daostreet.com", - "wss://bitshares.nu/ws", - "wss://api.open-asset.tech/ws", - "wss://france.bitshares.apasia.tech/ws", - "wss://england.bitshares.apasia.tech/ws", - "wss://netherlands.bitshares.apasia.tech/ws", - "wss://australia.bitshares.apasia.tech/ws", - "wss://dex.rnglab.org", - "wss://la.dexnode.net/ws", - "wss://api-ru.bts.blckchnd.com", - "wss://node.market.rudex.org", - "wss://api.bitsharesdex.com", "wss://api.fr.bitsharesdex.com", - "wss://blockzms.xyz/ws", "wss://eu.nodes.bitshares.ws", "wss://us.nodes.bitshares.ws", "wss://sg.nodes.bitshares.ws", - "wss://ws.winex.pro", "wss://api.bts.mobi/ws", - "wss://api.btsxchng.com", - "wss://api.bts.network/", "wss://btsws.roelandp.nl/ws", "wss://api.bitshares.bhuz.info/ws", "wss://bts-api.lafona.net/ws", "wss://kimziv.com/ws", "wss://api.btsgo.net/ws", - "wss://bts.proxyhosts.info/wss", "wss://bts.open.icowallet.net/ws", - "wss://de.bts.dcn.cx/ws", - "wss://fi.bts.dcn.cx/ws", - "wss://crazybit.online", "wss://freedom.bts123.cc:15138/", - "wss://bitshares.bts123.cc:15138/", "wss://api.bts.ai", - "wss://ws.hellobts.com", - "wss://bitshares.cyberit.io", - "wss://bts-seoul.clockwork.gr", - "wss://bts.liuye.tech:4443/ws", "wss://btsfullnode.bangzi.info/ws", "wss://api.dex.trading/", "wss://citadel.li/node" From f594f11521ececc3c25520c8b623d7ec0eb28f99 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Thu, 20 Jun 2019 11:15:49 +0300 Subject: [PATCH 1489/1846] Add measure_latency() to main_controller.py --- dexbot/controllers/main_controller.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 5d0ff0673..fc00ecd3f 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -10,6 +10,8 @@ from appdirs import user_data_dir from bitshares.bitshares import BitShares from bitshares.instance import set_shared_bitshares_instance +from bitsharesapi.bitsharesnoderpc import BitSharesNodeRPC +from grapheneapi.exceptions import NumRetriesReached class MainController: @@ -93,6 +95,22 @@ def remove_worker(self, worker_name): config = self.config.get_worker_config(worker_name) WorkerInfrastructure.remove_offline_worker(config, worker_name, self.bitshares_instance) + def measure_latency(self, node): + """ Measures latency of given node in milliseconds + + :param String node: Bitshares node address + :return: int: latency in milliseconds + """ + try: + start = time.time() + BitSharesNodeRPC(node, num_retries=1) + latency = (time.time() - start) * 1000 + except NumRetriesReached: + self.log.warning('Coudn\'t connect to {}'.format(node)) + return False + + return latency + @staticmethod def create_worker(worker_name): # Deletes old worker's data From 401ea02710c3b3d5a339196e43a1d987965ebd46 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Thu, 20 Jun 2019 12:48:59 +0300 Subject: [PATCH 1490/1846] Change measure_latency() to staticmethod --- dexbot/controllers/main_controller.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index fc00ecd3f..34fa4e6ad 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -1,6 +1,7 @@ import os import logging import sys +import time from dexbot import VERSION, APP_NAME, AUTHOR from dexbot.helper import initialize_orders_log, initialize_data_folders @@ -95,7 +96,8 @@ def remove_worker(self, worker_name): config = self.config.get_worker_config(worker_name) WorkerInfrastructure.remove_offline_worker(config, worker_name, self.bitshares_instance) - def measure_latency(self, node): + @staticmethod + def measure_latency(node): """ Measures latency of given node in milliseconds :param String node: Bitshares node address @@ -106,7 +108,6 @@ def measure_latency(self, node): BitSharesNodeRPC(node, num_retries=1) latency = (time.time() - start) * 1000 except NumRetriesReached: - self.log.warning('Coudn\'t connect to {}'.format(node)) return False return latency From 7932845e11e494b781b304525831e7e9eb5896a4 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Thu, 20 Jun 2019 13:52:35 +0300 Subject: [PATCH 1491/1846] Change settings window behaviour Changed `Restore Defaults` button position and look Changed `Save` behavior to save changes and close window Add `Close without Saving` discards all current changes and closes window --- dexbot/controllers/settings_controller.py | 21 +++++++---- dexbot/views/settings.py | 15 +++++--- dexbot/views/ui/settings_window.ui | 45 +++-------------------- 3 files changed, 29 insertions(+), 52 deletions(-) diff --git a/dexbot/controllers/settings_controller.py b/dexbot/controllers/settings_controller.py index 374419afd..715246f2e 100644 --- a/dexbot/controllers/settings_controller.py +++ b/dexbot/controllers/settings_controller.py @@ -58,7 +58,7 @@ def move_down(self): self.view.notification_label.setText('Unsaved changes detected; List order has changed.') def save_settings(self): - """ Save items in the tree widget list into the config file and reload the items + """ Save items in the tree widget list into the config file and close window """ nodes = [] @@ -68,8 +68,10 @@ def save_settings(self): nodes.append(self.view.root_item.child(index).text(0)) # Send the nodes to controller to handle the save - self.save_nodes_to_config(nodes) - self.initialize_node_list() + if self.save_nodes_to_config(nodes): + + # Close settings dialog on save + self.view.reject() def remove_node(self): """ Remove item from the widget tree list @@ -106,10 +108,15 @@ def save_nodes_to_config(self, nodes): # Remove empty nodes before saving, this is just to make sure no empty strings end up in config file nodes = self.remove_empty_items(nodes) - self.config['node'] = nodes - self.config.save_config() - # Update status - self.view.notification_label.setText('Settings successfully saved!') + if nodes: + self.config['node'] = nodes + self.config.save_config() + # Update status + self.view.notification_label.setText('Settings successfully saved!') + return True + else: + self.view.notification_label.setText('Can\'t save empty list') + return False def restore_defaults(self): self.initialize_node_list(nodes=self.config.node_list) diff --git a/dexbot/views/settings.py b/dexbot/views/settings.py index 47e124dc0..9cd6fdda4 100644 --- a/dexbot/views/settings.py +++ b/dexbot/views/settings.py @@ -1,10 +1,10 @@ from dexbot.controllers.settings_controller import SettingsController from dexbot.views.ui.settings_window_ui import Ui_settings_dialog -from PyQt5 import QtWidgets +from PyQt5.QtWidgets import QDialog, QDialogButtonBox -class SettingsView(QtWidgets.QDialog, Ui_settings_dialog): +class SettingsView(QDialog, Ui_settings_dialog): def __init__(self): super().__init__() @@ -25,8 +25,13 @@ def __init__(self): self.remove_button.clicked.connect(self.controller.remove_node) self.move_up_button.clicked.connect(self.controller.move_up) self.move_down_button.clicked.connect(self.controller.move_down) - self.restore_defaults_button.clicked.connect(self.controller.restore_defaults) + # self.restore_defaults_button.clicked.connect(self.controller.restore_defaults) # Dialog controls - self.button_box.rejected.connect(self.reject) - self.button_box.accepted.connect(self.controller.save_settings) + self.restore_defaults = self.button_box.button(QDialogButtonBox.RestoreDefaults) + self.discard = self.button_box.button(QDialogButtonBox.Discard) + self.save = self.button_box.button(QDialogButtonBox.Save) + + self.discard.clicked.connect(self.reject) + self.restore_defaults.clicked.connect(self.controller.restore_defaults) + self.save.clicked.connect(self.controller.save_settings) diff --git a/dexbot/views/ui/settings_window.ui b/dexbot/views/ui/settings_window.ui index f56deb0a6..1ae031b55 100644 --- a/dexbot/views/ui/settings_window.ui +++ b/dexbot/views/ui/settings_window.ui @@ -118,7 +118,7 @@ border: 0px; background-color: #623a3a; width: 100px; height: 20px; border-radius: 5px; color: #fff; - Delete + Remove @@ -176,44 +176,6 @@ - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 20 - - - - - - - - - 0 - 0 - - - - - 60 - 20 - - - - border: 0px; background-color: #3a5162; width: 100px; height: 20px; border-radius: 5px; color: #fff; - - - Defaults - - - @@ -274,8 +236,11 @@ + + Qt::Horizontal + - QDialogButtonBox::Cancel|QDialogButtonBox::Save + QDialogButtonBox::Discard|QDialogButtonBox::RestoreDefaults|QDialogButtonBox::Save false From 8e9de6cf2e0f908ca9175ed00c3875ece7e8c849 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Thu, 20 Jun 2019 14:01:33 +0300 Subject: [PATCH 1492/1846] Change main view behavior when unlocking wallet DEXBot won't try to connect to Bitshares if there is already a Bitshares instance or the node is not reponding. --- dexbot/views/worker_list.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 3e9c943bf..9a718982c 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -51,11 +51,33 @@ def __init__(self, main_controller): QFontDatabase.addApplicationFont(":/bot_widget/font/SourceSansPro-Bold.ttf") + def connect_to_bitshares(self): + # Check if there is already a connection + if self.config['node']: + # Test nodes first + self.status_bar.showMessage('Connecting to Bitshares...') + latency = self.main_controller.measure_latency(self.config['node']) + + if not latency: + self.status_bar.showMessage('ver {} - Coudn\'t connect to Bitshares. ' + 'Please use different node(s) and retry.'.format(__version__)) + self.main_controller.set_bitshares_instance(None) + return False + + self.main_controller.new_bitshares_instance(self.config['node']) + self.status_bar.showMessage(self.get_statusbar_message()) + return True + else: + # Config has no nodes in it + self.status_bar.showMessage('ver {} - Node(s) not found. ' + 'Please add node(s) from settings.'.format(__version__)) + return False + def handle_login(self): - """ This function handles login to the wallet. To avoid lag when creating + if not self.main_controller.bitshares_instance: + if not self.connect_to_bitshares(): + return - """ - self.main_controller.new_bitshares_instance(self.config['node']) wallet_controller = WalletController(self.main_controller.bitshares_instance) if wallet_controller.wallet_created(): @@ -131,6 +153,8 @@ def handle_open_settings(self): self.config = Config() self.main_controller.config = self.config + self.connect_to_bitshares() + @staticmethod def handle_open_documentation(): webbrowser.open('https://github.com/Codaone/DEXBot/wiki') From 9ff6d84ccef38f845a5fad802a775590873e14fb Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 20 Jun 2019 11:35:04 -0700 Subject: [PATCH 1493/1846] remove duplicate balances property --- dexbot/orderengines/bitshares_engine.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index ecd304e14..4df8fd649 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -667,13 +667,6 @@ def retry_action(self, action, *args, **kwargs): else: raise - @property - def balances(self): - """ Returns all the balances of the account assigned for the worker. - - :return: Balances in list where each asset is in their own Amount object - """ - return self._account.balances def get_own_orders(self, refresh=True): """ Return the account's open orders in the current market From b85871cd3d04b80e3af66dd00094f8e7d2ddd60b Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 20 Jun 2019 12:12:51 -0700 Subject: [PATCH 1494/1846] put back balances properties --- dexbot/orderengines/bitshares_engine.py | 18 ++++++------------ dexbot/strategies/base.py | 9 +++++++++ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index 4df8fd649..ea75d6b1e 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -667,6 +667,12 @@ def retry_action(self, action, *args, **kwargs): else: raise + @property + def balances(self): + """ Returns all the balances of the account assigned for the worker. + :return: Balances in list where each asset is in their own Amount object + """ + return self._account.balances def get_own_orders(self, refresh=True): """ Return the account's open orders in the current market @@ -722,22 +728,10 @@ def market(self): def base_asset(self): return self._market.get_string('/').split('/')[1] -# return self.worker['market'].split('/')[1] - @property def quote_asset(self): return self._market.get_string('/').split('/')[0] -# return self.worker['market'].split('/')[0] - - @property - def balances(self): - """ Returns all the balances of the account assigned for the worker. - - :return: Balances in list where each asset is in their own Amount object - """ - return self._account.balances - @property def all_own_orders(self): """ Return the worker's open orders in all markets diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 52b459364..7068b93ed 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -281,6 +281,15 @@ def calc_profit(self): return profit + @property + def balances(self): + """ Returns all the balances of the account assigned for the worker. + + :return: Balances in list where each asset is in their own Amount object + """ + return self._account.balances + + @staticmethod def purge_all_local_worker_data(worker_name): """ Removes worker's data and orders from local sqlite database From 5c32aa8c207179cadfe4b55063018416164dbb37 Mon Sep 17 00:00:00 2001 From: "hapax.io" <39811582+thehapax@users.noreply.github.com> Date: Thu, 20 Jun 2019 16:01:49 -0700 Subject: [PATCH 1495/1846] too many blank lines fix --- dexbot/strategies/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 7068b93ed..ad79d7e08 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -289,7 +289,6 @@ def balances(self): """ return self._account.balances - @staticmethod def purge_all_local_worker_data(worker_name): """ Removes worker's data and orders from local sqlite database From 7ed599f22e969680002f526afabfef8528cc9140 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 21 Jun 2019 12:40:16 +0500 Subject: [PATCH 1496/1846] Add small delay before yielding testnet bitshares-core node may take several seconds to fully start API. The easiest way is to add small delay. --- tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 5c810b485..2cb2cbf63 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ import pytest import socket import random +import time from bitshares import BitShares from bitshares.instance import set_shared_bitshares_instance @@ -77,6 +78,7 @@ def bitshares_testnet(session_id, unused_port, docker_manager): detach=True, ) container.service_port = port + time.sleep(3) yield container container.remove(v=True, force=True) From c33696efb4a1b80308dddc008878d9c15747ae92 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 21 Jun 2019 12:43:22 +0500 Subject: [PATCH 1497/1846] Refactor measure_latency() Instead of measuring latency for several nodes at once, measure latency independently for each node until working one is found. Also handle "Connection refused" errors, DNS resolution errors and so on. Instead of returning False, raise NumRetriesReached if all nodes failed. --- dexbot/controllers/main_controller.py | 30 +++++++++++++++++---------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 34fa4e6ad..86f07ea2b 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -97,20 +97,28 @@ def remove_worker(self, worker_name): WorkerInfrastructure.remove_offline_worker(config, worker_name, self.bitshares_instance) @staticmethod - def measure_latency(node): - """ Measures latency of given node in milliseconds + def measure_latency(nodes): + """ Measures latency of first alive node from given nodes in milliseconds - :param String node: Bitshares node address + :param str,list nodes: Bitshares node address(-es) :return: int: latency in milliseconds + :raises grapheneapi.exceptions.NumRetriesReached: if failed to find a working node """ - try: - start = time.time() - BitSharesNodeRPC(node, num_retries=1) - latency = (time.time() - start) * 1000 - except NumRetriesReached: - return False - - return latency + if isinstance(nodes, str): + nodes = [nodes] + + # Check nodes one-by-one until first working found + for node in nodes: + try: + start = time.time() + BitSharesNodeRPC(node, num_retries=1) + latency = (time.time() - start) * 1000 + return latency + except (NumRetriesReached, OSError): + # [Errno 111] Connection refused -> OSError + continue + + raise NumRetriesReached @staticmethod def create_worker(worker_name): From 0e280cfa8cc25262a2395bbfc9605c586c82b7d4 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 21 Jun 2019 12:51:16 +0500 Subject: [PATCH 1498/1846] Wrap measure_latency() in try/except See https://docs.quantifiedcode.com/python-anti-patterns/readability/asking_for_permission_instead_of_forgiveness_when_working_with_files.html --- dexbot/views/worker_list.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 9a718982c..8e18bf8f7 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -19,6 +19,7 @@ from PyQt5.QtGui import QFontDatabase from PyQt5.QtWidgets import QMainWindow from bitsharesapi.bitsharesnoderpc import BitSharesNodeRPC +from grapheneapi.exceptions import NumRetriesReached class MainView(QMainWindow, Ui_MainWindow): @@ -54,11 +55,11 @@ def __init__(self, main_controller): def connect_to_bitshares(self): # Check if there is already a connection if self.config['node']: - # Test nodes first + # Test nodes first. This only checks if we're able to connect self.status_bar.showMessage('Connecting to Bitshares...') - latency = self.main_controller.measure_latency(self.config['node']) - - if not latency: + try: + self.main_controller.measure_latency(self.config['node']) + except NumRetriesReached: self.status_bar.showMessage('ver {} - Coudn\'t connect to Bitshares. ' 'Please use different node(s) and retry.'.format(__version__)) self.main_controller.set_bitshares_instance(None) From 1def739e66c26461e897ba43b3b8b1472b84ea2d Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 21 Jun 2019 13:04:27 +0500 Subject: [PATCH 1499/1846] Refactor get_statusbar_message() * Use new method measure_latency() instead of own implementation * Measure latency of a currently connected node. Before this the latency of a first alive node was measured despite of what node was currently connected --- dexbot/views/worker_list.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 8e18bf8f7..f6ac94142 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -18,7 +18,6 @@ from PyQt5.QtGui import QFontDatabase from PyQt5.QtWidgets import QMainWindow -from bitsharesapi.bitsharesnoderpc import BitSharesNodeRPC from grapheneapi.exceptions import NumRetriesReached @@ -202,16 +201,14 @@ def _update_statusbar_message(self): time.sleep(0.5) def get_statusbar_message(self): - node = self.config['node'] + node = self.main_controller.bitshares_instance.rpc.url try: - start = time.time() - rpc = BitSharesNodeRPC(node, num_retries=1) - latency = (time.time() - start) * 1000 + latency = self.main_controller.measure_latency(node) except BaseException: latency = -1 if latency != -1: - return "ver {} - Node delay: {:.2f}ms - node: {}".format(__version__, latency, rpc.url) + return "ver {} - Node delay: {:.2f}ms - node: {}".format(__version__, latency, node) else: return "ver {} - Node disconnected".format(__version__) From b53883ca55562537119fb94d548fe7153b49211b Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 21 Jun 2019 13:09:24 +0500 Subject: [PATCH 1500/1846] Add test for MainController.measure_latency() --- tests/test_measure_latency.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/test_measure_latency.py diff --git a/tests/test_measure_latency.py b/tests/test_measure_latency.py new file mode 100644 index 000000000..b96edd70d --- /dev/null +++ b/tests/test_measure_latency.py @@ -0,0 +1,30 @@ +import pytest + +from dexbot.controllers.main_controller import MainController +from grapheneapi.exceptions import NumRetriesReached + + +@pytest.fixture +def failing_nodes(unused_port): + nodes = ['wss://localhost:{}'.format(unused_port()) for _ in range(3)] + return nodes + + +@pytest.fixture +def many_failing_one_working(unused_port, bitshares_testnet): + nodes = ['wss://localhost:{}'.format(unused_port()) for _ in range(3)] + nodes.append('ws://127.0.0.1:{}'.format(bitshares_testnet.service_port)) + return nodes + + +def test_measure_latency_all_failing(failing_nodes): + """ Expect an error if no nodes could be reached + """ + with pytest.raises(NumRetriesReached): + MainController.measure_latency(failing_nodes) + + +def test_measure_latency_one_working(many_failing_one_working): + """ Test connection to 3 nodes where only 3rd is working + """ + MainController.measure_latency(many_failing_one_working) From ea4f1552b8a85a9a2c36b6b93f01feac594cec63 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Mon, 24 Jun 2019 07:55:03 +0300 Subject: [PATCH 1501/1846] Remove old commented code --- dexbot/views/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dexbot/views/settings.py b/dexbot/views/settings.py index 9cd6fdda4..aeebc7b43 100644 --- a/dexbot/views/settings.py +++ b/dexbot/views/settings.py @@ -25,7 +25,6 @@ def __init__(self): self.remove_button.clicked.connect(self.controller.remove_node) self.move_up_button.clicked.connect(self.controller.move_up) self.move_down_button.clicked.connect(self.controller.move_down) - # self.restore_defaults_button.clicked.connect(self.controller.restore_defaults) # Dialog controls self.restore_defaults = self.button_box.button(QDialogButtonBox.RestoreDefaults) From 92d9f79bb811c3636d4ba55eba6cb0a2712ddcef Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Mon, 24 Jun 2019 08:12:47 +0300 Subject: [PATCH 1502/1846] Refactor GUI buttons event connections This will get rid of using lambda functions when connecting buttons to events by using pyqtSlots instead. --- dexbot/views/worker_list.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index f6ac94142..b22abc7fa 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -16,6 +16,7 @@ from dexbot.qt_queue.idle_queue import idle_add from dexbot.qt_queue.queue_dispatcher import ThreadDispatcher +from PyQt5.QtCore import pyqtSlot from PyQt5.QtGui import QFontDatabase from PyQt5.QtWidgets import QMainWindow from grapheneapi.exceptions import NumRetriesReached @@ -39,10 +40,11 @@ def __init__(self, main_controller): self.layout = FlowLayout(self.scrollAreaContent) self.dispatcher = None - self.add_worker_button.clicked.connect(lambda: self.handle_add_worker()) - self.settings_button.clicked.connect(lambda: self.handle_open_settings()) - self.help_button.clicked.connect(lambda: self.handle_open_documentation()) - self.unlock_wallet_button.clicked.connect(lambda: self.handle_login()) + # GUI buttons + self.add_worker_button.clicked.connect(self.handle_add_worker) + self.settings_button.clicked.connect(self.handle_open_settings) + self.help_button.clicked.connect(self.handle_open_documentation) + self.unlock_wallet_button.clicked.connect(self.handle_login) # Hide certain buttons by default until login success self.add_worker_button.hide() @@ -73,6 +75,7 @@ def connect_to_bitshares(self): 'Please add node(s) from settings.'.format(__version__)) return False + @pyqtSlot(name='handle_login') def handle_login(self): if not self.main_controller.bitshares_instance: if not self.connect_to_bitshares(): @@ -131,6 +134,7 @@ def change_worker_widget_name(self, old_worker_name, new_worker_name): worker_data = self.worker_widgets.pop(old_worker_name) self.worker_widgets[new_worker_name] = worker_data + @pyqtSlot(name='handle_add_worker') @gui_error def handle_add_worker(self): create_worker_dialog = CreateWorkerView(self.main_controller.bitshares_instance) @@ -144,6 +148,7 @@ def handle_add_worker(self): self.config.add_worker_config(worker_name, create_worker_dialog.worker_data) self.add_worker_widget(worker_name) + @pyqtSlot(name='handle_open_settings') @gui_error def handle_open_settings(self): settings_dialog = SettingsView() @@ -156,6 +161,7 @@ def handle_open_settings(self): self.connect_to_bitshares() @staticmethod + @pyqtSlot(name='handle_open_documentation') def handle_open_documentation(): webbrowser.open('https://github.com/Codaone/DEXBot/wiki') From 9a55fabaf4200fed4418ad5b8199c3b253daa581 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 26 Jun 2019 10:08:48 +0300 Subject: [PATCH 1503/1846] Fix error when opening settings window Error appeared when the config file doesn't have any nodes --- dexbot/controllers/settings_controller.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/dexbot/controllers/settings_controller.py b/dexbot/controllers/settings_controller.py index 715246f2e..ad6dae3af 100644 --- a/dexbot/controllers/settings_controller.py +++ b/dexbot/controllers/settings_controller.py @@ -96,11 +96,13 @@ def initialize_node_list(self, nodes=None): if nodes is None: nodes = self.view.controller.nodes - # Add nodes to the widget list - for node in nodes: - item = QTreeWidgetItem(self.view.nodes_tree_widget) - item.setText(0, node) - item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) + # Check for nodes, since it is possible that the config is empty + if nodes: + # Add nodes to the widget list + for node in nodes: + item = QTreeWidgetItem(self.view.nodes_tree_widget) + item.setText(0, node) + item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) def save_nodes_to_config(self, nodes): """ Save nodes to the config file From 19203626fbf2be753189040eb7111cb1d983f6ca Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 26 Jun 2019 10:10:38 +0300 Subject: [PATCH 1504/1846] Change settings save --- dexbot/controllers/settings_controller.py | 4 +++- dexbot/views/worker_list.py | 11 ++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/dexbot/controllers/settings_controller.py b/dexbot/controllers/settings_controller.py index ad6dae3af..8ccc7baf3 100644 --- a/dexbot/controllers/settings_controller.py +++ b/dexbot/controllers/settings_controller.py @@ -59,6 +59,8 @@ def move_down(self): def save_settings(self): """ Save items in the tree widget list into the config file and close window + + :returns int: 1 settings saved (accepted) """ nodes = [] @@ -71,7 +73,7 @@ def save_settings(self): if self.save_nodes_to_config(nodes): # Close settings dialog on save - self.view.reject() + self.view.accept() def remove_node(self): """ Remove item from the widget tree list diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index b22abc7fa..18b54272d 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -152,13 +152,14 @@ def handle_add_worker(self): @gui_error def handle_open_settings(self): settings_dialog = SettingsView() - settings_dialog.exec_() + reconnect = settings_dialog.exec_() - # Reinitialize config after closing the settings window - self.config = Config() - self.main_controller.config = self.config + if reconnect: + # Reinitialize config after closing the settings window + self.config = Config() + self.main_controller.config = self.config - self.connect_to_bitshares() + self.connect_to_bitshares() @staticmethod @pyqtSlot(name='handle_open_documentation') From a4380b7c26cc04ef67941e85719988c8ef3c5262 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Wed, 26 Jun 2019 11:32:51 +0300 Subject: [PATCH 1505/1846] Change closing the settings window to reconnect only when saving --- dexbot/views/worker_list.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 18b54272d..b22abc7fa 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -152,14 +152,13 @@ def handle_add_worker(self): @gui_error def handle_open_settings(self): settings_dialog = SettingsView() - reconnect = settings_dialog.exec_() + settings_dialog.exec_() - if reconnect: - # Reinitialize config after closing the settings window - self.config = Config() - self.main_controller.config = self.config + # Reinitialize config after closing the settings window + self.config = Config() + self.main_controller.config = self.config - self.connect_to_bitshares() + self.connect_to_bitshares() @staticmethod @pyqtSlot(name='handle_open_documentation') From c788474b22db6675e6c056fdb9393942751d48bf Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Thu, 27 Jun 2019 09:07:45 +0300 Subject: [PATCH 1506/1846] Revert "Change closing the settings window to reconnect only when saving" This reverts commit a4380b7c26cc04ef67941e85719988c8ef3c5262. --- dexbot/views/worker_list.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index b22abc7fa..18b54272d 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -152,13 +152,14 @@ def handle_add_worker(self): @gui_error def handle_open_settings(self): settings_dialog = SettingsView() - settings_dialog.exec_() + reconnect = settings_dialog.exec_() - # Reinitialize config after closing the settings window - self.config = Config() - self.main_controller.config = self.config + if reconnect: + # Reinitialize config after closing the settings window + self.config = Config() + self.main_controller.config = self.config - self.connect_to_bitshares() + self.connect_to_bitshares() @staticmethod @pyqtSlot(name='handle_open_documentation') From f35dd1e2a49f42fd40aa1f65d79b63a6f641c4be Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Thu, 27 Jun 2019 11:32:10 +0300 Subject: [PATCH 1507/1846] Change dexbot version number to 0.12.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 44125bfbe..4417c56f7 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.11.12' +VERSION = '0.12.0' AUTHOR = 'Codaone Oy' __version__ = VERSION From eff1f3c054a0079ce924df0e2b5d1936a7d45341 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Wed, 15 May 2019 22:54:12 +0200 Subject: [PATCH 1508/1846] Add windows installer --- installer/windows/.gitignore | 61 +++++++++++ .../windows/bundle/assets/dexbot-icon.ico | Bin 0 -> 93062 bytes .../windows/bundle/assets/dexbot-icon.jpg | Bin 0 -> 28403 bytes .../windows/bundle/assets/dexbot-icon130.jpg | Bin 0 -> 19782 bytes .../windows/bundle/assets/dexbot-icon156.jpg | Bin 0 -> 29358 bytes installer/windows/bundle/bundle.wixproj | 62 +++++++++++ installer/windows/bundle/bundle.wxs | 34 ++++++ .../windows/bundle/prerequisites/uptick.bat | 1 + .../bundle/resources/classic_theme.wxl | 51 +++++++++ .../bundle/resources/classic_theme.xml | 58 ++++++++++ .../windows/bundle/resources/license.rtf | 53 ++++++++++ installer/windows/dexbot_installer.sln | 31 ++++++ installer/windows/msi/Product.wxs | 99 ++++++++++++++++++ installer/windows/msi/README.txt | 18 ++++ installer/windows/msi/assets/dexbot-icon.ico | Bin 0 -> 93062 bytes installer/windows/msi/assets/dexbot-icon.jpg | Bin 0 -> 29358 bytes installer/windows/msi/msi.wixproj | 53 ++++++++++ 17 files changed, 521 insertions(+) create mode 100644 installer/windows/.gitignore create mode 100644 installer/windows/bundle/assets/dexbot-icon.ico create mode 100644 installer/windows/bundle/assets/dexbot-icon.jpg create mode 100644 installer/windows/bundle/assets/dexbot-icon130.jpg create mode 100644 installer/windows/bundle/assets/dexbot-icon156.jpg create mode 100644 installer/windows/bundle/bundle.wixproj create mode 100644 installer/windows/bundle/bundle.wxs create mode 100644 installer/windows/bundle/prerequisites/uptick.bat create mode 100644 installer/windows/bundle/resources/classic_theme.wxl create mode 100644 installer/windows/bundle/resources/classic_theme.xml create mode 100644 installer/windows/bundle/resources/license.rtf create mode 100644 installer/windows/dexbot_installer.sln create mode 100644 installer/windows/msi/Product.wxs create mode 100644 installer/windows/msi/README.txt create mode 100644 installer/windows/msi/assets/dexbot-icon.ico create mode 100644 installer/windows/msi/assets/dexbot-icon.jpg create mode 100644 installer/windows/msi/msi.wixproj diff --git a/installer/windows/.gitignore b/installer/windows/.gitignore new file mode 100644 index 000000000..754bddb54 --- /dev/null +++ b/installer/windows/.gitignore @@ -0,0 +1,61 @@ + +DexbotInstallerBootstrapper/bin/Debug + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc diff --git a/installer/windows/bundle/assets/dexbot-icon.ico b/installer/windows/bundle/assets/dexbot-icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3b965e21e0591b85a69f59d8213f4650d9b74624 GIT binary patch literal 93062 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 RuoSQquoSQq_>ZT+{{lNO#@_${ literal 0 HcmV?d00001 diff --git a/installer/windows/bundle/assets/dexbot-icon.jpg b/installer/windows/bundle/assets/dexbot-icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b1e13442a3652ced0b9fff74bdb9e5287141138a GIT binary patch literal 28403 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 literal 0 HcmV?d00001 diff --git a/installer/windows/bundle/assets/dexbot-icon156.jpg b/installer/windows/bundle/assets/dexbot-icon156.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9b9adff80ca4d6288aecdb7577112d24d955637c GIT binary patch literal 29358 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 literal 0 HcmV?d00001 diff --git a/installer/windows/bundle/bundle.wixproj b/installer/windows/bundle/bundle.wixproj new file mode 100644 index 000000000..e8bd4bf53 --- /dev/null +++ b/installer/windows/bundle/bundle.wixproj @@ -0,0 +1,62 @@ + + + + Debug + x86 + 3.10 + 0a6fa202-7443-4f18-a462-f77c543bf921 + 2.0 + DEXBotInstallerBundle + Bundle + bundle + + + bin\$(Configuration)\ + obj\$(Configuration)\ + Debug + + + bin\$(Configuration)\ + obj\$(Configuration)\ + + + + + + + $(WixExtDir)\WixBalExtension.dll + WixBalExtension + + + + + + + + + + + + + + + Always + + + + + + + + + + + + \ No newline at end of file diff --git a/installer/windows/bundle/bundle.wxs b/installer/windows/bundle/bundle.wxs new file mode 100644 index 000000000..92aba4cd4 --- /dev/null +++ b/installer/windows/bundle/bundle.wxs @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + diff --git a/installer/windows/bundle/prerequisites/uptick.bat b/installer/windows/bundle/prerequisites/uptick.bat new file mode 100644 index 000000000..839484557 --- /dev/null +++ b/installer/windows/bundle/prerequisites/uptick.bat @@ -0,0 +1 @@ +pip install uptick \ No newline at end of file diff --git a/installer/windows/bundle/resources/classic_theme.wxl b/installer/windows/bundle/resources/classic_theme.wxl new file mode 100644 index 000000000..d787daa1f --- /dev/null +++ b/installer/windows/bundle/resources/classic_theme.wxl @@ -0,0 +1,51 @@ + + + [WixBundleName] Setup + [WixBundleName] + Are you sure you want to cancel? + Setup Help + + /install | /repair | /uninstall | /layout [directory] - installs, repairs, uninstalls or + creates a complete local copy of the bundle in directory. Install is the default. + + /passive | /quiet - displays minimal UI with no prompts or displays no UI and + no prompts. By default UI and all prompts are displayed. + + /norestart - suppress any attempts to restart. By default UI will prompt before restart. + /log log.txt - logs to a specific file. By default a log file is created in %TEMP%. + + &Close + I &agree to the license terms and conditions + &Options + &Install + &Close + Setup Options + Install location: + &Browse + &OK + &Cancel + Setup Progress[WixBundleAction] + Please wait while DEXBot will be installed. + Processing: + Initializing... + &Cancel + Repair/Uninstall DEXBot + Please choose wether to repair or uninstall DEXBot. + &Repair + &Uninstall + &Close + Setup Successful + Setup successfully installed. Click Launch to run DEXBot or click Close to run it later. + &Launch + You must restart your computer before you can use the software. + &Restart + &Close + Setup Failed + For more information see the <a href="#">log file</a>. + You must restart your computer to complete the rollback of the software. + &Restart + &Close + [WixBundleName] Installation + Click Install to install [WixBundleName] on your computer. + By installing you accept these <a href="#">license terms</a> + \ No newline at end of file diff --git a/installer/windows/bundle/resources/classic_theme.xml b/installer/windows/bundle/resources/classic_theme.xml new file mode 100644 index 000000000..412e7cf78 --- /dev/null +++ b/installer/windows/bundle/resources/classic_theme.xml @@ -0,0 +1,58 @@ + + + #(loc.Caption) + Segoe UI + Segoe UI + Segoe UI + Segoe UI + Segoe UI + Segoe UI + Segoe UI + + + + #(loc.HelpHeader) + #(loc.HelpText) + + + + #(loc.WillInstall) + #(loc.WillInstallDescription) + #(loc.InstallLicenseLinkText) + + + + + + + #(loc.ProgressHeader) + #(loc.ProgressDescription) + + #(loc.OverallProgressPackageText) + + + + + #(loc.ModifyHeader) + #(loc.ModifyDescription) + + + + + + #(loc.SuccessHeader) + #(loc.SuccessDescription) + + #(loc.SuccessRestartText) + + + + + #(loc.FailureHeader) + #(loc.FailureHyperlinkLogText) + + #(loc.FailureRestartText) + + + + \ No newline at end of file diff --git a/installer/windows/bundle/resources/license.rtf b/installer/windows/bundle/resources/license.rtf new file mode 100644 index 000000000..07e3fcc4b --- /dev/null +++ b/installer/windows/bundle/resources/license.rtf @@ -0,0 +1,53 @@ + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +################################################################################### + +Furthermore the install wizard will install Python, Pip and Uptick on your machine. + +################################################################################### + +Python License: + +https://docs.python.org/3/license.html + +################################################################################### + +Pip License: + +https://github.com/pypa/pip/blob/master/LICENSE.txt + +################################################################################### + +Uptick License: + +The MIT License (MIT) + +Copyright (c) 2017-2018 ChainSquad GmbH +Copyright (c) 2015-2016 Fabian Schuh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +################################################################################### \ No newline at end of file diff --git a/installer/windows/dexbot_installer.sln b/installer/windows/dexbot_installer.sln new file mode 100644 index 000000000..9e8ae93eb --- /dev/null +++ b/installer/windows/dexbot_installer.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.572 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "msi", "msi\msi.wixproj", "{95B3AE56-37CC-4CDF-813C-6566698D5709}" +EndProject +Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "bundle", "bundle\bundle.wixproj", "{0A6FA202-7443-4F18-A462-F77C543BF921}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x86 = Debug|x86 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {95B3AE56-37CC-4CDF-813C-6566698D5709}.Debug|x86.ActiveCfg = Debug|x86 + {95B3AE56-37CC-4CDF-813C-6566698D5709}.Debug|x86.Build.0 = Debug|x86 + {95B3AE56-37CC-4CDF-813C-6566698D5709}.Release|x86.ActiveCfg = Release|x86 + {95B3AE56-37CC-4CDF-813C-6566698D5709}.Release|x86.Build.0 = Release|x86 + {0A6FA202-7443-4F18-A462-F77C543BF921}.Debug|x86.ActiveCfg = Debug|x86 + {0A6FA202-7443-4F18-A462-F77C543BF921}.Debug|x86.Build.0 = Debug|x86 + {0A6FA202-7443-4F18-A462-F77C543BF921}.Release|x86.ActiveCfg = Release|x86 + {0A6FA202-7443-4F18-A462-F77C543BF921}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {64E79EE7-3984-4CFD-8932-28A7BB3AF21D} + EndGlobalSection +EndGlobal diff --git a/installer/windows/msi/Product.wxs b/installer/windows/msi/Product.wxs new file mode 100644 index 000000000..520a80cf4 --- /dev/null +++ b/installer/windows/msi/Product.wxs @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/installer/windows/msi/README.txt b/installer/windows/msi/README.txt new file mode 100644 index 000000000..342bd9cce --- /dev/null +++ b/installer/windows/msi/README.txt @@ -0,0 +1,18 @@ +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +You can have a look into the source code on Github. +https://github.com/Codaone/DEXBot/ + +If you have any trouble using DEXBot, please visit our telegram chat room or contact us via our website. + +Website: +https://dexbot.info + +Telegram: +https://t.me/DEXBOTbts \ No newline at end of file diff --git a/installer/windows/msi/assets/dexbot-icon.ico b/installer/windows/msi/assets/dexbot-icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3b965e21e0591b85a69f59d8213f4650d9b74624 GIT binary patch literal 93062 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 RuoSQquoSQq_>ZT+{{lNO#@_${ literal 0 HcmV?d00001 diff --git a/installer/windows/msi/assets/dexbot-icon.jpg b/installer/windows/msi/assets/dexbot-icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9b9adff80ca4d6288aecdb7577112d24d955637c GIT binary patch literal 29358 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 literal 0 HcmV?d00001 diff --git a/installer/windows/msi/msi.wixproj b/installer/windows/msi/msi.wixproj new file mode 100644 index 000000000..ce9ed82d4 --- /dev/null +++ b/installer/windows/msi/msi.wixproj @@ -0,0 +1,53 @@ + + + + Debug + x86 + 3.10 + 95b3ae56-37cc-4cdf-813c-6566698d5709 + 2.0 + DEXBotInstaller + Package + msi + + + bin\$(Configuration)\ + obj\$(Configuration)\ + Debug + + + bin\$(Configuration)\ + obj\$(Configuration)\ + + + + + + + + + + + + + + + + $(WixExtDir)\WixUtilExtension.dll + WixUtilExtension + + + + + + + + + \ No newline at end of file From f2735949bf4f58a5b9e46701a3a31da5dac87f2f Mon Sep 17 00:00:00 2001 From: dominic22 Date: Sun, 26 May 2019 22:26:43 +0200 Subject: [PATCH 1509/1846] Add variables, adjust texts --- installer/windows/bundle/bundle.wixproj | 3 ++- installer/windows/bundle/bundle.wxs | 23 +++++++++++++------ .../bundle/resources/classic_theme.wxl | 13 +++++++---- .../bundle/resources/classic_theme.xml | 9 ++++---- .../windows/bundle/resources/variables.wxi | 6 +++++ installer/windows/msi/Product.wxs | 20 ++++------------ 6 files changed, 43 insertions(+), 31 deletions(-) create mode 100644 installer/windows/bundle/resources/variables.wxi diff --git a/installer/windows/bundle/bundle.wixproj b/installer/windows/bundle/bundle.wixproj index e8bd4bf53..5f68b11b3 100644 --- a/installer/windows/bundle/bundle.wixproj +++ b/installer/windows/bundle/bundle.wixproj @@ -6,7 +6,7 @@ 3.10 0a6fa202-7443-4f18-a462-f77c543bf921 2.0 - DEXBotInstallerBundle + DEXBot-installer Bundle bundle @@ -42,6 +42,7 @@ Always + diff --git a/installer/windows/bundle/bundle.wxs b/installer/windows/bundle/bundle.wxs index 92aba4cd4..7b528eb6d 100644 --- a/installer/windows/bundle/bundle.wxs +++ b/installer/windows/bundle/bundle.wxs @@ -1,9 +1,12 @@ - + + @@ -16,6 +19,14 @@ + + + + + + + + - - - - + + diff --git a/installer/windows/bundle/resources/classic_theme.wxl b/installer/windows/bundle/resources/classic_theme.wxl index d787daa1f..f35162c2c 100644 --- a/installer/windows/bundle/resources/classic_theme.wxl +++ b/installer/windows/bundle/resources/classic_theme.wxl @@ -24,17 +24,22 @@ &Browse &OK &Cancel - Setup Progress[WixBundleAction] - Please wait while DEXBot will be installed. + + + Setup Progress + Please wait until the setup is completed... Processing: Initializing... &Cancel Repair/Uninstall DEXBot - Please choose wether to repair or uninstall DEXBot. + Please click Repair to restore all files and shortcuts or click Uninstall to remove DEXBot v[WixBundleVersion]. &Repair &Uninstall &Close Setup Successful + DEXBot v[WixBundleVersion] successfully installed. Click Launch to run DEXBot or Close to run it at a later time. + DEXBot v[WixBundleVersion] successfully removed from your computer. + DEXBot v[WixBundleVersion] successfuly repaired. Click Launch to run DEXBot or Close to run in at a later time. Setup successfully installed. Click Launch to run DEXBot or click Close to run it later. &Launch You must restart your computer before you can use the software. @@ -46,6 +51,6 @@ &Restart &Close [WixBundleName] Installation - Click Install to install [WixBundleName] on your computer. + Click Install to install [WixBundleName] v[WixBundleVersion] on your computer. By installing you accept these <a href="#">license terms</a> \ No newline at end of file diff --git a/installer/windows/bundle/resources/classic_theme.xml b/installer/windows/bundle/resources/classic_theme.xml index 412e7cf78..0211c3c1b 100644 --- a/installer/windows/bundle/resources/classic_theme.xml +++ b/installer/windows/bundle/resources/classic_theme.xml @@ -27,21 +27,22 @@ #(loc.ProgressHeader) #(loc.ProgressDescription) - #(loc.OverallProgressPackageText) #(loc.ModifyHeader) - #(loc.ModifyDescription) + #(loc.ModifyDescription) - #(loc.SuccessHeader) - #(loc.SuccessDescription) + #(loc.SuccessHeader) + #(loc.SuccessInstallHeader) + #(loc.SuccessUninstallHeader) + #(loc.SuccessRepairHeader) #(loc.SuccessRestartText) diff --git a/installer/windows/bundle/resources/variables.wxi b/installer/windows/bundle/resources/variables.wxi new file mode 100644 index 000000000..ec88824b8 --- /dev/null +++ b/installer/windows/bundle/resources/variables.wxi @@ -0,0 +1,6 @@ + + + + + + diff --git a/installer/windows/msi/Product.wxs b/installer/windows/msi/Product.wxs index 520a80cf4..74dd75eff 100644 --- a/installer/windows/msi/Product.wxs +++ b/installer/windows/msi/Product.wxs @@ -1,10 +1,11 @@ + @@ -14,17 +15,6 @@ - - - - - - - @@ -57,7 +47,7 @@ - + From 9c4167fe3c1d5a70d236dedab4b7748d94e9e91a Mon Sep 17 00:00:00 2001 From: dominic22 Date: Fri, 31 May 2019 16:39:26 +0200 Subject: [PATCH 1510/1846] Initial try to add installer into build process --- appveyor.yml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 4db634823..8d033551c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,5 @@ version: 0.1.{build} -image: Visual Studio 2015 - environment: matrix: # Python 3.6.6 - 64-bit @@ -21,11 +19,26 @@ install: - copy c:\Python36-x64\python.exe c:\Python36-x64\python3.exe - python --version - make install + - copy %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe C:\projects\dexbot\installer\windows\msi\DEXBot.exe after_test: - make package - '7z a DEXBot-gui-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe' + + +image: Visual Studio 2017 + +configuration: +- Release +- Debug +platform: Any CPU +build: + verbosity: normal +# scripts to run before build +before_build: + - cmd: nuget restore + # @TODO: Run tests.. test_script: - "echo tests..." @@ -33,6 +46,13 @@ test_script: artifacts: - path: DEXBot-gui-win64.zip name: DEXBot-gui-win64.zip + - path: C:\projects\dexbot\\windows\bundle\bin\Release\DEXBot-installer.exe + name: DEXBot-installer.exe + - path: C:\projects\dexbot\\windows\bundle\bin\Debug\DEXBot-installer.exe + name: DEXBot-installer-Debug.exe + + + #---------------------------------# # Deployment # From 56f7d3503f85dc23cdf886cbc2a2df3b9441689f Mon Sep 17 00:00:00 2001 From: dominic22 Date: Sat, 1 Jun 2019 18:22:42 +0200 Subject: [PATCH 1511/1846] Remove duplicated key --- appveyor.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 8d033551c..d64524865 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -11,8 +11,6 @@ environment: build: off -configuration: Release - install: - SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%;C:\MinGW\bin - copy c:\MinGW\bin\mingw32-make.exe c:\MinGW\bin\make.exe @@ -38,7 +36,7 @@ build: # scripts to run before build before_build: - cmd: nuget restore - + # @TODO: Run tests.. test_script: - "echo tests..." From 0d67b2b771779b6c23193a20745410a2605919d2 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Sat, 1 Jun 2019 18:23:36 +0200 Subject: [PATCH 1512/1846] Remove duplicated key --- appveyor.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index d64524865..249401bab 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,8 +9,6 @@ environment: # Build # #---------------------------------# -build: off - install: - SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%;C:\MinGW\bin - copy c:\MinGW\bin\mingw32-make.exe c:\MinGW\bin\make.exe @@ -23,8 +21,6 @@ after_test: - make package - '7z a DEXBot-gui-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe' - - image: Visual Studio 2017 configuration: From e274fcb7a916c739d02736cabc2853e9ab04e1ef Mon Sep 17 00:00:00 2001 From: dominic22 Date: Sat, 1 Jun 2019 18:32:31 +0200 Subject: [PATCH 1513/1846] move image to top --- appveyor.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 249401bab..0611b90aa 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,7 @@ version: 0.1.{build} +image: Visual Studio 2017 + environment: matrix: # Python 3.6.6 - 64-bit @@ -21,11 +23,6 @@ after_test: - make package - '7z a DEXBot-gui-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe' -image: Visual Studio 2017 - -configuration: -- Release -- Debug platform: Any CPU build: verbosity: normal From d24d61a9c399d03251f367a06b599bb5c496663d Mon Sep 17 00:00:00 2001 From: dominic22 Date: Sat, 1 Jun 2019 18:36:14 +0200 Subject: [PATCH 1514/1846] remove platform --- appveyor.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 0611b90aa..3d769cbb3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,6 @@ version: 0.1.{build} -image: Visual Studio 2017 +image: Visual Studio 2015 environment: matrix: @@ -23,7 +23,6 @@ after_test: - make package - '7z a DEXBot-gui-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe' -platform: Any CPU build: verbosity: normal # scripts to run before build From a6fa6e067c16a342649cdb9dfb02a85061b75cc6 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Sat, 1 Jun 2019 18:39:18 +0200 Subject: [PATCH 1515/1846] adjust path --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 3d769cbb3..e6f76a2a4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,6 @@ version: 0.1.{build} -image: Visual Studio 2015 +image: Visual Studio 2017 environment: matrix: @@ -17,7 +17,7 @@ install: - copy c:\Python36-x64\python.exe c:\Python36-x64\python3.exe - python --version - make install - - copy %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe C:\projects\dexbot\installer\windows\msi\DEXBot.exe + - copy C:\Python36-x64\Scripts\DEXBot-gui.exe C:\projects\dexbot\installer\windows\msi\DEXBot.exe after_test: - make package From fe97c8cc537913ac2c675e4753cf859784b4c026 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Sat, 1 Jun 2019 18:42:11 +0200 Subject: [PATCH 1516/1846] change to vs 2015 --- appveyor.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index e6f76a2a4..cee4c648f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,6 @@ version: 0.1.{build} -image: Visual Studio 2017 +image: Visual Studio 2015 environment: matrix: @@ -23,12 +23,20 @@ after_test: - make package - '7z a DEXBot-gui-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe' +# wix + +configuration: +- Release +- Debug +platform: Any CPU build: verbosity: normal # scripts to run before build before_build: - cmd: nuget restore +# wix + # @TODO: Run tests.. test_script: - "echo tests..." From 76c40300962dbfc51471d67f4c6790a67211b09e Mon Sep 17 00:00:00 2001 From: dominic22 Date: Sat, 1 Jun 2019 21:51:45 +0200 Subject: [PATCH 1517/1846] remove nuget restore --- appveyor.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index cee4c648f..30a77e378 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -31,9 +31,6 @@ configuration: platform: Any CPU build: verbosity: normal -# scripts to run before build -before_build: - - cmd: nuget restore # wix From b4c4fcf5beb02307fe2c131ddf69a9f8dfb4110d Mon Sep 17 00:00:00 2001 From: dominic22 Date: Sat, 1 Jun 2019 21:57:01 +0200 Subject: [PATCH 1518/1846] Change platform and add project dependency --- appveyor.yml | 2 +- installer/windows/dexbot_installer.sln | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 30a77e378..004e183f5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -28,7 +28,7 @@ after_test: configuration: - Release - Debug -platform: Any CPU +platform: x86 build: verbosity: normal diff --git a/installer/windows/dexbot_installer.sln b/installer/windows/dexbot_installer.sln index 9e8ae93eb..b5f4d45e2 100644 --- a/installer/windows/dexbot_installer.sln +++ b/installer/windows/dexbot_installer.sln @@ -6,6 +6,9 @@ MinimumVisualStudioVersion = 10.0.40219.1 Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "msi", "msi\msi.wixproj", "{95B3AE56-37CC-4CDF-813C-6566698D5709}" EndProject Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "bundle", "bundle\bundle.wixproj", "{0A6FA202-7443-4F18-A462-F77C543BF921}" + ProjectSection(ProjectDependencies) = postProject + {95B3AE56-37CC-4CDF-813C-6566698D5709} = {95B3AE56-37CC-4CDF-813C-6566698D5709} + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution From 0c9652429decf86d0c35731e6d351879c3760b25 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Sat, 1 Jun 2019 22:21:15 +0200 Subject: [PATCH 1519/1846] Add preprocessor var for build config, --- appveyor.yml | 5 +++-- installer/windows/bundle/bundle.wixproj | 6 +++++- installer/windows/bundle/bundle.wxs | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 004e183f5..b35ca84c3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -38,12 +38,13 @@ build: test_script: - "echo tests..." +# artifact as relativ path to the build folder C:\projects\dexbot artifacts: - path: DEXBot-gui-win64.zip name: DEXBot-gui-win64.zip - - path: C:\projects\dexbot\\windows\bundle\bin\Release\DEXBot-installer.exe + - path: installer\windows\bundle\bin\Release\DEXBot-installer.exe name: DEXBot-installer.exe - - path: C:\projects\dexbot\\windows\bundle\bin\Debug\DEXBot-installer.exe + - path: installer\windows\bundle\bin\Debug\DEXBot-installer.exe name: DEXBot-installer-Debug.exe diff --git a/installer/windows/bundle/bundle.wixproj b/installer/windows/bundle/bundle.wixproj index 5f68b11b3..8187c7287 100644 --- a/installer/windows/bundle/bundle.wixproj +++ b/installer/windows/bundle/bundle.wixproj @@ -13,11 +13,15 @@ bin\$(Configuration)\ obj\$(Configuration)\ - Debug + Build=$(Configuration) + + bin\$(Configuration)\ obj\$(Configuration)\ + Build=$(Configuration) + diff --git a/installer/windows/bundle/bundle.wxs b/installer/windows/bundle/bundle.wxs index 7b528eb6d..c7a2938f6 100644 --- a/installer/windows/bundle/bundle.wxs +++ b/installer/windows/bundle/bundle.wxs @@ -36,7 +36,7 @@ DisplayName="Processing Python..." InstallCommand="/q Include_pip=1 PrependPath=1 Include_launcher=1"> - + From f75721baa6892e7e6d6c73ed508bd429a96c4667 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Sat, 1 Jun 2019 22:52:45 +0200 Subject: [PATCH 1520/1846] Change artifact path --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index b35ca84c3..a049dd6d1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -42,9 +42,9 @@ test_script: artifacts: - path: DEXBot-gui-win64.zip name: DEXBot-gui-win64.zip - - path: installer\windows\bundle\bin\Release\DEXBot-installer.exe + - path: installer\windows\bundle\bin\Release name: DEXBot-installer.exe - - path: installer\windows\bundle\bin\Debug\DEXBot-installer.exe + - path: installer\windows\bundle\bin\Debug name: DEXBot-installer-Debug.exe From e737c5cf5c8928f3df2e174278d097a47f2d1cef Mon Sep 17 00:00:00 2001 From: dominic22 Date: Sun, 2 Jun 2019 10:59:57 +0200 Subject: [PATCH 1521/1846] change copy script --- appveyor.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index a049dd6d1..c054e4afa 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,6 +3,8 @@ version: 0.1.{build} image: Visual Studio 2015 environment: + # change artifact name + # artifactName: $(APPVEYOR_BUILD_VERSION)-$(APPVEYOR_REPO_COMMIT).zip matrix: # Python 3.6.6 - 64-bit - PYTHON: "C:\\Python36-x64" @@ -17,7 +19,7 @@ install: - copy c:\Python36-x64\python.exe c:\Python36-x64\python3.exe - python --version - make install - - copy C:\Python36-x64\Scripts\DEXBot-gui.exe C:\projects\dexbot\installer\windows\msi\DEXBot.exe + - copy %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe C:\projects\dexbot\installer\windows\msi\DEXBot.exe after_test: - make package @@ -42,10 +44,12 @@ test_script: artifacts: - path: DEXBot-gui-win64.zip name: DEXBot-gui-win64.zip - - path: installer\windows\bundle\bin\Release + - path: DEXBot-installer.exe name: DEXBot-installer.exe - - path: installer\windows\bundle\bin\Debug - name: DEXBot-installer-Debug.exe + + # see https://help.appveyor.com/discussions/questions/1494-change-artifact-name + #https://help.appveyor.com/discussions/questions/4249-renaming-artifact + #%artifactName% From dc3e20e7fa6f9df947dac3490fabddc0059f2952 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Sun, 2 Jun 2019 11:07:48 +0200 Subject: [PATCH 1522/1846] always copy script executable --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index c054e4afa..3f2a0c9b6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,7 +19,7 @@ install: - copy c:\Python36-x64\python.exe c:\Python36-x64\python3.exe - python --version - make install - - copy %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe C:\projects\dexbot\installer\windows\msi\DEXBot.exe + - copy /Y c:\Python36-x64\DEXBot-gui.exe C:\projects\dexbot\installer\windows\msi\DEXBot.exe after_test: - make package From 0ca2e2c77a70d5900619d297dddeb1508940d225 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Sun, 2 Jun 2019 11:18:09 +0200 Subject: [PATCH 1523/1846] revert executable location --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 3f2a0c9b6..18cfe2962 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,7 +19,7 @@ install: - copy c:\Python36-x64\python.exe c:\Python36-x64\python3.exe - python --version - make install - - copy /Y c:\Python36-x64\DEXBot-gui.exe C:\projects\dexbot\installer\windows\msi\DEXBot.exe + - copy /Y C:\Python36-x64\Scripts\DEXBot-gui.exe C:\projects\dexbot\installer\windows\msi\DEXBot.exe after_test: - make package From 1347cfea40cd3bc1b45bb69311af0a11d98f8471 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Sun, 2 Jun 2019 17:47:10 +0200 Subject: [PATCH 1524/1846] change artifact path --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 18cfe2962..43b00d483 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -44,7 +44,7 @@ test_script: artifacts: - path: DEXBot-gui-win64.zip name: DEXBot-gui-win64.zip - - path: DEXBot-installer.exe + - path: installer\windows\bundle\bin\%CONFIGURATION% name: DEXBot-installer.exe # see https://help.appveyor.com/discussions/questions/1494-change-artifact-name From 5882e260e14ba0620dd0e19f3dfd8f7a6d305276 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Sun, 2 Jun 2019 18:00:23 +0200 Subject: [PATCH 1525/1846] set version by env var --- appveyor.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 43b00d483..f0bb07e01 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,6 +20,7 @@ install: - python --version - make install - copy /Y C:\Python36-x64\Scripts\DEXBot-gui.exe C:\projects\dexbot\installer\windows\msi\DEXBot.exe + - SET ApplicationVersion=$(APPVEYOR_BUILD_VERSION) after_test: - make package @@ -44,7 +45,7 @@ test_script: artifacts: - path: DEXBot-gui-win64.zip name: DEXBot-gui-win64.zip - - path: installer\windows\bundle\bin\%CONFIGURATION% + - path: installer\windows\bundle\bin\%CONFIGURATION%\DEXBot-installer.exe name: DEXBot-installer.exe # see https://help.appveyor.com/discussions/questions/1494-change-artifact-name @@ -76,8 +77,8 @@ deploy: # Notifications # #---------------------------------# -notifications: - - provider: Slack - auth_token: - secure: G9OMj9l2s3+lX8cRiNXXhuQJpnnjcBc0cqP8gzkdKVWqGA8vBTOIPGxD/536VKpeBH/5dJFQWT+vmnGS+XciaCg4hh5s6hDpnvePq2+uEYE= - channel: '#ci' +# notifications: +# - provider: Slack +# auth_token: +# secure: G9OMj9l2s3+lX8cRiNXXhuQJpnnjcBc0cqP8gzkdKVWqGA8vBTOIPGxD/536VKpeBH/5dJFQWT+vmnGS+XciaCg4hh5s6hDpnvePq2+uEYE= +# channel: '#ci' From f3470c59303b4d50e7e23923b7b66ed54aea6840 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Sun, 2 Jun 2019 18:11:55 +0200 Subject: [PATCH 1526/1846] change build order --- appveyor.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index f0bb07e01..54e16ce0f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,12 +19,10 @@ install: - copy c:\Python36-x64\python.exe c:\Python36-x64\python3.exe - python --version - make install - - copy /Y C:\Python36-x64\Scripts\DEXBot-gui.exe C:\projects\dexbot\installer\windows\msi\DEXBot.exe - - SET ApplicationVersion=$(APPVEYOR_BUILD_VERSION) - -after_test: - make package - '7z a DEXBot-gui-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe' + - copy /Y C:\Python36-x64\Scripts\DEXBot-gui.exe C:\projects\dexbot\installer\windows\msi\DEXBot.exe + - SET ApplicationVersion=$(APPVEYOR_BUILD_VERSION) # wix From ec51c749cdaf1f9078fd623c3cdf6b2c5de04d61 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Tue, 4 Jun 2019 19:43:25 +0200 Subject: [PATCH 1527/1846] output dir --- appveyor.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index 54e16ce0f..388d449ee 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -21,6 +21,10 @@ install: - make install - make package - '7z a DEXBot-gui-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe' + - dir + - echo -############################################################################- + - cd C:\Python36-x64\Scripts\DEXBot-gui.exe | dir + - echo -############################################################################- - copy /Y C:\Python36-x64\Scripts\DEXBot-gui.exe C:\projects\dexbot\installer\windows\msi\DEXBot.exe - SET ApplicationVersion=$(APPVEYOR_BUILD_VERSION) From a639b8c73c4b864837da8b92b7022ea902216ecb Mon Sep 17 00:00:00 2001 From: dominic22 Date: Tue, 4 Jun 2019 19:55:38 +0200 Subject: [PATCH 1528/1846] further output --- appveyor.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 388d449ee..4f322c2c3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,12 +20,14 @@ install: - python --version - make install - make package - - '7z a DEXBot-gui-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe' - dir - echo -############################################################################- - - cd C:\Python36-x64\Scripts\DEXBot-gui.exe | dir + - cd C:\Python36-x64\Scripts | dir + - echo -############################################################################- + - cd %APPVEYOR_BUILD_FOLDER%\dist | dir - echo -############################################################################- - copy /Y C:\Python36-x64\Scripts\DEXBot-gui.exe C:\projects\dexbot\installer\windows\msi\DEXBot.exe + - '7z a DEXBot-gui-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe' - SET ApplicationVersion=$(APPVEYOR_BUILD_VERSION) # wix From 650a0a22f8564af6857e260d19ef661751a49559 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Tue, 4 Jun 2019 20:03:30 +0200 Subject: [PATCH 1529/1846] fix output commands --- appveyor.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 4f322c2c3..fa03ee199 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,11 +20,11 @@ install: - python --version - make install - make package - - dir + - dir .\dist - echo -############################################################################- - - cd C:\Python36-x64\Scripts | dir + - dir C:\Python36-x64\Scripts - echo -############################################################################- - - cd %APPVEYOR_BUILD_FOLDER%\dist | dir + - dir %APPVEYOR_BUILD_FOLDER%\dist - echo -############################################################################- - copy /Y C:\Python36-x64\Scripts\DEXBot-gui.exe C:\projects\dexbot\installer\windows\msi\DEXBot.exe - '7z a DEXBot-gui-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe' From bac5c80d2dbdf57784976ee81adfc35a234e0321 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Tue, 4 Jun 2019 20:09:36 +0200 Subject: [PATCH 1530/1846] copy correct file --- appveyor.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index fa03ee199..889cfa4d7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -26,8 +26,12 @@ install: - echo -############################################################################- - dir %APPVEYOR_BUILD_FOLDER%\dist - echo -############################################################################- - - copy /Y C:\Python36-x64\Scripts\DEXBot-gui.exe C:\projects\dexbot\installer\windows\msi\DEXBot.exe + - copy /Y C:\projects\dexbot\dist\DEXBot-gui.exe C:\projects\dexbot\installer\windows\msi\DEXBot.exe - '7z a DEXBot-gui-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe' + - echo -############################################################################- + - dir %APPVEYOR_BUILD_FOLDER%\dist + - echo $(APPVEYOR_BUILD_VERSION) + - echo %APPVEYOR_BUILD_VERSION% - SET ApplicationVersion=$(APPVEYOR_BUILD_VERSION) # wix From bc2ddd2efefdf481f1969f0e03f0b430806f1bba Mon Sep 17 00:00:00 2001 From: dominic22 Date: Tue, 4 Jun 2019 20:20:31 +0200 Subject: [PATCH 1531/1846] echo version --- appveyor.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index 889cfa4d7..c25f389fe 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -32,6 +32,8 @@ install: - dir %APPVEYOR_BUILD_FOLDER%\dist - echo $(APPVEYOR_BUILD_VERSION) - echo %APPVEYOR_BUILD_VERSION% + - echo %VERSION% + - echo %__version__% - SET ApplicationVersion=$(APPVEYOR_BUILD_VERSION) # wix From f76c4096af47fdb0911c012f7a0d9dc0af72b5c1 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Tue, 4 Jun 2019 22:08:29 +0200 Subject: [PATCH 1532/1846] read env vars by __init__.py, add version constant to project file --- appveyor.yml | 15 ++++----------- dexbot/__init__.py | 8 ++++---- installer/windows/bundle/bundle.wixproj | 2 ++ 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index c25f389fe..01d0c611b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,21 +20,14 @@ install: - python --version - make install - make package - - dir .\dist - - echo -############################################################################- - - dir C:\Python36-x64\Scripts - - echo -############################################################################- - - dir %APPVEYOR_BUILD_FOLDER%\dist - - echo -############################################################################- - copy /Y C:\projects\dexbot\dist\DEXBot-gui.exe C:\projects\dexbot\installer\windows\msi\DEXBot.exe - '7z a DEXBot-gui-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe' + # read dexbot version from __init__.py + - for /f "delims== tokens=1,2" %G in (test.txt) do set %G=%Hfor /f "delims== tokens=1,2" %G in (__init__.py) do set %G=%H - echo -############################################################################- - - dir %APPVEYOR_BUILD_FOLDER%\dist - - echo $(APPVEYOR_BUILD_VERSION) - - echo %APPVEYOR_BUILD_VERSION% - echo %VERSION% - - echo %__version__% - - SET ApplicationVersion=$(APPVEYOR_BUILD_VERSION) + - SET ApplicationVersion=%VERSION% + - echo %ApplicationVersion% # wix diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 847b259f4..cf5cd3bd5 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ -APP_NAME = 'dexbot' -VERSION = '0.10.12' -AUTHOR = 'Codaone Oy' -__version__ = VERSION +APP_NAME='dexbot' +VERSION='0.10.12' +AUTHOR='Codaone Oy' +__version__=VERSION diff --git a/installer/windows/bundle/bundle.wixproj b/installer/windows/bundle/bundle.wixproj index 8187c7287..9893ae1da 100644 --- a/installer/windows/bundle/bundle.wixproj +++ b/installer/windows/bundle/bundle.wixproj @@ -14,6 +14,7 @@ bin\$(Configuration)\ obj\$(Configuration)\ Build=$(Configuration) + Version=$(VERSION) @@ -21,6 +22,7 @@ bin\$(Configuration)\ obj\$(Configuration)\ Build=$(Configuration) + Version=$(VERSION) From 2b72e1510c33feeaa24e02e049db45dc4c71f9ff Mon Sep 17 00:00:00 2001 From: dominic22 Date: Tue, 4 Jun 2019 22:13:36 +0200 Subject: [PATCH 1533/1846] fix command line --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 01d0c611b..3744be7be 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -23,7 +23,7 @@ install: - copy /Y C:\projects\dexbot\dist\DEXBot-gui.exe C:\projects\dexbot\installer\windows\msi\DEXBot.exe - '7z a DEXBot-gui-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe' # read dexbot version from __init__.py - - for /f "delims== tokens=1,2" %G in (test.txt) do set %G=%Hfor /f "delims== tokens=1,2" %G in (__init__.py) do set %G=%H + - for /f "delims== tokens=1,2" %G in (C:\projects\dexbot\dexbot\__init__.py) do set %G=%H - echo -############################################################################- - echo %VERSION% - SET ApplicationVersion=%VERSION% From 94575292a7ba7ce34b1910ee6458b9748ef5f40f Mon Sep 17 00:00:00 2001 From: dominic22 Date: Tue, 4 Jun 2019 22:20:05 +0200 Subject: [PATCH 1534/1846] update path to file --- appveyor.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 3744be7be..ae4e88ffd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -23,7 +23,10 @@ install: - copy /Y C:\projects\dexbot\dist\DEXBot-gui.exe C:\projects\dexbot\installer\windows\msi\DEXBot.exe - '7z a DEXBot-gui-win64.zip %APPVEYOR_BUILD_FOLDER%\dist\DEXBot-gui.exe' # read dexbot version from __init__.py - - for /f "delims== tokens=1,2" %G in (C:\projects\dexbot\dexbot\__init__.py) do set %G=%H + - dir + - echo ############ + - dir .\dexbot + - for /f "delims== tokens=1,2" %G in (__init__.py) do set %G=%H - echo -############################################################################- - echo %VERSION% - SET ApplicationVersion=%VERSION% From f7b295a165a886d9e173ccf0cd597fc6d0270dbb Mon Sep 17 00:00:00 2001 From: dominic22 Date: Tue, 4 Jun 2019 22:23:53 +0200 Subject: [PATCH 1535/1846] update path #2 --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index ae4e88ffd..573d7439c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -26,7 +26,7 @@ install: - dir - echo ############ - dir .\dexbot - - for /f "delims== tokens=1,2" %G in (__init__.py) do set %G=%H + - for /f "delims== tokens=1,2" %G in (.\dexbot\__init__.py) do set %G=%H - echo -############################################################################- - echo %VERSION% - SET ApplicationVersion=%VERSION% From bf6e10363fdff3e7aa6854177ab1d27b972cfd7d Mon Sep 17 00:00:00 2001 From: dominic22 Date: Tue, 4 Jun 2019 22:28:20 +0200 Subject: [PATCH 1536/1846] use double % --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 573d7439c..d34e0e572 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -26,7 +26,7 @@ install: - dir - echo ############ - dir .\dexbot - - for /f "delims== tokens=1,2" %G in (.\dexbot\__init__.py) do set %G=%H + - for /f "delims== tokens=1,2" %%G in (.\dexbot\__init__.py) do set %%G=%%H - echo -############################################################################- - echo %VERSION% - SET ApplicationVersion=%VERSION% From 22513bb2ac88e5b8913729a922c29c943a213e62 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Tue, 4 Jun 2019 22:36:04 +0200 Subject: [PATCH 1537/1846] Change project preprocessor vars --- installer/windows/bundle/bundle.wixproj | 6 ++---- installer/windows/msi/msi.wixproj | 5 +++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/installer/windows/bundle/bundle.wixproj b/installer/windows/bundle/bundle.wixproj index 9893ae1da..1068dad4d 100644 --- a/installer/windows/bundle/bundle.wixproj +++ b/installer/windows/bundle/bundle.wixproj @@ -13,16 +13,14 @@ bin\$(Configuration)\ obj\$(Configuration)\ - Build=$(Configuration) - Version=$(VERSION) + ApplicationVersion=$(VERSION);Build=$(Configuration) bin\$(Configuration)\ obj\$(Configuration)\ - Build=$(Configuration) - Version=$(VERSION) + ApplicationVersion=$(VERSION);Build=$(Configuration) diff --git a/installer/windows/msi/msi.wixproj b/installer/windows/msi/msi.wixproj index ce9ed82d4..56c7bdc33 100644 --- a/installer/windows/msi/msi.wixproj +++ b/installer/windows/msi/msi.wixproj @@ -13,11 +13,12 @@ bin\$(Configuration)\ obj\$(Configuration)\ - Debug + Debug;ApplicationVersion=$(VERSION); bin\$(Configuration)\ obj\$(Configuration)\ + ApplicationVersion=$(VERSION); @@ -25,7 +26,7 @@ - + From 1dc7a9dfa81e83ce936f6dc5dde6b0b5a5de2e48 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Tue, 4 Jun 2019 22:46:21 +0200 Subject: [PATCH 1538/1846] add before build to set vars --- appveyor.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index d34e0e572..fa7809c0c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -33,6 +33,8 @@ install: - echo %ApplicationVersion% # wix +before_build: + - for /f "delims== tokens=1,2" %%G in (.\dexbot\__init__.py) do set %%G=%%H configuration: - Release From 70ea2f3447ca86a461550222e4cb5c8a5b1bfca8 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Wed, 5 Jun 2019 18:16:30 +0200 Subject: [PATCH 1539/1846] add comment in init.py --- appveyor.yml | 8 +++++--- dexbot/__init__.py | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index fa7809c0c..616ec834b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,6 +3,7 @@ version: 0.1.{build} image: Visual Studio 2015 environment: + - for /f "delims== eol=# tokens=1,2" %%G in (.\dexbot\__init__.py) do set %%G=%%H # change artifact name # artifactName: $(APPVEYOR_BUILD_VERSION)-$(APPVEYOR_REPO_COMMIT).zip matrix: @@ -26,7 +27,7 @@ install: - dir - echo ############ - dir .\dexbot - - for /f "delims== tokens=1,2" %%G in (.\dexbot\__init__.py) do set %%G=%%H + - for /f "delims== eol=# tokens=1,2" %%G in (.\dexbot\__init__.py) do set %%G=%%H - echo -############################################################################- - echo %VERSION% - SET ApplicationVersion=%VERSION% @@ -34,11 +35,12 @@ install: # wix before_build: - - for /f "delims== tokens=1,2" %%G in (.\dexbot\__init__.py) do set %%G=%%H + - for /f "delims== eol=# tokens=1,2" %%G in (.\dexbot\__init__.py) do set %%G=%%H + - echo -############################################################################- + - echo %VERSION% configuration: - Release -- Debug platform: x86 build: verbosity: normal diff --git a/dexbot/__init__.py b/dexbot/__init__.py index cf5cd3bd5..890ca7be7 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,3 +1,4 @@ +# please don't add spaces between the variable and the value, othwerwise appveyor won't be able to read the dexbot version APP_NAME='dexbot' VERSION='0.10.12' AUTHOR='Codaone Oy' From adcbdaf046f798ff3a23c3ccafd3fb0a3d372f7f Mon Sep 17 00:00:00 2001 From: dominic22 Date: Wed, 5 Jun 2019 19:09:52 +0200 Subject: [PATCH 1540/1846] try to set applicationversion by env var --- appveyor.yml | 3 ++- installer/windows/bundle/bundle.wixproj | 20 ++++++++++++++----- .../windows/bundle/resources/variables.wxi | 2 +- installer/windows/msi/Product.wxs | 2 +- installer/windows/msi/msi.wixproj | 4 ++-- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 616ec834b..2a651d01f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,9 +3,9 @@ version: 0.1.{build} image: Visual Studio 2015 environment: - - for /f "delims== eol=# tokens=1,2" %%G in (.\dexbot\__init__.py) do set %%G=%%H # change artifact name # artifactName: $(APPVEYOR_BUILD_VERSION)-$(APPVEYOR_REPO_COMMIT).zip + ApplicationVersion: '3.0' matrix: # Python 3.6.6 - 64-bit - PYTHON: "C:\\Python36-x64" @@ -32,6 +32,7 @@ install: - echo %VERSION% - SET ApplicationVersion=%VERSION% - echo %ApplicationVersion% + - echo $(ApplicationVersion) # wix before_build: diff --git a/installer/windows/bundle/bundle.wixproj b/installer/windows/bundle/bundle.wixproj index 1068dad4d..71d62b85f 100644 --- a/installer/windows/bundle/bundle.wixproj +++ b/installer/windows/bundle/bundle.wixproj @@ -6,6 +6,8 @@ 3.10 0a6fa202-7443-4f18-a462-f77c543bf921 2.0 + 0.0.0.0 + DEXBot-installer Bundle bundle @@ -13,15 +15,18 @@ bin\$(Configuration)\ obj\$(Configuration)\ - ApplicationVersion=$(VERSION);Build=$(Configuration) - - + Debug;ApplicationVersion=$(ApplicationVersion);Build=$(Configuration) + ApplicationVersion=$(ApplicationVersion);Build=$(Configuration) + + bin\$(Configuration)\ obj\$(Configuration)\ - ApplicationVersion=$(VERSION);Build=$(Configuration) - + Debug;ApplicationVersions=%ApplicationVersions%;Build=$(Configuration) + ApplicationVersions=%ApplicationVersions%;Build=$(Configuration) + + @@ -56,6 +61,11 @@ + + set VERSION=%271.2%27 +set ApplicationVersion=%271.2%27 +echo %25VERSION%25 + diff --git a/installer/windows/msi/Product.wxs b/installer/windows/msi/Product.wxs index 74dd75eff..984528407 100644 --- a/installer/windows/msi/Product.wxs +++ b/installer/windows/msi/Product.wxs @@ -4,7 +4,7 @@ diff --git a/installer/windows/msi/msi.wixproj b/installer/windows/msi/msi.wixproj index 56c7bdc33..142fd501e 100644 --- a/installer/windows/msi/msi.wixproj +++ b/installer/windows/msi/msi.wixproj @@ -13,12 +13,12 @@ bin\$(Configuration)\ obj\$(Configuration)\ - Debug;ApplicationVersion=$(VERSION); + Debug;ApplicationVersion=%25VERSION%25; bin\$(Configuration)\ obj\$(Configuration)\ - ApplicationVersion=$(VERSION); + Debug;ApplicationVersion=%25VERSION%25; From cc059fdd0f05eeeb69dc0239d15d9f5de0340b5c Mon Sep 17 00:00:00 2001 From: dominic22 Date: Wed, 5 Jun 2019 19:19:00 +0200 Subject: [PATCH 1541/1846] rename applicationversion to version --- appveyor.yml | 7 +++---- installer/windows/bundle/bundle.wixproj | 14 +++++++------- installer/windows/bundle/bundle.wxs | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 2a651d01f..2ce45e42b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,7 +5,7 @@ image: Visual Studio 2015 environment: # change artifact name # artifactName: $(APPVEYOR_BUILD_VERSION)-$(APPVEYOR_REPO_COMMIT).zip - ApplicationVersion: '3.0' + VERSION: '3.0' matrix: # Python 3.6.6 - 64-bit - PYTHON: "C:\\Python36-x64" @@ -30,9 +30,8 @@ install: - for /f "delims== eol=# tokens=1,2" %%G in (.\dexbot\__init__.py) do set %%G=%%H - echo -############################################################################- - echo %VERSION% - - SET ApplicationVersion=%VERSION% - - echo %ApplicationVersion% - - echo $(ApplicationVersion) + - echo %VERSION% + - echo $(VERSION) # wix before_build: diff --git a/installer/windows/bundle/bundle.wixproj b/installer/windows/bundle/bundle.wixproj index 71d62b85f..f51f74578 100644 --- a/installer/windows/bundle/bundle.wixproj +++ b/installer/windows/bundle/bundle.wixproj @@ -6,8 +6,8 @@ 3.10 0a6fa202-7443-4f18-a462-f77c543bf921 2.0 - 0.0.0.0 - + + 0.0.0.0 DEXBot-installer Bundle bundle @@ -15,16 +15,16 @@ bin\$(Configuration)\ obj\$(Configuration)\ - Debug;ApplicationVersion=$(ApplicationVersion);Build=$(Configuration) - ApplicationVersion=$(ApplicationVersion);Build=$(Configuration) + Debug;VERSION=$(VERSION);Build=$(Configuration) + VERSION=$(VERSION);Build=$(Configuration) bin\$(Configuration)\ obj\$(Configuration)\ - Debug;ApplicationVersions=%ApplicationVersions%;Build=$(Configuration) - ApplicationVersions=%ApplicationVersions%;Build=$(Configuration) + Debug;VERSION=$(VERSION);Build=$(Configuration) + VERSION=$(VERSION);Build=$(Configuration) @@ -63,7 +63,7 @@ set VERSION=%271.2%27 -set ApplicationVersion=%271.2%27 +echo asd echo %25VERSION%25 - 0.0.0.0 + DEXBot-installer Bundle bundle @@ -15,16 +15,15 @@ bin\$(Configuration)\ obj\$(Configuration)\ - Debug;VERSION=$(VERSION);Build=$(Configuration) - VERSION=$(VERSION);Build=$(Configuration) + ApplicationVersion=$(VERSION);Build=$(Configuration) + bin\$(Configuration)\ obj\$(Configuration)\ - Debug;VERSION=$(VERSION);Build=$(Configuration) - VERSION=$(VERSION);Build=$(Configuration) + ApplicationVersion=$(VERSION);Build=$(Configuration) diff --git a/installer/windows/bundle/bundle.wxs b/installer/windows/bundle/bundle.wxs index afed2693c..c7a2938f6 100644 --- a/installer/windows/bundle/bundle.wxs +++ b/installer/windows/bundle/bundle.wxs @@ -5,7 +5,7 @@ diff --git a/installer/windows/bundle/resources/variables.wxi b/installer/windows/bundle/resources/variables.wxi index 31e603af9..7a0550248 100644 --- a/installer/windows/bundle/resources/variables.wxi +++ b/installer/windows/bundle/resources/variables.wxi @@ -1,6 +1,8 @@ - + + + From 351743916b7642ca4924e976b995f1012b15f4a1 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Wed, 5 Jun 2019 19:42:27 +0200 Subject: [PATCH 1543/1846] change msi project file --- installer/windows/msi/msi.wixproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/installer/windows/msi/msi.wixproj b/installer/windows/msi/msi.wixproj index 142fd501e..694018eae 100644 --- a/installer/windows/msi/msi.wixproj +++ b/installer/windows/msi/msi.wixproj @@ -13,12 +13,12 @@ bin\$(Configuration)\ obj\$(Configuration)\ - Debug;ApplicationVersion=%25VERSION%25; + ApplicationVersion=$(VERSION);Build=$(Configuration) bin\$(Configuration)\ obj\$(Configuration)\ - Debug;ApplicationVersion=%25VERSION%25; + ApplicationVersion=$(VERSION);Build=$(Configuration) From d133e6e35ce83f023ba25a4a11aff6815c171482 Mon Sep 17 00:00:00 2001 From: dominic22 Date: Wed, 5 Jun 2019 19:50:32 +0200 Subject: [PATCH 1544/1846] replace ' with empty char at version var --- appveyor.yml | 11 ++++++----- installer/windows/bundle/bundle.wixproj | 11 ++++------- installer/windows/bundle/bundle.wxs | 2 +- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index c293f241b..ceaa9bfba 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -30,14 +30,15 @@ install: - for /f "delims== eol=# tokens=1,2" %%G in (.\dexbot\__init__.py) do set %%G=%%H - echo -############################################################################- - echo %VERSION% - - SET ApplicationVersion=%VERSION% + # set to version and replace ' with empty character + - set ApplicationVersion=%VERSION:'=% - echo %ApplicationVersion% # wix -before_build: - - for /f "delims== eol=# tokens=1,2" %%G in (.\dexbot\__init__.py) do set %%G=%%H - - echo -############################################################################- - - echo %VERSION% +#before_build: + # - for /f "delims== eol=# tokens=1,2" %%G in (.\dexbot\__init__.py) do set %%G=%%H + #- echo -############################################################################- + #- echo %VERSION% configuration: - Release diff --git a/installer/windows/bundle/bundle.wixproj b/installer/windows/bundle/bundle.wixproj index b2b7ca33b..c2bd1e8cd 100644 --- a/installer/windows/bundle/bundle.wixproj +++ b/installer/windows/bundle/bundle.wixproj @@ -6,7 +6,7 @@ 3.10 0a6fa202-7443-4f18-a462-f77c543bf921 2.0 - + DEXBot-installer Bundle @@ -15,15 +15,14 @@ bin\$(Configuration)\ obj\$(Configuration)\ - ApplicationVersion=$(VERSION);Build=$(Configuration) - + ApplicationVersion=$(VERSION);Build=$(Configuration) bin\$(Configuration)\ obj\$(Configuration)\ - ApplicationVersion=$(VERSION);Build=$(Configuration) + ApplicationVersion=$(VERSION);Build=$(Configuration) @@ -61,9 +60,7 @@ - set VERSION=%271.2%27 -echo asd -echo %25VERSION%25 + - \ No newline at end of file + diff --git a/installer/windows/bundle/bundle.wxs b/installer/windows/bundle/bundle.wxs index 218609d98..e47ec408f 100644 --- a/installer/windows/bundle/bundle.wxs +++ b/installer/windows/bundle/bundle.wxs @@ -1,9 +1,9 @@ - + - + - + &Cancel - Setup Progress + Setup Progress Please wait until the setup is completed... Processing: Initializing... @@ -53,4 +53,4 @@ [WixBundleName] Installation Click Install to install [WixBundleName] v[WixBundleVersion] on your computer. By installing you accept these <a href="#">license terms</a> - \ No newline at end of file + diff --git a/installer/windows/bundle/resources/classic_theme.xml b/installer/windows/bundle/resources/classic_theme.xml index 9df3457bf..159a649eb 100644 --- a/installer/windows/bundle/resources/classic_theme.xml +++ b/installer/windows/bundle/resources/classic_theme.xml @@ -56,4 +56,4 @@ - \ No newline at end of file + diff --git a/installer/windows/msi/Product.wxs b/installer/windows/msi/Product.wxs index 74dd75eff..dd2deb9fb 100644 --- a/installer/windows/msi/Product.wxs +++ b/installer/windows/msi/Product.wxs @@ -20,7 +20,7 @@ - + @@ -43,7 +43,7 @@ - + @@ -52,7 +52,7 @@ - + @@ -86,4 +86,4 @@ - \ No newline at end of file + diff --git a/installer/windows/msi/README.txt b/installer/windows/msi/README.txt index 342bd9cce..5d2e8ee3f 100644 --- a/installer/windows/msi/README.txt +++ b/installer/windows/msi/README.txt @@ -15,4 +15,4 @@ Website: https://dexbot.info Telegram: -https://t.me/DEXBOTbts \ No newline at end of file +https://t.me/DEXBOTbts diff --git a/installer/windows/msi/msi.wixproj b/installer/windows/msi/msi.wixproj index ee290c915..03312094e 100644 --- a/installer/windows/msi/msi.wixproj +++ b/installer/windows/msi/msi.wixproj @@ -51,4 +51,4 @@ --> - \ No newline at end of file + diff --git a/pyuic.json b/pyuic.json index 266116fda..54aacc68b 100644 --- a/pyuic.json +++ b/pyuic.json @@ -10,4 +10,4 @@ "pyrcc_options": "", "pyuic": "pyuic5", "pyuic_options": "--import-from=dexbot.resources" -} \ No newline at end of file +} diff --git a/setup.py b/setup.py index da61cad6c..9aab3a9c2 100755 --- a/setup.py +++ b/setup.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 -from dexbot import VERSION, APP_NAME - -from setuptools import setup, find_packages from distutils.command import build as build_module +from setuptools import find_packages, setup + +from dexbot import APP_NAME, VERSION + cmd_class = {} console_scripts = ['dexbot-cli = dexbot.cli:main'] install_requires = [] @@ -17,10 +18,8 @@ def run(self): try: from pyqt_distutils.build_ui import build_ui - cmd_class = { - 'build_ui': build_ui, - 'build': BuildCommand - } + + cmd_class = {'build_ui': build_ui, 'build': BuildCommand} console_scripts.append('dexbot-gui = dexbot.gui:main') install_requires.extend(["pyqt-distutils"]) except BaseException as e: @@ -47,9 +46,7 @@ def run(self): 'Intended Audience :: Developers', ], cmdclass=cmd_class, - entry_points={ - 'console_scripts': console_scripts - }, + entry_points={'console_scripts': console_scripts}, install_requires=install_requires, include_package_data=True, ) diff --git a/tests/conftest.py b/tests/conftest.py index 204c21c0c..3161c5895 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,18 +1,17 @@ -import uuid -import docker import os.path -import pytest -import socket import random +import socket import time +import uuid +import docker +import pytest from bitshares import BitShares -from bitshares.instance import set_shared_bitshares_instance -from bitshares.genesisbalance import GenesisBalance from bitshares.account import Account from bitshares.asset import Asset -from bitshares.exceptions import AssetDoesNotExistsException, AccountDoesNotExistsException - +from bitshares.exceptions import AccountDoesNotExistsException, AssetDoesNotExistsException +from bitshares.genesisbalance import GenesisBalance +from bitshares.instance import set_shared_bitshares_instance from bitsharesbase.account import PublicKey from bitsharesbase.chains import known_chains diff --git a/tests/gecko_test.py b/tests/gecko_test.py index 45a3a0d27..6ce64f441 100644 --- a/tests/gecko_test.py +++ b/tests/gecko_test.py @@ -1,12 +1,17 @@ import click -from dexbot.styles import yellow from dexbot.strategies.external_feeds.gecko_feed import get_gecko_price from dexbot.strategies.external_feeds.process_pair import split_pair +from dexbot.styles import yellow def print_usage(): - print("Usage: python3 gecko_feed.py", yellow('[symbol]'), - "Symbol is required, for example:", yellow('BTC/USD'), sep='') + print( + "Usage: python3 gecko_feed.py", + yellow('[symbol]'), + "Symbol is required, for example:", + yellow('BTC/USD'), + sep='', + ) # Unit tests diff --git a/tests/migrations/conftest.py b/tests/migrations/conftest.py index ce46044b6..2bc5108e1 100644 --- a/tests/migrations/conftest.py +++ b/tests/migrations/conftest.py @@ -1,14 +1,13 @@ +import logging import os -import pytest import tempfile -import logging -from sqlalchemy import create_engine, Column, String, Integer, Float +import pytest +from dexbot.storage import DatabaseWorker +from sqlalchemy import Column, Float, Integer, String, create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -from dexbot.storage import DatabaseWorker - log = logging.getLogger("dexbot") log.setLevel(logging.DEBUG) diff --git a/tests/migrations/test_migrations.py b/tests/migrations/test_migrations.py index 2fbae51cb..e12520c20 100644 --- a/tests/migrations/test_migrations.py +++ b/tests/migrations/test_migrations.py @@ -1,5 +1,4 @@ import pytest - from dexbot.storage import DatabaseWorker diff --git a/tests/process_pair_test.py b/tests/process_pair_test.py index ae8f09897..7427d9d8e 100644 --- a/tests/process_pair_test.py +++ b/tests/process_pair_test.py @@ -1,5 +1,10 @@ -from dexbot.strategies.external_feeds.process_pair import split_pair, get_consolidated_pair, filter_prefix_symbol, \ - filter_bit_symbol +from dexbot.strategies.external_feeds.process_pair import ( + filter_bit_symbol, + filter_prefix_symbol, + get_consolidated_pair, + split_pair, +) + """ This is the unit test for filters in process_pair module. @@ -24,9 +29,20 @@ def test_split_symbol(): def test_filters(): - test_symbols = ['USDT', 'bridge.USD', 'Rudex.USD', 'open.USD', - 'GDEX.USD', 'Spark.USD', 'bridge.BTC', 'BTC', 'LTC', - 'bitUSD', 'bitEUR', 'bitHKD'] + test_symbols = [ + 'USDT', + 'bridge.USD', + 'Rudex.USD', + 'open.USD', + 'GDEX.USD', + 'Spark.USD', + 'bridge.BTC', + 'BTC', + 'LTC', + 'bitUSD', + 'bitEUR', + 'bitHKD', + ] print("Test Symbols", test_symbols, sep=":") r = [filter_prefix_symbol(i) for i in test_symbols] print("Filter prefix symbol", r, sep=":") diff --git a/tests/storage/conftest.py b/tests/storage/conftest.py index a790b8270..458643da7 100644 --- a/tests/storage/conftest.py +++ b/tests/storage/conftest.py @@ -1,6 +1,6 @@ -import pytest import logging +import pytest from dexbot.storage import Storage log = logging.getLogger("dexbot") diff --git a/tests/storage/test_storage.py b/tests/storage/test_storage.py index 30003202a..3eacb3777 100644 --- a/tests/storage/test_storage.py +++ b/tests/storage/test_storage.py @@ -1,4 +1,5 @@ import logging + import pytest log = logging.getLogger("dexbot") diff --git a/tests/strategies/king_of_the_hill/conftest.py b/tests/strategies/king_of_the_hill/conftest.py index 694feab08..8519f3c82 100644 --- a/tests/strategies/king_of_the_hill/conftest.py +++ b/tests/strategies/king_of_the_hill/conftest.py @@ -1,10 +1,10 @@ -import pytest -import time +import copy import logging +import time -from dexbot.strategies.king_of_the_hill import Strategy +import pytest from dexbot.strategies.base import StrategyBase -import copy +from dexbot.strategies.king_of_the_hill import Strategy log = logging.getLogger("dexbot") diff --git a/tests/strategies/king_of_the_hill/test_king_of_the_hill.py b/tests/strategies/king_of_the_hill/test_king_of_the_hill.py index 9994e10d0..a25e9133d 100644 --- a/tests/strategies/king_of_the_hill/test_king_of_the_hill.py +++ b/tests/strategies/king_of_the_hill/test_king_of_the_hill.py @@ -1,6 +1,6 @@ import logging -import pytest +import pytest log = logging.getLogger("dexbot") log.setLevel(logging.DEBUG) diff --git a/tests/strategies/relative_orders/conftest.py b/tests/strategies/relative_orders/conftest.py index 6a481d34a..b500a476b 100644 --- a/tests/strategies/relative_orders/conftest.py +++ b/tests/strategies/relative_orders/conftest.py @@ -1,8 +1,9 @@ -import pytest -import time import copy import logging import random +import time + +import pytest from dexbot.strategies.base import StrategyBase from dexbot.strategies.relative_orders import Strategy diff --git a/tests/strategies/relative_orders/test_relative_orders.py b/tests/strategies/relative_orders/test_relative_orders.py index ba5576ae0..ad63f21b0 100644 --- a/tests/strategies/relative_orders/test_relative_orders.py +++ b/tests/strategies/relative_orders/test_relative_orders.py @@ -1,8 +1,8 @@ -import math -import pytest import logging +import math import time +import pytest from bitshares.market import Market # Turn on debug for dexbot logger diff --git a/tests/strategies/staggered_orders/conftest.py b/tests/strategies/staggered_orders/conftest.py index 90396f1fd..469a21c53 100644 --- a/tests/strategies/staggered_orders/conftest.py +++ b/tests/strategies/staggered_orders/conftest.py @@ -1,12 +1,11 @@ -import pytest import copy -import time -import tempfile -import os import logging +import os +import tempfile +import time +import pytest from bitshares.amount import Amount - from dexbot.strategies.staggered_orders import Strategy log = logging.getLogger("dexbot") diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index 1c0b8c3d4..a669f7ef3 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -1,8 +1,8 @@ import logging -import pytest import math - from datetime import datetime + +import pytest from bitshares.account import Account from bitshares.amount import Amount diff --git a/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py b/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py index 5fa407e88..970b9d12f 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py @@ -1,7 +1,7 @@ import logging import math -import pytest +import pytest from dexbot.strategies.staggered_orders import VirtualOrder # Turn on debug for dexbot logger diff --git a/tests/strategies/staggered_orders/test_staggered_orders_init.py b/tests/strategies/staggered_orders/test_staggered_orders_init.py index 01679c301..397211a03 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_init.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_init.py @@ -1,7 +1,7 @@ import copy import logging -import pytest +import pytest from dexbot.strategies.staggered_orders import Strategy # Turn on debug for dexbot logger diff --git a/tests/strategies/staggered_orders/test_staggered_orders_mwsa.py b/tests/strategies/staggered_orders/test_staggered_orders_mwsa.py index 9f4df474f..20f36f768 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_mwsa.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_mwsa.py @@ -1,4 +1,5 @@ import logging + import pytest # Turn on debug for dexbot logger diff --git a/tests/styles_test.py b/tests/styles_test.py index c3b7f5b7e..53e7a8bc6 100644 --- a/tests/styles_test.py +++ b/tests/styles_test.py @@ -1,5 +1,4 @@ -from dexbot.styles import green, blue, yellow, red, pink, bold, underline - +from dexbot.styles import blue, bold, green, pink, red, underline, yellow if __name__ == '__main__': # Test each text style diff --git a/tests/test_measure_latency.py b/tests/test_measure_latency.py index 1912c6e6c..3e86e74da 100644 --- a/tests/test_measure_latency.py +++ b/tests/test_measure_latency.py @@ -1,5 +1,4 @@ import pytest - from dexbot.controllers.main_controller import MainController from grapheneapi.exceptions import NumRetriesReached diff --git a/tests/test_prepared_testnet.py b/tests/test_prepared_testnet.py index 51912f0f5..8fbf632c4 100644 --- a/tests/test_prepared_testnet.py +++ b/tests/test_prepared_testnet.py @@ -1,5 +1,4 @@ import pytest - from bitshares.account import Account from bitshares.asset import Asset diff --git a/tests/test_worker_infrastructure.py b/tests/test_worker_infrastructure.py index afced72f8..314d0a286 100644 --- a/tests/test_worker_infrastructure.py +++ b/tests/test_worker_infrastructure.py @@ -1,8 +1,8 @@ -import threading import logging +import threading import time -import pytest +import pytest from dexbot.worker import WorkerInfrastructure logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s') diff --git a/tests/waves_test.py b/tests/waves_test.py index 8410d862d..317eb6df0 100644 --- a/tests/waves_test.py +++ b/tests/waves_test.py @@ -1,4 +1,4 @@ -from dexbot.strategies.external_feeds.process_pair import split_pair, filter_prefix_symbol, filter_bit_symbol +from dexbot.strategies.external_feeds.process_pair import filter_bit_symbol, filter_prefix_symbol, split_pair from dexbot.strategies.external_feeds.waves_feed import get_waves_price """ This is the unit test for getting external feed data from waves DEX. From 26200a6c3abe9826ce98524d664e39e13d6f7a8e Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 13 Mar 2020 00:45:55 +0500 Subject: [PATCH 1731/1846] Make timeout to be optional Closes: #686 --- dexbot/node_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/node_manager.py b/dexbot/node_manager.py index 6bbdc05b0..929ab5623 100644 --- a/dexbot/node_manager.py +++ b/dexbot/node_manager.py @@ -65,7 +65,7 @@ def check_node(node, timeout): return node_info -def get_sorted_nodelist(nodelist, timeout): +def get_sorted_nodelist(nodelist, timeout=2): """ Check all nodes and poll for latency, eliminate nodes with no response, then sort nodes by increasing latency and return as a list """ From de00f7d449e6b59b6e4ec197e274d2987006ab8e Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 6 Jan 2020 17:38:54 +0500 Subject: [PATCH 1732/1846] Adjust docstring to fix rendering --- dexbot/config.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/dexbot/config.py b/dexbot/config.py index fa54b8920..f44247db9 100644 --- a/dexbot/config.py +++ b/dexbot/config.py @@ -182,11 +182,14 @@ def assets_intersections(config): example, 3 workers with 30% 30% 30%, and 2 workers with 0. These 2 workers will take the remaining `(100 - 3*30) / 2 = 5`. - Example return as a dict: - {'foo': {'RUBLE': {'sum_pct': 0, 'zero_workers': 0}, - 'USD': {'sum_pct': 0, 'zero_workers': 0}, - 'CNY': {'sum_pct': 0, 'zero_workers': 0} - } + Example return as a dict + + .. code-block:: python + + {'foo': {'RUBLE': {'sum_pct': 0, 'zero_workers': 0}, + 'USD': {'sum_pct': 0, 'zero_workers': 0}, + 'CNY': {'sum_pct': 0, 'zero_workers': 0} + } } """ From fbcb69ecf05bac0f02e96ac2a7de46badeda3fe0 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 6 Jan 2020 17:39:32 +0500 Subject: [PATCH 1733/1846] Remove reference to non-existent docs --- docs/configuration.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 7ac764f65..9cdf267be 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -15,11 +15,6 @@ The configuration consists of a series of questions about the bots you wish to c how they behave (i.e. spend *your* money) so it is important you understand the strategy before deploying a bot. - a. :doc:`echo` For testing this just logs events on a market, does no trading. - b. :doc:`follow_orders` My (Ian Haywood) main bot, an extension of stakemachine's `wall`, - it has been used to provide liquidity on AUD:BTS. - Does function but by no mean perfect, see caveats in the docs. - 3. Strategy-specific questions The questions that follow are determined by the strategy chosen, and each strategy will have its own questions around From 7221b03043876c0b265c509cf051480ca1e159f9 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 6 Jan 2020 18:00:40 +0500 Subject: [PATCH 1734/1846] Remove statemachine mentiones from docs --- docs/index.rst | 1 - docs/statemachine.rst | 17 ----------------- 2 files changed, 18 deletions(-) delete mode 100644 docs/statemachine.rst diff --git a/docs/index.rst b/docs/index.rst index 45c285a94..285247e1c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,7 +24,6 @@ Developing own Strategies :maxdepth: 1 storage - statemachine strategybase events diff --git a/docs/statemachine.rst b/docs/statemachine.rst deleted file mode 100644 index c5e09f591..000000000 --- a/docs/statemachine.rst +++ /dev/null @@ -1,17 +0,0 @@ -************ -Statemachine -************ - -The base strategy comes with a state machine that can be used by your -strategy. - -Similar to :doc:`storage`, the methods of this class can be used in your -strategy directly, e.g., via ``self.get_state()``, since the class is -inherited by :doc:`strategybase`. - -API ---- - -.. autoclass:: dexbot.statemachine.StateMachine - :members: - :noindex: From 1711d2302a34da291bb75458d99e71cc8ed050d8 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 27 Jan 2020 23:56:42 +0500 Subject: [PATCH 1735/1846] Fix price inversion for virtual sell orders --- 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 fd9864074..f88861e70 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -2152,7 +2152,7 @@ def place_virtual_sell_order(self, amount, price): self.log.info( 'Placing a virtual sell order with {:.{prec}f} {} @ {:.8f}'.format( - amount, symbol, order['price'], prec=self.market['quote']['precision'] + amount, symbol, order['price'] ** -1, prec=self.market['quote']['precision'] ) ) self.virtual_orders.append(order) From 1c9ea7131d1c5e2c6c2253026191ef6ac638e58d Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 27 Jan 2020 23:57:28 +0500 Subject: [PATCH 1736/1846] Add workaround for test_maintain_strategy_one_sided --- .../staggered_orders/test_staggered_orders_complex.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index a669f7ef3..be354bfe9 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -93,7 +93,6 @@ def test_maintain_strategy_basic(mode, worker, do_initial_allocation): assert worker.sell_orders[-1]['price'] ** -1 > worker.upper_bound / (1 + worker.increment * 2) -@pytest.mark.xfail(reason='https://github.com/Codaone/DEXBot/issues/575') @pytest.mark.parametrize('mode', MODES) def test_maintain_strategy_one_sided(mode, base_worker, config_only_base, do_initial_allocation): """ Test for one-sided start (buy only) @@ -102,7 +101,7 @@ def test_maintain_strategy_one_sided(mode, base_worker, config_only_base, do_ini do_initial_allocation(worker, mode) # Check target spread is reached - assert worker.actual_spread < worker.target_spread + worker.increment + assert worker.actual_spread == pytest.approx(worker.target_spread + worker.increment, abs=(worker.increment / 2)) # Check number of orders price = worker.center_price / math.sqrt(1 + worker.target_spread) From a2a7e1849fd889d2570c12b6463f1402b718aa56 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 14 Mar 2020 17:32:29 +0500 Subject: [PATCH 1737/1846] Update bitshares libraries Closes: #692 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index b08bafdb4..8e2b6521d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,8 +8,8 @@ requests==2.21.0 yarl==1.1.0 ccxt==1.17.434 pywaves==0.8.20 -git+https://github.com/Codaone/python-graphenelib.git@ba3702a25498f56a8ade8b855b812e7ec00311e2#egg=graphenelib -git+https://github.com/Codaone/python-bitshares.git@0c9bf5ef3808572b7a10dfc32615906083e6b8ff#egg=bitshares +graphenelib==1.2.0 +bitshares==0.4.0 uptick==0.2.4 ruamel.yaml>=0.15.37 appdirs>=1.4.3 From 58cc48d04c01aaec5f007890c235fcbaa07a1598 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 28 Jan 2020 15:49:47 +0500 Subject: [PATCH 1738/1846] Add switch to disable fallback logic in SO --- .../config_parts/staggered_config.py | 8 +++++ dexbot/strategies/staggered_orders.py | 2 ++ .../test_staggered_orders_complex.py | 36 +++++++++++++++++-- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/config_parts/staggered_config.py b/dexbot/strategies/config_parts/staggered_config.py index 409d2d133..041f18200 100644 --- a/dexbot/strategies/config_parts/staggered_config.py +++ b/dexbot/strategies/config_parts/staggered_config.py @@ -95,6 +95,14 @@ def configure(cls, return_base_config=True): 'Order depth to maintain on books', (2, 9999999, None), ), + ConfigElement( + 'enable_fallback_logic', + 'bool', + True, + 'Enable fallback logic', + 'When unable to close the spread, cancel lowest buy order and place closer buy order', + None, + ), ] @classmethod diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index fd9864074..74dbca71c 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -49,6 +49,7 @@ def __init__(self, *args, **kwargs): self.is_instant_fill_enabled = self.worker.get('instant_fill', True) self.is_center_price_dynamic = self.worker['center_price_dynamic'] self.operational_depth = self.worker.get('operational_depth', 6) + self.enable_fallback_logic = self.worker.get('enable_fallback_logic', True) if self.is_center_price_dynamic: self.center_price = None @@ -258,6 +259,7 @@ def maintain_strategy(self, *args, **kwargs): or self.base_balance_history[0] != self.base_balance_history[2] or self.quote_balance_history[0] != self.quote_balance_history[2] or trx_executed + or not self.enable_fallback_logic ): self.last_check = datetime.now() self.log_maintenance_time() diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index a669f7ef3..d3d496e0b 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -148,12 +148,11 @@ def test_maintain_strategy_1sat(base_worker, config_1_sat, do_initial_allocation # Combine each mode with base and quote @pytest.mark.parametrize('asset', ['base', 'quote']) -@pytest.mark.parametrize('mode', MODES) -def test_maintain_strategy_fallback_logic(asset, mode, worker, do_initial_allocation): +def test_maintain_strategy_fallback_logic(asset, worker, do_initial_allocation): """ Check fallback logic: when spread is not reached, furthest order should be cancelled to make free funds to close spread """ - do_initial_allocation(worker, mode) + do_initial_allocation(worker, worker.mode) # TODO: strategy must turn off bootstrapping once target spread is reached worker['bootstrapping'] = False @@ -178,6 +177,37 @@ def test_maintain_strategy_fallback_logic(asset, mode, worker, do_initial_alloca assert spread_after <= worker.target_spread + worker.increment +@pytest.mark.parametrize('asset', ['base', 'quote']) +def test_maintain_strategy_fallback_logic_disabled(asset, worker, do_initial_allocation): + """ Check fallback logic: when spread is not reached, furthest order should be cancelled to make free funds to + close spread + """ + worker.enable_fallback_logic = False + do_initial_allocation(worker, worker.mode) + # TODO: strategy must turn off bootstrapping once target spread is reached + worker['bootstrapping'] = False + + if asset == 'base': + worker.cancel_orders_wrapper(worker.buy_orders[0]) + amount = worker.balance(worker.market['base']['symbol']) + worker.bitshares.reserve(amount, account=worker.account) + elif asset == 'quote': + worker.cancel_orders_wrapper(worker.sell_orders[0]) + amount = worker.balance(worker.market['quote']['symbol']) + worker.bitshares.reserve(amount, account=worker.account) + + worker.refresh_orders() + spread_before = get_spread(worker) + assert spread_before > worker.target_spread + worker.increment + + for _ in range(0, 6): + worker.maintain_strategy() + + worker.refresh_orders() + spread_after = get_spread(worker) + assert spread_after == spread_before + + def test_check_operational_depth(worker, do_initial_allocation): """ Test for correct operational depth following """ From 6d57aa5122701c041028adb9e9cf05e3262bc603 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 3 Feb 2020 20:39:34 +0500 Subject: [PATCH 1739/1846] Fix operational depth when fallback logic is off --- dexbot/strategies/staggered_orders.py | 8 ++++---- .../test_staggered_orders_complex.py | 18 ++++++++++++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 74dbca71c..85c9f772b 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -177,10 +177,6 @@ def maintain_strategy(self, *args, **kwargs): self.log_maintenance_time() return - # Ensure proper operational depth - self.check_operational_depth(self.real_buy_orders, self.virtual_buy_orders) - self.check_operational_depth(self.real_sell_orders, self.virtual_sell_orders) - # Remember current boostrapping state before sending transactions previous_bootstrap_state = self['bootstrapping'] @@ -227,6 +223,10 @@ def maintain_strategy(self, *args, **kwargs): del self.base_balance_history[0] del self.quote_balance_history[0] + # Ensure proper operational depth + self.check_operational_depth(self.real_buy_orders, self.virtual_buy_orders) + self.check_operational_depth(self.real_sell_orders, self.virtual_sell_orders) + # Greatly increase check interval to lower CPU load whether there is no funds to allocate or we cannot # allocate funds for some reason if ( diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index d3d496e0b..685ba71bb 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -183,17 +183,18 @@ def test_maintain_strategy_fallback_logic_disabled(asset, worker, do_initial_all close spread """ worker.enable_fallback_logic = False - do_initial_allocation(worker, worker.mode) + worker.operational_depth = 2 + do_initial_allocation(worker, 'valley') # TODO: strategy must turn off bootstrapping once target spread is reached worker['bootstrapping'] = False if asset == 'base': - worker.cancel_orders_wrapper(worker.buy_orders[0]) - amount = worker.balance(worker.market['base']['symbol']) + worker.cancel_orders_wrapper(worker.buy_orders[:3]) + amount = worker.buy_orders[0]['base'] * 3 worker.bitshares.reserve(amount, account=worker.account) elif asset == 'quote': - worker.cancel_orders_wrapper(worker.sell_orders[0]) - amount = worker.balance(worker.market['quote']['symbol']) + worker.cancel_orders_wrapper(worker.sell_orders[:3]) + amount = worker.sell_orders[0]['base'] * 3 worker.bitshares.reserve(amount, account=worker.account) worker.refresh_orders() @@ -204,9 +205,14 @@ def test_maintain_strategy_fallback_logic_disabled(asset, worker, do_initial_all worker.maintain_strategy() worker.refresh_orders() - spread_after = get_spread(worker) + spread_after = worker.get_actual_spread() + # Spread didn't changed assert spread_after == spread_before + # Also check that operational depth is proper + assert len(worker.real_buy_orders) == pytest.approx(worker.operational_depth, abs=1) + assert len(worker.real_sell_orders) == pytest.approx(worker.operational_depth, abs=1) + def test_check_operational_depth(worker, do_initial_allocation): """ Test for correct operational depth following From 31908d01e77badd3f39da570f29cc7eba1177f0b Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 28 Jan 2020 17:10:49 +0500 Subject: [PATCH 1740/1846] Close spread before replacing virtual orders with real --- dexbot/strategies/staggered_orders.py | 74 +++++++++-------- .../test_staggered_orders_complex.py | 81 ++++++++----------- 2 files changed, 74 insertions(+), 81 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 85c9f772b..13c9ea1ab 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -79,7 +79,6 @@ def __init__(self, *args, **kwargs): self.virtual_buy_orders = [] self.virtual_sell_orders = [] self.virtual_orders_restored = False - self.actual_spread = self.target_spread + 1 self.quote_total_balance = 0 self.base_total_balance = 0 self.quote_balance = None @@ -177,6 +176,11 @@ def maintain_strategy(self, *args, **kwargs): self.log_maintenance_time() return + # Ensure proper operational depth + if self.get_actual_spread() < self.target_spread + self.increment: + self.check_operational_depth(self.real_buy_orders, self.virtual_buy_orders) + self.check_operational_depth(self.real_sell_orders, self.virtual_sell_orders) + # Remember current boostrapping state before sending transactions previous_bootstrap_state = self['bootstrapping'] @@ -268,32 +272,19 @@ def maintain_strategy(self, *args, **kwargs): # There are no funds and current orders aren't close enough, try to fix the situation by shifting orders. # This is a fallback logic. - # Get highest buy and lowest sell prices from orders - highest_buy_price = 0 - lowest_sell_price = 0 - - if self.buy_orders: - highest_buy_price = self.buy_orders[0].get('price') - - if self.sell_orders: - lowest_sell_price = self.sell_orders[0].get('price') - # Invert the sell price to BASE so it can be used in comparison - lowest_sell_price = lowest_sell_price ** -1 - - if highest_buy_price and lowest_sell_price: - self.actual_spread = (lowest_sell_price / highest_buy_price) - 1 - if self.actual_spread < self.target_spread + self.increment: - # Target spread is reached, no need to cancel anything - self.last_check = datetime.now() - self.log_maintenance_time() - return - elif self.buy_orders: - # If target spread is not reached and no balance to allocate, cancel lowest buy order - self.log.info( - 'Free balances are not changing, bootstrap is off and target spread is not reached. ' - 'Cancelling lowest buy order as a fallback' - ) - self.cancel_orders_wrapper(self.buy_orders[-1]) + actual_spread = self.get_actual_spread() + if actual_spread < self.target_spread + self.increment: + # Target spread is reached, no need to cancel anything + self.last_check = datetime.now() + self.log_maintenance_time() + return + elif self.buy_orders: + # If target spread is not reached and no balance to allocate, cancel lowest buy order + self.log.info( + 'Free balances are not changing, bootstrap is off and target spread is not reached. ' + 'Cancelling lowest buy order as a fallback' + ) + self.cancel_orders_wrapper(self.buy_orders[-1]) self.last_check = datetime.now() self.log_maintenance_time() @@ -302,6 +293,23 @@ def maintain_strategy(self, *args, **kwargs): if self.view: self.update_gui_profit() + def get_actual_spread(self, buy_price=None, sell_price=None): + """ Calculates current spread on own orders using cached orders + """ + if buy_price and sell_price: + highest_buy_price = buy_price + lowest_sell_price = sell_price + else: + try: + highest_buy_price = self.buy_orders[0].get('price') + lowest_sell_price = self.sell_orders[0].get('price') + # Invert the sell price to BASE so it can be used in comparison + lowest_sell_price = lowest_sell_price ** -1 + except IndexError: + return float('Inf') + spread = (lowest_sell_price / highest_buy_price) - 1 + return spread + def log_maintenance_time(self): """ Measure time from self.start and print a log message """ @@ -780,9 +788,9 @@ def allocate_asset(self, asset, asset_balance): closest_opposite_price = (self.market_center_price / (1 + self.target_spread / 2)) ** -1 closest_own_price = closest_own_order['price'] - self.actual_spread = (closest_opposite_price / closest_own_price) - 1 + actual_spread = self.get_actual_spread(buy_price=closest_own_price, sell_price=closest_opposite_price) - if self.actual_spread >= self.target_spread + self.increment: + if actual_spread >= self.target_spread + self.increment: if not self.check_partial_fill(closest_own_order, fill_threshold=0): # Replace closest order if it was partially filled for any % """ Note on partial filled orders handling: if target spread is not reached and we need to place @@ -807,7 +815,7 @@ def allocate_asset(self, asset, asset_balance): self['bootstrapping'] = False """ Note: because we're using operations batching, there is possible a situation when we will have - both free balances and `self.actual_spread >= self.target_spread + self.increment`. In such case + both free balances and `actual_spread >= self.target_spread + self.increment`. In such case there will be TWO orders placed, one buy and one sell despite only one would be enough to reach target spread. Sure, we can add a workaround for that by overriding `closest_opposite_price` for second call of allocate_asset(). We are not doing this because we're not doing assumption on @@ -818,12 +826,12 @@ def allocate_asset(self, asset, asset_balance): # Place order closer to the center price self.log.debug( 'Placing closer {} order; actual spread: {:.4%}, target + increment: {:.4%}'.format( - order_type, self.actual_spread, self.target_spread + self.increment + order_type, actual_spread, self.target_spread + self.increment ) ) if self['bootstrapping']: self.place_closer_order(asset, closest_own_order) - elif opposite_orders and self.actual_spread - self.increment < self.target_spread + self.increment: + elif opposite_orders and actual_spread - self.increment < self.target_spread + self.increment: """ Place max-sized closer order if only one order needed to reach target spread (avoid unneeded increases) """ @@ -881,7 +889,7 @@ def allocate_asset(self, asset, asset_balance): self.place_closer_order(asset, closest_own_order, allow_partial=True) # Store balance data whether new actual spread will match target spread - if self.actual_spread + self.increment >= self.target_spread and not self.bitshares.txbuffer.is_empty(): + if actual_spread + self.increment >= self.target_spread and not self.bitshares.txbuffer.is_empty(): # Transactions are not yet sent, so balance refresh is not needed self.store_profit_estimation_data(force=True) elif not opposite_orders: diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index 685ba71bb..d34bbe045 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -13,26 +13,6 @@ MODES = ['mountain', 'valley', 'neutral', 'buy_slope', 'sell_slope'] -def get_spread(worker): - """ Get actual spread on SO worker - - :param Strategy worker: an active worker instance - """ - if worker.buy_orders: - highest_buy_price = worker.buy_orders[0].get('price') - else: - return float('Inf') - - if worker.sell_orders: - lowest_sell_price = worker.sell_orders[0].get('price') - # Invert the sell price to BASE so it can be used in comparison - lowest_sell_price = lowest_sell_price ** -1 - else: - return float('Inf') - - return (lowest_sell_price / highest_buy_price) - 1 - - ################### # Most complex methods which depends on high-level methods ################### @@ -71,7 +51,7 @@ def test_maintain_strategy_basic(mode, worker, do_initial_allocation): worker = do_initial_allocation(worker, mode) # Check target spread is reached - assert worker.actual_spread < worker.target_spread + worker.increment + assert worker.get_actual_spread() < worker.target_spread + worker.increment # Check number of orders price = worker.center_price * math.sqrt(1 + worker.target_spread) @@ -101,9 +81,6 @@ def test_maintain_strategy_one_sided(mode, base_worker, config_only_base, do_ini worker = base_worker(config_only_base) do_initial_allocation(worker, mode) - # Check target spread is reached - assert worker.actual_spread < worker.target_spread + worker.increment - # Check number of orders price = worker.center_price / math.sqrt(1 + worker.target_spread) buy_orders_count = worker.calc_buy_orders_count(price, worker.lower_bound) @@ -124,7 +101,7 @@ def test_maintain_strategy_1sat(base_worker, config_1_sat, do_initial_allocation do_initial_allocation(worker, worker.mode) # Check target spread is reached - assert worker.actual_spread < worker.target_spread + worker.increment + assert worker.get_actual_spread() < worker.target_spread + worker.increment # Check number of orders price = worker.center_price * math.sqrt(1 + worker.target_spread) @@ -166,14 +143,14 @@ def test_maintain_strategy_fallback_logic(asset, worker, do_initial_allocation): worker.bitshares.reserve(amount, account=worker.account) worker.refresh_orders() - spread_before = get_spread(worker) + spread_before = worker.get_actual_spread() assert spread_before > worker.target_spread + worker.increment for _ in range(0, 6): worker.maintain_strategy() worker.refresh_orders() - spread_after = get_spread(worker) + spread_after = worker.get_actual_spread() assert spread_after <= worker.target_spread + worker.increment @@ -198,7 +175,7 @@ def test_maintain_strategy_fallback_logic_disabled(asset, worker, do_initial_all worker.bitshares.reserve(amount, account=worker.account) worker.refresh_orders() - spread_before = get_spread(worker) + spread_before = worker.get_actual_spread() assert spread_before > worker.target_spread + worker.increment for _ in range(0, 6): @@ -206,7 +183,6 @@ def test_maintain_strategy_fallback_logic_disabled(asset, worker, do_initial_all worker.refresh_orders() spread_after = worker.get_actual_spread() - # Spread didn't changed assert spread_after == spread_before # Also check that operational depth is proper @@ -879,19 +855,19 @@ def test_allocate_asset_basic(worker): worker.calculate_asset_thresholds() worker.refresh_balances() - spread_after = get_spread(worker) + spread_after = worker.get_actual_spread() # Allocate asset until target spread will be reached while spread_after >= worker.target_spread + worker.increment: free_base = worker.base_balance free_quote = worker.quote_balance - spread_before = get_spread(worker) + spread_before = worker.get_actual_spread() worker.allocate_asset('base', free_base) worker.allocate_asset('quote', free_quote) worker.refresh_orders() worker.refresh_balances(use_cached_orders=True) - spread_after = get_spread(worker) + spread_after = worker.get_actual_spread() # Update whistory of balance changes worker.base_balance_history.append(worker.base_balance['amount']) @@ -1095,7 +1071,7 @@ def test_allocate_asset_filled_order_on_massively_imbalanced_sides(worker, do_in Test for https://github.com/Codaone/DEXBot/issues/588 """ do_initial_allocation(worker, worker.mode) - spread_before = get_spread(worker) + spread_before = worker.get_actual_spread() log.info('Worker spread after bootstrap: {}'.format(spread_before)) # TODO: automatically turn off bootstrapping after target spread is closed? worker['bootstrapping'] = False @@ -1108,15 +1084,15 @@ def test_allocate_asset_filled_order_on_massively_imbalanced_sides(worker, do_in # Place limited orders; the goal is to limit order amount to be much smaller than opposite quote_limit = worker.buy_orders[0]['quote']['amount'] * worker.partial_fill_threshold / 2 - spread_after = get_spread(worker) + spread_after = worker.get_actual_spread() while spread_after >= worker.target_spread + worker.increment: # We're using spread check because we cannot just place same number of orders as num_orders_to_cancel because # it may result in too close spread because of price shifts worker.place_closer_order('quote', worker.sell_orders[0], own_asset_limit=quote_limit) worker.refresh_orders() - spread_after = get_spread(worker) + spread_after = worker.get_actual_spread() - log.info('Worker spread: {}'.format(get_spread(worker))) + log.info('Worker spread: {}'.format(worker.get_actual_spread())) # Fill only one newly placed order from another account additional_account = base_account() @@ -1135,7 +1111,7 @@ def test_allocate_asset_filled_order_on_massively_imbalanced_sides(worker, do_in worker.refresh_balances(use_cached_orders=True) # Filling of one order should result in spread > target spread, othewise allocate_asset will not place closer prder - spread_after = get_spread(worker) + spread_after = worker.get_actual_spread() assert spread_after >= worker.target_spread + worker.increment # Allocate obtained BASE @@ -1144,7 +1120,7 @@ def test_allocate_asset_filled_order_on_massively_imbalanced_sides(worker, do_in worker.allocate_asset('base', worker.base_balance) worker.refresh_orders() worker.refresh_balances(use_cached_orders=True) - spread_after = get_spread(worker) + spread_after = worker.get_actual_spread() counter += 1 # Counter is for preventing infinity loop assert counter < 20 @@ -1163,7 +1139,7 @@ def test_allocate_asset_partially_filled_order_on_massively_imbalanced_sides( Test for https://github.com/Codaone/DEXBot/issues/588 """ do_initial_allocation(worker, worker.mode) - spread_before = get_spread(worker) + spread_before = worker.get_actual_spread() log.info('Worker spread after bootstrap: {}'.format(spread_before)) # TODO: automatically turn off bootstrapping after target spread is closed? worker['bootstrapping'] = False @@ -1176,15 +1152,15 @@ def test_allocate_asset_partially_filled_order_on_massively_imbalanced_sides( # Place limited orders; the goal is to limit order amount to be much smaller than opposite quote_limit = worker.buy_orders[0]['quote']['amount'] * worker.partial_fill_threshold / 2 - spread_after = get_spread(worker) + spread_after = worker.get_actual_spread() while spread_after >= worker.target_spread + worker.increment: # We're using spread check because we cannot just place same number of orders as num_orders_to_cancel because # it may result in too close spread because of price shifts worker.place_closer_order('quote', worker.sell_orders[0], own_asset_limit=quote_limit) worker.refresh_orders() - spread_after = get_spread(worker) + spread_after = worker.get_actual_spread() - log.info('Worker spread: {}'.format(get_spread(worker))) + log.info('Worker spread: {}'.format(worker.get_actual_spread())) # Fill only one newly placed order from another account additional_account = base_account() @@ -1207,10 +1183,10 @@ def test_allocate_asset_partially_filled_order_on_massively_imbalanced_sides( assert not worker.check_partial_fill(worker.sell_orders[0], fill_threshold=(1 - worker.partial_fill_threshold)) # Expect dust order cancel + closer order - log.info('spread before allocate_asset(): {}'.format(get_spread(worker))) + log.info('spread before allocate_asset(): {}'.format(worker.get_actual_spread())) worker.allocate_asset('base', worker.base_balance) worker.refresh_orders() - spread_after = get_spread(worker) + spread_after = worker.get_actual_spread() assert spread_after < worker.target_spread + worker.increment @@ -1240,14 +1216,14 @@ def test_allocate_asset_limiting_on_sell_side(mode, worker, do_initial_allocatio # Allocate asset until target spread will be reached worker.refresh_orders() worker.refresh_balances(use_cached_orders=True) - spread_after = get_spread(worker) + spread_after = worker.get_actual_spread() counter = 0 while spread_after >= worker.target_spread + worker.increment: worker.allocate_asset('base', worker.base_balance) worker.allocate_asset('quote', worker.quote_balance) worker.refresh_orders() worker.refresh_balances(use_cached_orders=True) - spread_after = get_spread(worker) + spread_after = worker.get_actual_spread() counter += 1 # Counter is for preventing infinity loop assert counter < 20 @@ -1295,14 +1271,14 @@ def test_allocate_asset_limiting_on_buy_side(mode, worker, do_initial_allocation # Allocate asset until target spread will be reached worker.refresh_orders() worker.refresh_balances(use_cached_orders=True) - spread_after = get_spread(worker) + spread_after = worker.get_actual_spread() counter = 0 while spread_after >= worker.target_spread + worker.increment: worker.allocate_asset('base', worker.base_balance) worker.allocate_asset('quote', worker.quote_balance) worker.refresh_orders() worker.refresh_balances(use_cached_orders=True) - spread_after = get_spread(worker) + spread_after = worker.get_actual_spread() counter += 1 # Counter is for preventing infinity loop assert counter < 20 @@ -1321,6 +1297,15 @@ def test_allocate_asset_limiting_on_buy_side(mode, worker, do_initial_allocation ) +def test_get_actual_spread(worker): + worker.maintain_strategy() + # Twice run needed + worker.maintain_strategy() + worker.refresh_orders() + spread = worker.get_actual_spread() + assert float('Inf') > spread > 0 + + def test_tick(worker): """ Check tick counter increment """ From 00c963681c4a7f1a9802b41dd151bb326c27708d Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 28 Jan 2020 17:38:33 +0500 Subject: [PATCH 1741/1846] Add workaround to fix some tests Problem described here https://github.com/Codaone/DEXBot/issues/575 Workaround is to use approx value for spread comparison accepting a slight error. --- .../staggered_orders/test_staggered_orders_complex.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index d34bbe045..60eaeb6f9 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -43,7 +43,6 @@ def test_maintain_strategy_no_manual_cp_empty_market(worker): assert worker.market_center_price is None -@pytest.mark.xfail(reason='https://github.com/Codaone/DEXBot/issues/575') @pytest.mark.parametrize('mode', MODES) def test_maintain_strategy_basic(mode, worker, do_initial_allocation): """ Check if intial orders placement is correct @@ -51,7 +50,7 @@ def test_maintain_strategy_basic(mode, worker, do_initial_allocation): worker = do_initial_allocation(worker, mode) # Check target spread is reached - assert worker.get_actual_spread() < worker.target_spread + worker.increment + assert worker.get_actual_spread() == pytest.approx(worker.target_spread, abs=(worker.increment / 2)) # Check number of orders price = worker.center_price * math.sqrt(1 + worker.target_spread) @@ -95,13 +94,12 @@ def test_maintain_strategy_one_sided(mode, base_worker, config_only_base, do_ini assert worker.buy_orders[-1]['price'] < worker.lower_bound * (1 + worker.increment * 2) -@pytest.mark.xfail(reason='https://github.com/Codaone/DEXBot/issues/575') def test_maintain_strategy_1sat(base_worker, config_1_sat, do_initial_allocation): worker = base_worker(config_1_sat) do_initial_allocation(worker, worker.mode) # Check target spread is reached - assert worker.get_actual_spread() < worker.target_spread + worker.increment + assert worker.get_actual_spread() == pytest.approx(worker.target_spread, abs=(worker.increment / 2)) # Check number of orders price = worker.center_price * math.sqrt(1 + worker.target_spread) From ab9608e9d20becd22987577f44967d5e27439c78 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 28 Jan 2020 17:44:35 +0500 Subject: [PATCH 1742/1846] Remove calculation of asset thresholds Set asset threshold when increasing orders only. Threshold now is a min amount required to increase an order. There is a balance check in place_closer_xxx_order(), so https://github.com/Codaone/DEXBot/issues/554 should not appear. Closes: #554 --- dexbot/strategies/staggered_orders.py | 24 ++++--------------- .../test_staggered_orders_complex.py | 1 - .../test_staggered_orders_unittests.py | 10 -------- 3 files changed, 4 insertions(+), 31 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 13c9ea1ab..46161b77b 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -156,10 +156,6 @@ def maintain_strategy(self, *args, **kwargs): # Store balance entry for profit estimation if needed self.store_profit_estimation_data() - # Calculate asset thresholds once - if not self.quote_asset_threshold or not self.base_asset_threshold: - self.calculate_asset_thresholds() - # Remove orders that exceed boundaries success = self.remove_outside_orders(self.sell_orders, self.buy_orders) if not success: @@ -322,22 +318,6 @@ def calculate_min_amounts(self): self.order_min_base = 2 * 10 ** -self.market['base']['precision'] / self.increment self.order_min_quote = 2 * 10 ** -self.market['quote']['precision'] / self.increment - def calculate_asset_thresholds(self): - """ Calculate minimal asset thresholds to allocate. - - The goal is to avoid trying to allocate too small amounts which may lead to "Trying to buy/sell 0" - situations. - """ - # Keep at least N of precision - reserve_ratio = 10 - - if self.market['quote']['precision'] <= self.market['base']['precision']: - self.quote_asset_threshold = reserve_ratio * 10 ** -self.market['quote']['precision'] - self.base_asset_threshold = self.quote_asset_threshold * self.market_center_price - else: - self.base_asset_threshold = reserve_ratio * 10 ** -self.market['base']['precision'] - self.quote_asset_threshold = self.base_asset_threshold / self.market_center_price - def refresh_balances(self, use_cached_orders=False): """ This function is used to refresh account balances @@ -1049,6 +1029,10 @@ def _increase_single_order(self, asset, asset_balance, order, new_order_amount): order_type, price, asset_balance['amount'], needed_balance, symbol, prec=precision ) ) + if asset == 'quote': + self.quote_asset_threshold = needed_balance + elif asset == 'base': + self.base_asset_threshold = needed_balance # Increase finished return True diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index 60eaeb6f9..6404866bd 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -851,7 +851,6 @@ def test_allocate_asset_basic(worker): """ Check that free balance is shrinking after each allocation and spread is decreasing """ - worker.calculate_asset_thresholds() worker.refresh_balances() spread_after = worker.get_actual_spread() diff --git a/tests/strategies/staggered_orders/test_staggered_orders_unittests.py b/tests/strategies/staggered_orders/test_staggered_orders_unittests.py index ce2b27f48..40742c67e 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_unittests.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_unittests.py @@ -24,16 +24,6 @@ def test_calculate_min_amounts(worker): assert worker.order_min_quote > 10 ** -worker.market['quote']['precision'] -def test_calculate_asset_thresholds(worker): - """ Check asset threshold - - Todo: https://github.com/Codaone/DEXBot/issues/554 - """ - worker.calculate_asset_thresholds() - assert worker.base_asset_threshold > 0 - assert worker.quote_asset_threshold > 0 - - def test_calc_buy_orders_count(worker): worker.increment = 0.01 assert worker.calc_buy_orders_count(100, 90) == 11 From eb958052d3e2bb4256c8063ddc7247aa6bc4b25c Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 30 Jan 2020 12:09:05 +0500 Subject: [PATCH 1743/1846] Append orderid when reconstructing order data --- dexbot/orderengines/bitshares_engine.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index 133622a02..0a74fac14 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -564,6 +564,7 @@ def place_market_buy_order(self, amount, price, return_none=False, *args, **kwar # 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) + buy_order['id'] = buy_transaction['orderid'] self.recheck_orders = True return buy_order else: @@ -620,6 +621,7 @@ def place_market_sell_order(self, amount, price, return_none=False, invert=False if sell_order and 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['id'] = sell_transaction['orderid'] self.recheck_orders = True if sell_order and invert: sell_order.invert() From b4290ccd29b729a6024eb635707e12b6fa15a942 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 1 Feb 2020 21:45:43 +0500 Subject: [PATCH 1744/1846] Fix proper order reconstruction calculate_order_data() should reconstruct buy/sell orders differently to achive same result as return from the node (without inversion) --- dexbot/orderengines/bitshares_engine.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index 0a74fac14..a1b519a01 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -171,12 +171,22 @@ def balance(self, asset, fee_reservation=0): return balance - def calculate_order_data(self, order, amount, price): + def calculate_order_data(self, order_type, order, amount, price): + """ Reconstructs order data using price and amount + """ quote_asset = Amount(amount, self._market['quote']['symbol'], bitshares_instance=self.bitshares) - order['quote'] = quote_asset - order['price'] = price base_asset = Amount(amount * price, self._market['base']['symbol'], bitshares_instance=self.bitshares) - order['base'] = base_asset + if order_type == 'buy': + order['quote'] = quote_asset + order['price'] = price + order['base'] = base_asset + elif order_type == 'sell': + order['quote'] = base_asset + order['price'] = price ** -1 + order['base'] = quote_asset + else: + raise ValueError('Invalid order_type') + return order def calculate_worker_value(self, unit_of_measure): @@ -563,7 +573,7 @@ def place_market_buy_order(self, amount, price, return_none=False, *args, **kwar if buy_order and 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) + buy_order = self.calculate_order_data('buy', buy_order, amount, price) buy_order['id'] = buy_transaction['orderid'] self.recheck_orders = True return buy_order @@ -620,7 +630,7 @@ def place_market_sell_order(self, amount, price, return_none=False, invert=False sell_order = self.get_order(sell_transaction['orderid'], return_none=return_none) if sell_order and 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 = self.calculate_order_data('sell', sell_order, amount, price) sell_order['id'] = sell_transaction['orderid'] self.recheck_orders = True if sell_order and invert: From 3447bdf8d60228f3c52eba960cefc1c39ca18005 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 29 Jan 2020 16:42:37 +0500 Subject: [PATCH 1745/1846] Implement Stop Loss in SO strategy --- dexbot/controllers/strategy_controller.py | 10 ++++++ .../config_parts/staggered_config.py | 24 ++++++++++++++ dexbot/strategies/staggered_orders.py | 31 ++++++++++++++++++ .../test_staggered_orders_complex.py | 32 +++++++++++++++++++ 4 files changed, 97 insertions(+) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 2bca7c685..9f8dc2714 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -268,9 +268,11 @@ def __init__(self, view, configure, worker_controller, worker_data): # Event connecting widget.center_price_dynamic_input.clicked.connect(self.onchange_center_price_dynamic_input) + widget.enable_stop_loss_input.clicked.connect(self.onchange_enable_stop_loss_input) # Trigger the onchange events once self.onchange_center_price_dynamic_input(widget.center_price_dynamic_input.isChecked()) + self.onchange_enable_stop_loss_input(widget.enable_stop_loss_input.isChecked()) def onchange_center_price_dynamic_input(self, checked): if checked: @@ -278,6 +280,14 @@ def onchange_center_price_dynamic_input(self, checked): else: self.view.strategy_widget.center_price_input.setDisabled(False) + def onchange_enable_stop_loss_input(self, checked): + if checked: + self.view.strategy_widget.stop_loss_discount_input.setDisabled(False) + self.view.strategy_widget.stop_loss_amount_input.setDisabled(False) + else: + self.view.strategy_widget.stop_loss_discount_input.setDisabled(True) + self.view.strategy_widget.stop_loss_amount_input.setDisabled(True) + def set_required_base(self, text): self.view.strategy_widget.required_base_text.setText(text) diff --git a/dexbot/strategies/config_parts/staggered_config.py b/dexbot/strategies/config_parts/staggered_config.py index 041f18200..926a86fd4 100644 --- a/dexbot/strategies/config_parts/staggered_config.py +++ b/dexbot/strategies/config_parts/staggered_config.py @@ -103,6 +103,30 @@ def configure(cls, return_base_config=True): 'When unable to close the spread, cancel lowest buy order and place closer buy order', None, ), + ConfigElement( + 'enable_stop_loss', + 'bool', + False, + 'Enable Stop Loss', + 'Stop Loss order placed when bid price comes near lower bound', + None, + ), + ConfigElement( + 'stop_loss_discount', + 'float', + 5, + 'Stop Loss discount', + 'Discount percent, Stop Loss order price = bid price / (1 + discount percent)', + (0, None, 2, '%'), + ), + ConfigElement( + 'stop_loss_amount', + 'float', + 50, + 'Stop Loss Amount', + 'Relative amount of QUOTE asset to sell at Stop Loss, percentage', + (0, None, 2, '%'), + ), ] @classmethod diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 46161b77b..6213ea569 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -50,6 +50,9 @@ def __init__(self, *args, **kwargs): self.is_center_price_dynamic = self.worker['center_price_dynamic'] self.operational_depth = self.worker.get('operational_depth', 6) self.enable_fallback_logic = self.worker.get('enable_fallback_logic', True) + self.enable_stop_loss = self.worker.get('enable_stop_loss', False) + self.stop_loss_discount = self.worker.get('stop_loss_discount', 5) / 100 + self.stop_loss_amount = self.worker.get('stop_loss_amount', 50) / 100 if self.is_center_price_dynamic: self.center_price = None @@ -253,6 +256,9 @@ def maintain_strategy(self, *args, **kwargs): # Bootstrap was turned off, dump initial orders self.dump_initial_orders() + if self.enable_stop_loss: + self.stop_loss_check() + # Do not continue whether balances are changing or bootstrap is on if ( self['bootstrapping'] @@ -318,6 +324,31 @@ def calculate_min_amounts(self): self.order_min_base = 2 * 10 ** -self.market['base']['precision'] / self.increment self.order_min_quote = 2 * 10 ** -self.market['quote']['precision'] / self.increment + def stop_loss_check(self): + """ Check for Stop Loss condition and execute SL if needed + """ + if self.buy_orders: + return + + highest_bid = float(self.ticker().get('highestBid')) + if not highest_bid < self.lower_bound: + return + + if not highest_bid: + # highest_bid is 0 + highest_bid = self.lower_bound + + stop_loss_price = highest_bid / (1 + self.stop_loss_discount) + amount = self.quote_total_balance * self.stop_loss_amount + self.cancel_all_orders() + self.log.warning( + 'Executing Stop Loss, selling {:.{prec}f} {} @ {:.8f}'.format( + amount, self.market['quote']['symbol'], stop_loss_price, prec=self.market['quote']['precision'] + ) + ) + self.place_market_sell_order(amount, stop_loss_price, returnOrderId=True) + self.error() + def refresh_balances(self, use_cached_orders=False): """ This function is used to refresh account balances diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index 6404866bd..83f94890d 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -1,5 +1,6 @@ import logging import math +import time from datetime import datetime import pytest @@ -1303,6 +1304,37 @@ def test_get_actual_spread(worker): assert float('Inf') > spread > 0 +def test_stop_loss_check(worker, base_account, do_initial_allocation, issue_asset): + worker.operational_depth = 100 + worker.target_spread = 0.1 # speed up allocation + do_initial_allocation(worker, worker.mode) + additional_account = base_account() + # Issue additional QUOTE to 2nd account + issue_asset(worker.market['quote']['symbol'], 500, additional_account) + + # Sleep is needed to allow node to update ticker + time.sleep(2) + + # Normal conditions - stop loss should not be executed + worker.stop_loss_check() + assert worker.disabled is False + + # Place bid below lower bound + worker.market.buy(worker.lower_bound / 1.01, 1, account=additional_account) + + # Fill all orders pushing price below lower bound + worker.market.sell(worker.lower_bound, 500, account=additional_account) + + time.sleep(2) + worker.refresh_orders() + worker.stop_loss_check() + worker.refresh_orders() + assert len(worker.sell_orders) == 1 + order = worker.sell_orders[0] + assert order['price'] ** -1 < worker.lower_bound + assert worker.disabled is True + + def test_tick(worker): """ Check tick counter increment """ From 2c5831562aa5c879b43447b09c125c8823598afd Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 15 Mar 2020 14:23:17 +0500 Subject: [PATCH 1746/1846] Remove redundant code --- dexbot/controllers/strategy_controller.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 9f8dc2714..220c37f48 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -258,10 +258,6 @@ def __init__(self, view, configure, worker_controller, worker_data): self.configure = configure self.worker_controller = worker_controller - if view: - if not self.view.strategy_widget.center_price_dynamic_input.isChecked(): - self.view.strategy_widget.center_price_input.setDisabled(False) - super().__init__(view, configure, worker_controller, worker_data) widget = self.view.strategy_widget From fb2ce30f8dedcb612688ac587831b05546f013f5 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 4 Feb 2020 15:18:17 +0500 Subject: [PATCH 1747/1846] Add cli command to drop worker data --- dexbot/cli.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dexbot/cli.py b/dexbot/cli.py index 2b3130d27..7bc73a75f 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -13,6 +13,7 @@ from dexbot.cli_conf import SYSTEMD_SERVICE_NAME, get_whiptail, setup_systemd from dexbot.config import DEFAULT_CONFIG_FILE, Config from dexbot.helper import initialize_data_folders, initialize_orders_log +from dexbot.storage import Storage from dexbot.ui import chain, configfile, reset_nodes, unlock, verbose from uptick.decorators import online @@ -186,6 +187,14 @@ def cancel(ctx, market, account): log.info(f"Account does not exist: {account}") +@click.argument('worker_name') +def drop_state(worker_name): + """ Drop state of the worker (sqlite data) + """ + click.echo('Dropping state for {}'.format(worker_name)) + Storage.clear_worker_data(worker_name) + + def worker_job(worker, job): return lambda x, y: worker.do_next_tick(job) From e2429f27c6844cc80f7f43298853aa30235da7e1 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 9 Feb 2020 16:03:46 +0500 Subject: [PATCH 1748/1846] Add small sleep when dropping state from cli Database worker is threaded and don't have a chance to exec a noreturn query, so small time is needed. --- dexbot/cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dexbot/cli.py b/dexbot/cli.py index 7bc73a75f..e70b43c69 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -4,6 +4,7 @@ import os.path import signal import sys +import time from multiprocessing import freeze_support import bitshares.exceptions @@ -193,6 +194,7 @@ def drop_state(worker_name): """ click.echo('Dropping state for {}'.format(worker_name)) Storage.clear_worker_data(worker_name) + time.sleep(1) def worker_job(worker, job): From 34de8f56f848db6e4c225e9cc795c31737a34729 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 6 Mar 2020 00:26:00 +0500 Subject: [PATCH 1749/1846] Add preventive check for node sync --- dexbot/worker.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/dexbot/worker.py b/dexbot/worker.py index 1f7571353..70b32adf2 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -8,6 +8,7 @@ import dexbot.errors as errors from bitshares.instance import shared_bitshares_instance from bitshares.notify import Notify +from bitshares.utils import parse_time from dexbot.strategies.base import StrategyBase log = logging.getLogger(__name__) @@ -24,6 +25,7 @@ def __init__(self, config, bitshares_instance=None, view=None): # BitShares instance self.bitshares = bitshares_instance or shared_bitshares_instance() + self.block_time = None self.config = copy.deepcopy(config) self.view = view self.jobs = set() @@ -115,6 +117,8 @@ def on_block(self, data): finally: self.jobs = set() + self.check_node_time() + self.config_lock.acquire() for worker_name, worker in self.config["workers"].items(): if worker_name not in self.workers: @@ -188,6 +192,23 @@ def run(self): self.update_notify() self.notify.listen() + def check_node_time(self): + """ Check that we're connected to synced node + """ + props = self.bitshares.info() + current_time = parse_time(props['time']) + + if not self.block_time: + self.block_time = current_time + elif current_time < self.block_time: + current_node = self.bitshares.rpc.url + self.bitshares.rpc.next() + new_node = self.bitshares.rpc.url + log.warning('Current node out of sync, switching: {} -> {}'.format(current_node, new_node)) + return + else: + self.block_time = current_time + def stop(self, worker_name=None, pause=False): """ Used to stop the worker(s) From 65a946760d20c64915bf138a7762e3a84aa59e87 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 31 Jan 2020 12:05:09 +0500 Subject: [PATCH 1750/1846] Add check_last_run decorator --- dexbot/decorators.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 dexbot/decorators.py diff --git a/dexbot/decorators.py b/dexbot/decorators.py new file mode 100644 index 000000000..8d5b2447d --- /dev/null +++ b/dexbot/decorators.py @@ -0,0 +1,25 @@ +from datetime import datetime, timedelta +from functools import wraps + + +def check_last_run(func): + """ This decorator is intended to be used for control maintain_strategy() execution. It requires self.last_check and + self.check_interval to be set in calling class. + """ + + @wraps(func) + def wrapper(self, *args, **kwargs): + start = datetime.now() + delta = start - self.last_check + + # Don't allow to execute wrapped function if self.check_interval hasn't been passed + if delta < timedelta(seconds=self.check_interval): + return + + func(self, *args, **kwargs) + + self.last_check = datetime.now() + delta = datetime.now() - start + self.log.debug('Maintenance execution took: {:.2f} seconds'.format(delta.total_seconds())) + + return wrapper From e5b4a1c9486f003dd473b004433da3a8bccbac16 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 16 Mar 2020 17:42:21 +0500 Subject: [PATCH 1751/1846] Define self.last_check in StrategyBase Closes: #566 --- dexbot/strategies/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 0dc829059..1b1b8b5f4 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1,6 +1,7 @@ import logging import math import time +from datetime import datetime import bitshares.exceptions from bitshares.account import Account @@ -176,6 +177,9 @@ def __init__( # Disabled flag - this flag can be flipped to True by a worker and will be reset to False after reset only self.disabled = False + # Initial value for check_last_run decorator in dexbot/decorators.py + self.last_check = datetime(1970, 1, 1) + # Order expiration time in seconds self.expiration = 60 * 60 * 24 * 365 * 5 From 81e7c539edacad697679bdd54f3e963f7278dcfe Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 16 Mar 2020 18:04:02 +0500 Subject: [PATCH 1752/1846] Switch to use check_last_run in RO --- dexbot/strategies/relative_orders.py | 15 +++------------ tests/strategies/relative_orders/conftest.py | 2 +- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 797162e52..037846468 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -1,6 +1,6 @@ import math -from datetime import datetime, timedelta +from dexbot.decorators import check_last_run from dexbot.strategies.base import StrategyBase from dexbot.strategies.config_parts.relative_config import RelativeConfig from dexbot.strategies.external_feeds.price_feed import PriceFeed @@ -90,8 +90,7 @@ def __init__(self, *args, **kwargs): self.expiration = self.default_expiration self.ontick -= self.tick # Save a few cycles there - self.last_check = datetime.now() - self.min_check_interval = 8 + self.check_interval = 8 self.buy_price = None self.sell_price = None @@ -543,19 +542,13 @@ def calculate_manual_offset(center_price, manual_offset): else: return center_price * (1 + manual_offset) + @check_last_run def check_orders(self, *args, **kwargs): """ Tests if the orders need updating """ - delta = datetime.now() - self.last_check - # Store current available balance and balance in orders to the database for profit calculation purpose self.store_profit_estimation_data() - # Only allow to check orders whether minimal time passed - if delta < timedelta(seconds=self.min_check_interval) and not self.initializing: - self.log.debug('Ignoring market_update event as min_check_interval is not passed') - return - orders = self.fetch_orders() # Detect complete fill, order expiration, manual cancel, or just init @@ -617,8 +610,6 @@ def check_orders(self, *args, **kwargs): self.update_gui_slider() self.update_gui_profit() - self.last_check = datetime.now() - def get_own_last_trade(self): """ Returns dict with amounts and price of last trade """ history = self.account.history(only_ops=['fill_order']) diff --git a/tests/strategies/relative_orders/conftest.py b/tests/strategies/relative_orders/conftest.py index b500a476b..e0dea3178 100644 --- a/tests/strategies/relative_orders/conftest.py +++ b/tests/strategies/relative_orders/conftest.py @@ -104,7 +104,7 @@ def base_worker(bitshares, ro_worker_name): def _base_worker(config, worker_name=ro_worker_name): worker = Strategy(name=worker_name, config=config, bitshares_instance=bitshares) - worker.min_check_interval = 0 + worker.check_interval = 0 workers.append(worker) return worker From ed2ef04cb4969d0db0e7edda7bd4e457184ccd73 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 16 Mar 2020 18:12:45 +0500 Subject: [PATCH 1753/1846] Use check_last_run in SO --- dexbot/strategies/staggered_orders.py | 37 ++++--------------- tests/strategies/staggered_orders/conftest.py | 4 +- .../test_staggered_orders_unittests.py | 6 --- 3 files changed, 9 insertions(+), 38 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index fd9864074..df8b00c6b 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1,12 +1,12 @@ import math import time import uuid -from datetime import datetime, timedelta from functools import reduce import bitsharesapi.exceptions from bitshares.amount import Amount from bitshares.dex import Dex +from dexbot.decorators import check_last_run from dexbot.strategies.base import StrategyBase from dexbot.strategies.config_parts.staggered_config import StaggeredConfig @@ -96,8 +96,6 @@ def __init__(self, *args, **kwargs): # Order expiration time self.expiration = 60 * 60 * 24 * 365 * 5 - self.start = datetime.now() - self.last_check = datetime.now() # We do not waiting for order ids to be able to bundle operations self.returnOrderId = None @@ -109,7 +107,7 @@ def __init__(self, *args, **kwargs): # Minimal check interval is needed to prevent event queue accumulation self.min_check_interval = 1 self.max_check_interval = 120 - self.current_check_interval = self.min_check_interval + self.check_interval = self.min_check_interval # If no bootstrap state is recorded, assume we're in bootstrap self.get('bootstrapping', True) @@ -118,18 +116,12 @@ def __init__(self, *args, **kwargs): self.update_gui_profit() self.update_gui_slider() + @check_last_run def maintain_strategy(self, *args, **kwargs): """ Logic of the strategy :param args: :param kwargs: """ - self.start = datetime.now() - delta = self.start - self.last_check - - # Only allow to maintain whether minimal time passed. - if delta < timedelta(seconds=self.current_check_interval): - return - # Get all user's orders on current market self.refresh_orders() @@ -164,7 +156,6 @@ def maintain_strategy(self, *args, **kwargs): success = self.remove_outside_orders(self.sell_orders, self.buy_orders) if not success: # Return back to beginning - self.log_maintenance_time() return # Restore virtual orders on startup if needed @@ -173,7 +164,6 @@ def maintain_strategy(self, *args, **kwargs): if self.virtual_orders_restored: self.log.info('Virtual orders restored') - self.log_maintenance_time() return # Ensure proper operational depth @@ -229,7 +219,7 @@ def maintain_strategy(self, *args, **kwargs): # Greatly increase check interval to lower CPU load whether there is no funds to allocate or we cannot # allocate funds for some reason if ( - self.current_check_interval == self.min_check_interval + self.check_interval == self.min_check_interval and self.base_balance_history[1] == self.base_balance_history[2] and self.quote_balance_history[1] == self.quote_balance_history[2] ): @@ -237,8 +227,8 @@ def maintain_strategy(self, *args, **kwargs): self.log.debug( 'Raising check interval up to {} seconds to reduce CPU usage'.format(self.max_check_interval) ) - self.current_check_interval = self.max_check_interval - elif self.current_check_interval == self.max_check_interval and ( + self.check_interval = self.max_check_interval + elif self.check_interval == self.max_check_interval and ( self.base_balance_history[1] != self.base_balance_history[2] or self.quote_balance_history[1] != self.quote_balance_history[2] ): @@ -246,7 +236,7 @@ def maintain_strategy(self, *args, **kwargs): self.log.debug( 'Reducing check interval to {} seconds because of changed ' 'balances'.format(self.min_check_interval) ) - self.current_check_interval = self.min_check_interval + self.check_interval = self.min_check_interval if previous_bootstrap_state is True and self['bootstrapping'] is False: # Bootstrap was turned off, dump initial orders @@ -259,8 +249,6 @@ def maintain_strategy(self, *args, **kwargs): or self.quote_balance_history[0] != self.quote_balance_history[2] or trx_executed ): - self.last_check = datetime.now() - self.log_maintenance_time() return # There are no funds and current orders aren't close enough, try to fix the situation by shifting orders. @@ -282,8 +270,6 @@ def maintain_strategy(self, *args, **kwargs): self.actual_spread = (lowest_sell_price / highest_buy_price) - 1 if self.actual_spread < self.target_spread + self.increment: # Target spread is reached, no need to cancel anything - self.last_check = datetime.now() - self.log_maintenance_time() return elif self.buy_orders: # If target spread is not reached and no balance to allocate, cancel lowest buy order @@ -293,19 +279,10 @@ def maintain_strategy(self, *args, **kwargs): ) self.cancel_orders_wrapper(self.buy_orders[-1]) - self.last_check = datetime.now() - self.log_maintenance_time() - # Update profit estimate if self.view: self.update_gui_profit() - def log_maintenance_time(self): - """ Measure time from self.start and print a log message - """ - delta = datetime.now() - self.start - self.log.debug('Maintenance execution took: {:.2f} seconds'.format(delta.total_seconds())) - def calculate_min_amounts(self): """ Calculate minimal order amounts depending on defined increment """ diff --git a/tests/strategies/staggered_orders/conftest.py b/tests/strategies/staggered_orders/conftest.py index 469a21c53..b998537f9 100644 --- a/tests/strategies/staggered_orders/conftest.py +++ b/tests/strategies/staggered_orders/conftest.py @@ -411,10 +411,10 @@ def maintain_until_allocated(): def func(worker): # Speed up a little worker.min_check_interval = 0.01 - worker.current_check_interval = worker.min_check_interval + worker.check_interval = worker.min_check_interval while True: worker.maintain_strategy() - if not worker.current_check_interval == worker.min_check_interval: + if not worker.check_interval == worker.min_check_interval: # Use "if" statement instead of putting this into a "while" to avoid waiting max_check_interval on last # run break diff --git a/tests/strategies/staggered_orders/test_staggered_orders_unittests.py b/tests/strategies/staggered_orders/test_staggered_orders_unittests.py index ce2b27f48..47c3e66a1 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_unittests.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_unittests.py @@ -10,12 +10,6 @@ ################### -def test_log_maintenance_time(worker): - """ Should just not fail - """ - worker.log_maintenance_time() - - def test_calculate_min_amounts(worker): """ Min amounts should be greater than assets precision """ From a32cdf4aa4f362c9416c620e835848cb78cd8bf7 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 16 Mar 2020 18:23:26 +0500 Subject: [PATCH 1754/1846] Use check_last_run in KOTH --- dexbot/strategies/king_of_the_hill.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/dexbot/strategies/king_of_the_hill.py b/dexbot/strategies/king_of_the_hill.py index 9b4e6365a..05fcf49d8 100644 --- a/dexbot/strategies/king_of_the_hill.py +++ b/dexbot/strategies/king_of_the_hill.py @@ -1,9 +1,7 @@ -# Python imports import copy -from datetime import datetime, timedelta from decimal import Decimal -# Project imports +from dexbot.decorators import check_last_run from dexbot.strategies.base import StrategyBase from dexbot.strategies.config_parts.koth_config import KothConfig @@ -74,9 +72,7 @@ def __init__(self, *args, **kwargs): # We put an order to be higher than that order self.beaten_buy_order = None self.beaten_sell_order = None - # Set last check in the past to get immediate check at startup - self.last_check = datetime(2000, 1, 1) - self.min_check_interval = self.min_order_lifetime + self.check_interval = self.min_order_lifetime self.partial_fill_threshold = 0.8 # Stubs self.highest_bid = 0 @@ -90,21 +86,16 @@ def __init__(self, *args, **kwargs): self.log.info("{} initialized.".format(STRATEGY_NAME)) + @check_last_run def maintain_strategy(self, *args): """ Strategy main logic """ - delta = datetime.now() - self.last_check - # Only allow to check orders whether minimal time passed - if delta < timedelta(seconds=self.min_check_interval): - return if self.orders: self.check_orders() else: self.place_orders() - self.last_check = datetime.now() - def check_orders(self): """ Check whether own orders needs intervention """ From ca4d624a3aa88521dc5cdfdaaf98a72cc41c69ce Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 16 Mar 2020 21:49:53 +0500 Subject: [PATCH 1755/1846] Switch node on tapos_block_summary exception For details oh why and how it's happening, see https://github.com/xeroc/python-graphenelib/issues/144 Should not happen in new release of python-graphenelib. Node switch is still needed in case of unexpected reconnection to node out of sync. Closes: #680 --- dexbot/orderengines/bitshares_engine.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index 133622a02..c113365ac 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -638,6 +638,9 @@ def retry_action(self, action, *args, **kwargs): tries = 0 while True: try: + ref_block = self.bitshares.txbuffer.get("ref_block_num") + ref_block_prefix = self.bitshares.txbuffer.get("ref_block_prefix") + self.log.debug('Ref block num: {}, prefix: {}'.format(ref_block, ref_block_prefix)) return action(*args, **kwargs) except bitsharesapi.exceptions.UnhandledRPCError as exception: if "Assert Exception: amount_to_sell.amount > 0" in str(exception): @@ -669,13 +672,26 @@ def retry_action(self, action, *args, **kwargs): else: tries += 1 self.log.warning( - 'Too much difference between node block time and trx expiration, switching ' 'node' + 'Too much difference between node block time and trx expiration, switching node' ) self.bitshares.txbuffer.clear() self.bitshares.rpc.next() elif "Assert Exception: delta.amount > 0: Insufficient Balance" in str(exception): self.log.critical('Insufficient balance of fee asset') raise + elif "trx.ref_block_prefix == tapos_block_summary.block_id._hash" in str(exception): + if tries > MAX_TRIES: + raise + else: + # TODO: move node switch to a function + old = self.bitshares.rpc.url + self.log.warning('Got tapos_block_summary exception, switching node') + self.bitshares.clear() # reinstantiates txbuilder (it caches ref_block_num) + # TODO: Notify still uses old node, needs to be switched! + self.bitshares.rpc.next() + new = self.bitshares.rpc.url + self.log.info('Old: {}, new: {}'.format(old, new)) + tries += 1 else: raise From 98ee918410ddf0c672dd828cc7a220dca4d89dfd Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 18 Mar 2020 18:23:26 +0500 Subject: [PATCH 1756/1846] Fix error() in strategy template Closes: #473 --- dexbot/strategies/strategy_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/strategy_template.py b/dexbot/strategies/strategy_template.py index a5c9ee3f9..1b55069d4 100644 --- a/dexbot/strategies/strategy_template.py +++ b/dexbot/strategies/strategy_template.py @@ -127,7 +127,7 @@ def check_orders(self, *args, **kwargs): def error(self, *args, **kwargs): """ Defines what happens when error occurs """ - self.disabled = False + self.disabled = True def pause(self): """ Override pause() in StrategyBase """ From 00a920f7db86af5bd5ecf9bec0494075034bf7ca Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 18 Mar 2020 22:07:21 +0500 Subject: [PATCH 1757/1846] Adjust inaccuracy for full-sized closer order New algo allows 5% difference for closer order instead of fixed amount Closes: #739 --- 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 13c9ea1ab..738bf4e41 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1644,11 +1644,12 @@ def place_closer_order( # Check whether new order will excess the limiter. Limiter is set based on own_aseet_limit or # opposite_asset_limit kwargs if balance < limiter: + missing = limiter - balance if allow_partial or ( # Accept small inaccuracy for full-sized closer order place_order and not allow_partial - and limiter - balance < 20 * 10 ** -precision + and missing / limiter < 0.05 ): self.log.debug( 'Limiting {} order amount to available asset balance: {:.{prec}f} {}'.format( From 86ed9d44b40b78daf1f3c62058e34a3226733ce9 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Wed, 18 Mar 2020 22:34:14 +0500 Subject: [PATCH 1758/1846] Cleanup txbuffer if worker got disabled Closes: #505 --- dexbot/worker.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dexbot/worker.py b/dexbot/worker.py index 1f7571353..e5f68ed6c 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -131,6 +131,8 @@ def on_block(self, data): self.workers[worker_name].error_ontick(e) except Exception: self.workers[worker_name].log.exception("in error_ontick()") + finally: + self.bitshares.txbuffer.clear() self.config_lock.release() def on_market(self, data): @@ -154,6 +156,9 @@ def on_market(self, data): self.workers[worker_name].error_onMarketUpdate(e) except Exception: self.workers[worker_name].log.exception("in error_onMarketUpdate()") + finally: + self.bitshares.txbuffer.clear() + self.config_lock.release() def on_account(self, account_update): @@ -175,6 +180,8 @@ def on_account(self, account_update): self.workers[worker_name].error_onAccount(e) except Exception: self.workers[worker_name].log.exception("in error_onAccountUpdate()") + finally: + self.bitshares.txbuffer.clear() self.config_lock.release() def add_worker(self, worker_name, config): From b6779c3d23fc1368b3f06dd2b2c2f2abcf4d0905 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 19 Mar 2020 16:41:25 +0500 Subject: [PATCH 1759/1846] Wrap trx execution via retry_action() This is needed to add handling of common broadcast errors. Closes: #748 --- dexbot/strategies/staggered_orders.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index fd9864074..a727d3ca4 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -201,7 +201,7 @@ def maintain_strategy(self, *args, **kwargs): if not self.bitshares.txbuffer.is_empty(): trx_executed = True try: - self.execute() + trx = self.retry_action(self.execute) except bitsharesapi.exceptions.RPCError as exception: """ Handle exception without stopping the worker. The goal is to handle race condition when partially filled order was further filled before we actually replaced them. @@ -212,6 +212,8 @@ def maintain_strategy(self, *args, **kwargs): return else: raise + order_ids = [result[1] for result in trx['operation_results']] + self.log.debug('Placed orders: %s', order_ids) self.refresh_orders() self.sync_current_orders() From 264bc40191e8e85fcc6cf6b7ed1a82c5ed29a3a9 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 20 Mar 2020 08:22:19 +0200 Subject: [PATCH 1760/1846] Change dexbot version number to 0.19.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 98f724f6c..0a5045ea7 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.18.1' +VERSION = '0.19.0' AUTHOR = 'Codaone Oy' __version__ = VERSION From b4de2b5ec56486cb54ca462972656e6322e60c7c Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 20 Mar 2020 08:28:32 +0200 Subject: [PATCH 1761/1846] Change dexbot version number to 0.19.1 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 0a5045ea7..3c14338f3 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.19.0' +VERSION = '0.19.1' AUTHOR = 'Codaone Oy' __version__ = VERSION From b1c820843f722213b2a205a9f3f6f855f4d42cc3 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 20 Mar 2020 08:40:43 +0200 Subject: [PATCH 1762/1846] Change dexbot version number to 0.19.2 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 3c14338f3..9d2e1b252 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.19.1' +VERSION = '0.19.2' AUTHOR = 'Codaone Oy' __version__ = VERSION From 2d0d3e37a271afbf77df6f1965f9b5879af51713 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 20 Mar 2020 08:43:18 +0200 Subject: [PATCH 1763/1846] Change dexbot version number to 0.19.3 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 9d2e1b252..84d589b29 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.19.2' +VERSION = '0.19.3' AUTHOR = 'Codaone Oy' __version__ = VERSION From 5b2a933fbd3b008e8632299faf9bc1bc1fe5e19c Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 20 Mar 2020 08:48:46 +0200 Subject: [PATCH 1764/1846] Change dexbot version number to 0.20.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 84d589b29..c3ffb8895 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.19.3' +VERSION = '0.20.0' AUTHOR = 'Codaone Oy' __version__ = VERSION From 743cd10742402d4c74110e772d7458c8743b0999 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 20 Mar 2020 09:00:11 +0200 Subject: [PATCH 1765/1846] Change dexbot version number to 0.21.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index c3ffb8895..493a54559 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.20.0' +VERSION = '0.21.0' AUTHOR = 'Codaone Oy' __version__ = VERSION From 7ae807ca6c86a3a5386ff9a7378375863365580e Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 20 Mar 2020 09:05:56 +0200 Subject: [PATCH 1766/1846] Change dexbot version number to 0.21.1 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 493a54559..002b95633 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.21.0' +VERSION = '0.21.1' AUTHOR = 'Codaone Oy' __version__ = VERSION From 0af48d4b95d99cb98728fd8868b2c1bfddb1bc5c Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 20 Mar 2020 09:52:50 +0200 Subject: [PATCH 1767/1846] Change dexbot version number to 0.21.2 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 002b95633..c8fa53214 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.21.1' +VERSION = '0.21.2' AUTHOR = 'Codaone Oy' __version__ = VERSION From 21e1c54ff1608f31a19d31731aa2d9f27917ba13 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 20 Mar 2020 09:56:48 +0200 Subject: [PATCH 1768/1846] Change dexbot version number to 0.20.3 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index c8fa53214..a1311546f 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.21.2' +VERSION = '0.21.3' AUTHOR = 'Codaone Oy' __version__ = VERSION From 3ff6303f361e43bbeb7e0e2c5a70dfba5f52b1a5 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 20 Mar 2020 09:58:36 +0200 Subject: [PATCH 1769/1846] Change dexbot version number to 0.20.4 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index a1311546f..b8326a62d 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.21.3' +VERSION = '0.21.4' AUTHOR = 'Codaone Oy' __version__ = VERSION From a7614c6ac27c22b667691f0cf44cf30df34b87a8 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 20 Mar 2020 11:02:48 +0200 Subject: [PATCH 1770/1846] Change dexbot version number to 0.21.6 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index b8326a62d..7888dbc15 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.21.4' +VERSION = '0.21.6' AUTHOR = 'Codaone Oy' __version__ = VERSION From e6466f68b265c62799967498cf8954865b10a07b Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 20 Mar 2020 11:29:44 +0200 Subject: [PATCH 1771/1846] Change dexbot version number to 0.22.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 7888dbc15..dd1bb2d81 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.21.6' +VERSION = '0.22.0' AUTHOR = 'Codaone Oy' __version__ = VERSION From e150b4eb58d3b12c6d24401c00db3681f4d0c754 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 20 Mar 2020 11:42:09 +0200 Subject: [PATCH 1772/1846] Change dexbot version number to 0.22.1 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index dd1bb2d81..fe5b03a2c 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.22.0' +VERSION = '0.22.1' AUTHOR = 'Codaone Oy' __version__ = VERSION From e7aa51e1d099072479d2e6521e6834dedcd22ad9 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 20 Mar 2020 11:43:28 +0200 Subject: [PATCH 1773/1846] Change dexbot version number to 0.22.2 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index fe5b03a2c..fa60ab45f 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.22.1' +VERSION = '0.22.2' AUTHOR = 'Codaone Oy' __version__ = VERSION From 62876e8b59e2e4c3a0727591a2b7918b0a2c387e Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 20 Mar 2020 11:47:01 +0200 Subject: [PATCH 1774/1846] Change dexbot version number to 0.22.3 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index fa60ab45f..b1c6a2c2d 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.22.2' +VERSION = '0.22.3' AUTHOR = 'Codaone Oy' __version__ = VERSION From 9e96ada8fcca8ed78da78fd1424c3980ee2f91c5 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 20 Mar 2020 11:49:05 +0200 Subject: [PATCH 1775/1846] Change dexbot version number to 0.22.4 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index b1c6a2c2d..ed978df8b 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.22.3' +VERSION = '0.22.4' AUTHOR = 'Codaone Oy' __version__ = VERSION From e8a28637f54c05062e1bbabb5914edf705b2503e Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 20 Mar 2020 11:50:53 +0200 Subject: [PATCH 1776/1846] Change dexbot version number to 0.22.5 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index ed978df8b..4b89859fb 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.22.4' +VERSION = '0.22.5' AUTHOR = 'Codaone Oy' __version__ = VERSION From d1a12824f660ebfab6bf723fe61e4dcf45e8b74f Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Fri, 20 Mar 2020 12:00:05 +0200 Subject: [PATCH 1777/1846] Change dexbot version number to 0.23.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 4b89859fb..406877129 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.22.5' +VERSION = '0.23.0' AUTHOR = 'Codaone Oy' __version__ = VERSION From 4214894a858c7430d303b809f16bcd0a848ff68d Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 27 Mar 2020 16:23:40 +0500 Subject: [PATCH 1778/1846] Require config instance in ConfigValidator Now ConfigValidator doesn't have to instantiate config on its own, so it doesn't looses custom config. Closes: #631 --- dexbot/cli_conf.py | 21 ++++++++++----------- dexbot/config_validator.py | 15 +++++++-------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 23fa8818d..a946b4a2b 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -187,12 +187,12 @@ def get_strategy_tag(strategy_class): return None -def configure_worker(whiptail, worker_config, bitshares_instance): +def configure_worker(whiptail, worker_config, validator): """ Single worker configurator :param whiptail.Whiptail whiptail: instance of Whiptail or NoWhiptail - :param collections.OrderedDict worker_config: the config dictionary for this worker - :param bitshares.BitShares bitshares_instance: an instance of BitShares class + :param collections.OrderedDict worker_config: tohe config dictionary for this worker + :param dexbot.config_validator.ConfigValidator validator: dexbot config validator """ # By default always editing editing = True @@ -249,7 +249,7 @@ def configure_worker(whiptail, worker_config, bitshares_instance): account_name = None # Query user until correct account and key provided while not account_name: - account_name = add_account(whiptail, bitshares_instance) + account_name = add_account(validator, whiptail) worker_config[elem.key] = account_name else: # account name only for edit worker process_config_element(elem, whiptail, worker_config) @@ -270,7 +270,7 @@ def configure_dexbot(config, ctx): whiptail = get_whiptail('DEXBot configure') workers = config.get('workers', {}) bitshares_instance = ctx.bitshares - validator = ConfigValidator(bitshares_instance) + validator = ConfigValidator(config, bitshares_instance) if not workers: while True: @@ -278,7 +278,7 @@ def configure_dexbot(config, ctx): if len(txt) == 0: whiptail.alert("Worker name cannot be blank. ") else: - config['workers'] = {txt: configure_worker(whiptail, {}, bitshares_instance)} + config['workers'] = {txt: configure_worker(whiptail, {}, validator)} if not whiptail.confirm("Set up another worker?\n(DEXBot can run multiple workers in one instance)"): break setup_systemd(whiptail, config) @@ -324,7 +324,7 @@ def configure_dexbot(config, ctx): if len(my_workers): worker_name = whiptail.menu("Select worker to edit", my_workers) config['workers'][worker_name] = configure_worker( - whiptail, config['workers'][worker_name], bitshares_instance + whiptail, config['workers'][worker_name], validator ) else: whiptail.alert('No workers to edit.') @@ -347,7 +347,7 @@ def configure_dexbot(config, ctx): elif not validator.validate_worker_name(worker_name): whiptail.alert('Worker name needs to be unique. "{}" is already in use.'.format(worker_name)) else: - config['workers'][worker_name] = configure_worker(whiptail, {}, bitshares_instance) + config['workers'][worker_name] = configure_worker(whiptail, {}, validator) elif action == 'ADD': add_account(whiptail, bitshares_instance) elif action == 'DEL_ACCOUNT': @@ -390,14 +390,13 @@ def configure_dexbot(config, ctx): return config -def add_account(whiptail, bitshares_instance): +def add_account(validator, whiptail): """ "Add account" dialog + :param dexbot.config_validator.ConfigValidator validator: dexbot config validator :param whiptail.Whiptail whiptail: instance of Whiptail or NoWhiptail - :param bitshares.BitShares bitshares_instance: an instance of BitShares class :return str: user-supplied account name """ - validator = ConfigValidator(bitshares_instance) account = whiptail.prompt("Your Account Name") private_key = whiptail.prompt("Your Private Key", password=True) diff --git a/dexbot/config_validator.py b/dexbot/config_validator.py index c2dab4bf0..aca0c0075 100644 --- a/dexbot/config_validator.py +++ b/dexbot/config_validator.py @@ -3,16 +3,17 @@ from bitshares.exceptions import AccountDoesNotExistsException, AssetDoesNotExistsException, KeyAlreadyInStoreException from bitshares.instance import shared_bitshares_instance from bitsharesbase.account import PrivateKey -from dexbot.config import Config class ConfigValidator: """ Config validation methods + :param dexbot.config.Config config: dexbot config :param bitshares.BitShares: BitShares instance """ - def __init__(self, bitshares_instance): + def __init__(self, config, bitshares_instance): + self.config = config self.bitshares = bitshares_instance or shared_bitshares_instance() def validate_account_name(self, account): @@ -70,28 +71,26 @@ def validate_private_key_type(self, account, private_key): return False return True - @staticmethod - def validate_worker_name(worker_name, old_worker_name=None): + def validate_worker_name(self, worker_name, old_worker_name=None): """ Check whether worker name is unique or not :param str worker_name: name of the new worker :param str old_worker_name: old name of the worker """ if old_worker_name != worker_name: - worker_names = Config().workers_data.keys() + worker_names = self.config.workers_data.keys() # Check that the name is unique if worker_name in worker_names: return False return True return True - @staticmethod - def validate_account_not_in_use(account): + def validate_account_not_in_use(self, account): """ Check whether account is already used for another worker or not :param str account: bitshares account name """ - workers = Config().workers_data + workers = self.config.workers_data for worker_name, worker in workers.items(): if worker['account'] == account: return False From 04c232c84913e93a3aa18cd43a9dedce7c7f4529 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 27 Mar 2020 16:38:51 +0500 Subject: [PATCH 1779/1846] Fix ConfigValidator init in WorkerController --- dexbot/controllers/worker_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index aff6fb13b..4a2b38181 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -16,7 +16,7 @@ class WorkerController: def __init__(self, view, bitshares_instance, mode): self.view = view self.mode = mode - self.validator = ConfigValidator(bitshares_instance or shared_bitshares_instance()) + self.validator = ConfigValidator(Config(), bitshares_instance or shared_bitshares_instance()) @property def strategies(self): From 78bab4a82f3a3b5b72ff70a6a07d79f12c28380d Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 27 Mar 2020 17:21:40 +0500 Subject: [PATCH 1780/1846] Adjust 0 balance message in "both" mode To not scary user when some asset has 0 balance in "both" mode, change log message level to debug. Despite report, such condition doesn't leads to "worker disabled" error. Closes: #715 --- dexbot/strategies/king_of_the_hill.py | 18 ++++++++++++------ .../king_of_the_hill/test_king_of_the_hill.py | 7 +++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/king_of_the_hill.py b/dexbot/strategies/king_of_the_hill.py index 05fcf49d8..8c08c5b07 100644 --- a/dexbot/strategies/king_of_the_hill.py +++ b/dexbot/strategies/king_of_the_hill.py @@ -221,9 +221,12 @@ def place_order(self, order_type): amount_base = Decimal(self.amount_base).quantize(Decimal(0).scaleb(-self.market['base']['precision'])) if not amount_base: - self.log.error( - 'Cannot place {} order with 0 amount. Adjust your settings or add balance'.format(order_type) - ) + if self.mode == 'both': + self.log.debug('Not placing %s order in "both" mode due to insufficient balance', order_type) + else: + self.log.error( + 'Cannot place {} order with 0 amount. Adjust your settings or add balance'.format(order_type) + ) return False price = Decimal(self.top_buy_price) @@ -263,9 +266,12 @@ def place_order(self, order_type): amount_quote = Decimal(self.amount_quote).quantize(Decimal(0).scaleb(-self.market['quote']['precision'])) if not amount_quote: - self.log.error( - 'Cannot place {} order with 0 amount. Adjust your settings or add balance'.format(order_type) - ) + if self.mode == 'both': + self.log.debug('Not placing %s order in "both" mode due to insufficient balance', order_type) + else: + self.log.error( + 'Cannot place {} order with 0 amount. Adjust your settings or add balance'.format(order_type) + ) return False price = Decimal(self.top_sell_price) diff --git a/tests/strategies/king_of_the_hill/test_king_of_the_hill.py b/tests/strategies/king_of_the_hill/test_king_of_the_hill.py index a25e9133d..6c7a042ee 100644 --- a/tests/strategies/king_of_the_hill/test_king_of_the_hill.py +++ b/tests/strategies/king_of_the_hill/test_king_of_the_hill.py @@ -92,6 +92,13 @@ def test_place_order_zero_amount(worker, other_orders, monkeypatch): monkeypatch.setattr(worker.__class__, 'amount_base', 0) assert worker.place_order('buy') is False + # Test other modes too + worker.mode = 'buy' + assert worker.place_order('buy') is False + + worker.mode = 'sell' + assert worker.place_order('sell') is False + def test_place_orders(worker2, other_orders): """ Test that orders are placed according to mode (buy, sell, buy + sell). Simple test, just make sure buy/sell From 03c9b30bb5a90246ecd3b2a1d077dc3e84bdd21d Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 29 Mar 2020 20:33:59 +0500 Subject: [PATCH 1781/1846] Add bitasset fixtures from python-bitshares --- tests/conftest.py | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 3161c5895..40f97d1cc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ import os.path import random import socket +import string import time import uuid @@ -118,9 +119,11 @@ def create_asset(bitshares): """ Create a new asset """ - def _create_asset(asset, precision): + def _create_asset(asset, precision, is_bitasset=False): max_supply = 1000000000000000 / 10 ** precision if precision > 0 else 1000000000000000 - bitshares.create_asset(asset, precision, max_supply, account=DEFAULT_ACCOUNT) + bitshares.create_asset( + asset, precision, max_supply, is_bitasset=is_bitasset, account=DEFAULT_ACCOUNT, + ) return _create_asset @@ -141,6 +144,27 @@ def _issue_asset(asset, amount, to): return _issue_asset +@pytest.fixture(scope="session") +def base_bitasset(bitshares, unused_asset): + def func(): + bitasset_options = { + "feed_lifetime_sec": 86400, + "minimum_feeds": 1, + "force_settlement_delay_sec": 86400, + "force_settlement_offset_percent": 100, + "maximum_force_settlement_volume": 50, + "short_backing_asset": "1.3.0", + "extensions": [], + } + symbol = unused_asset() + bitshares.create_asset(symbol, 5, 10000000000, is_bitasset=True, bitasset_options=bitasset_options) + asset = Asset(symbol) + asset.update_feed_producers([DEFAULT_ACCOUNT]) + return asset + + return func + + @pytest.fixture(scope='session') def create_account(bitshares): """ Create new account @@ -180,6 +204,19 @@ def _unused_account(): return _unused_account +@pytest.fixture(scope="session") +def unused_asset(bitshares): + def func(): + while True: + asset = "".join(random.choice(string.ascii_uppercase) for x in range(7)) + try: + Asset(asset, bitshares_instance=bitshares) + except AssetDoesNotExistsException: + return asset + + return func + + @pytest.fixture(scope='session') def prepare_account(bitshares, unused_account, create_account, create_asset, issue_asset): """ Ensure an account with specified amounts of assets. Account must not exist! From a10b8da6bce109c3115e46486903689da7895639 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 29 Mar 2020 22:27:28 +0500 Subject: [PATCH 1782/1846] Move default account name to fixture --- tests/conftest.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 40f97d1cc..2ae5b7cf6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,12 +21,16 @@ known_chains["TEST"]["chain_id"] = "c74ddb39b3a233445dd95d7b6fc2d0fa4ba666698db26b53855d94fffcc460af" PRIVATE_KEYS = ['5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3'] -DEFAULT_ACCOUNT = 'init0' # Example how to split conftest.py into multiple files # pytest_plugins = ['fixture_a.py', 'fixture_b.py'] +@pytest.fixture(scope="session") +def default_account(): + return "init0" + + @pytest.fixture(scope='session') def session_id(): """ Generate unique session id. This is needed in case testsuite may run in parallel on the same server, for example @@ -100,11 +104,11 @@ def bitshares_instance(bitshares_testnet): @pytest.fixture(scope='session') -def claim_balance(bitshares_instance): +def claim_balance(bitshares_instance, default_account): """ Transfer balance from genesis into actual account """ genesis_balance = GenesisBalance('1.15.0', bitshares_instance=bitshares_instance) - genesis_balance.claim(account=DEFAULT_ACCOUNT) + genesis_balance.claim(account=default_account) @pytest.fixture(scope='session') @@ -115,14 +119,14 @@ def bitshares(bitshares_instance, claim_balance): @pytest.fixture(scope='session') -def create_asset(bitshares): +def create_asset(bitshares, default_account): """ Create a new asset """ def _create_asset(asset, precision, is_bitasset=False): max_supply = 1000000000000000 / 10 ** precision if precision > 0 else 1000000000000000 bitshares.create_asset( - asset, precision, max_supply, is_bitasset=is_bitasset, account=DEFAULT_ACCOUNT, + asset, precision, max_supply, is_bitasset=is_bitasset, account=default_account, ) return _create_asset @@ -145,7 +149,7 @@ def _issue_asset(asset, amount, to): @pytest.fixture(scope="session") -def base_bitasset(bitshares, unused_asset): +def base_bitasset(bitshares, unused_asset, default_account): def func(): bitasset_options = { "feed_lifetime_sec": 86400, @@ -159,23 +163,23 @@ def func(): symbol = unused_asset() bitshares.create_asset(symbol, 5, 10000000000, is_bitasset=True, bitasset_options=bitasset_options) asset = Asset(symbol) - asset.update_feed_producers([DEFAULT_ACCOUNT]) + asset.update_feed_producers([default_account]) return asset return func @pytest.fixture(scope='session') -def create_account(bitshares): +def create_account(bitshares, default_account): """ Create new account """ def _create_account(account): - parent_account = Account(DEFAULT_ACCOUNT, bitshares_instance=bitshares) + parent_account = Account(default_account, bitshares_instance=bitshares) public_key = PublicKey.from_privkey(PRIVATE_KEYS[0], prefix=bitshares.prefix) bitshares.create_account( account, - registrar=DEFAULT_ACCOUNT, + registrar=default_account, referrer=parent_account['id'], referrer_percent=0, owner_key=public_key, @@ -218,7 +222,7 @@ def func(): @pytest.fixture(scope='session') -def prepare_account(bitshares, unused_account, create_account, create_asset, issue_asset): +def prepare_account(bitshares, unused_account, create_account, create_asset, issue_asset, default_account): """ Ensure an account with specified amounts of assets. Account must not exist! :param dict assets: assets to credit account balance with @@ -244,7 +248,7 @@ def _prepare_account(assets, account=None): create_asset(asset, 5) if asset == 'TEST': - bitshares.transfer(account, amount, 'TEST', memo='prepare account', account=DEFAULT_ACCOUNT) + bitshares.transfer(account, amount, 'TEST', memo='prepare account', account=default_account) else: issue_asset(asset, amount, account) From 8bff94c841010440c74dcc2661be353ffa14d39b Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 29 Mar 2020 22:28:42 +0500 Subject: [PATCH 1783/1846] Add support for margin call orders in KOTH Closes: #677 --- dexbot/strategies/king_of_the_hill.py | 110 ++++++++++++++---- tests/strategies/king_of_the_hill/conftest.py | 62 ++++++++++ .../king_of_the_hill/test_king_of_the_hill.py | 37 +++++- 3 files changed, 185 insertions(+), 24 deletions(-) diff --git a/dexbot/strategies/king_of_the_hill.py b/dexbot/strategies/king_of_the_hill.py index 05fcf49d8..7601aa491 100644 --- a/dexbot/strategies/king_of_the_hill.py +++ b/dexbot/strategies/king_of_the_hill.py @@ -1,6 +1,7 @@ import copy from decimal import Decimal +from bitshares.price import Price from dexbot.decorators import check_last_run from dexbot.strategies.base import StrategyBase from dexbot.strategies.config_parts.koth_config import KothConfig @@ -66,17 +67,13 @@ def __init__(self, *args, **kwargs): self.min_order_lifetime = self.worker.get('min_order_lifetime', 1) self.orders = {} - # Current order we must be higher - self.buy_order_to_beat = None - self.sell_order_to_beat = None - # We put an order to be higher than that order - self.beaten_buy_order = None - self.beaten_sell_order = None self.check_interval = self.min_order_lifetime self.partial_fill_threshold = 0.8 # Stubs self.highest_bid = 0 self.lowest_ask = 0 + self.buy_gap = 0 + self.sell_gap = 0 if self.view: self.update_gui_slider() @@ -84,8 +81,37 @@ def __init__(self, *args, **kwargs): # Make sure we're starting from scratch as we don't keeping orders in the db self.cancel_all_orders() + self.call_orders_expected = False + self.debt_asset = None + self.check_bitasset_market() + self.log.info("{} initialized.".format(STRATEGY_NAME)) + def check_bitasset_market(self): + """ Check if worker market is MPA:COLLATERAL market + """ + if not (self.market['base'].is_bitasset or self.market['quote'].is_bitasset): + # One of the assets must be a bitasset + return + + if self.market['base'].is_bitasset: + self.market['base'].ensure_full() + if self.market['base']['bitasset_data']['is_prediction_market']: + return + backing = self.market['base']['bitasset_data']['options']['short_backing_asset'] + if backing == self.market['quote']['id']: + self.debt_asset = self.market['base'] + self.call_orders_expected = True + + if self.market['quote'].is_bitasset: + self.market['quote'].ensure_full() + if self.market['quote']['bitasset_data']['is_prediction_market']: + return + backing = self.market['quote']['bitasset_data']['options']['short_backing_asset'] + if backing == self.market['base']['id']: + self.debt_asset = self.market['quote'] + self.call_orders_expected = True + @check_last_run def maintain_strategy(self, *args): """ Strategy main logic @@ -113,18 +139,22 @@ def check_orders(self): self.log.info('Own {} order filled too much, resetting'.format(order_type)) need_cancel = True # Check if someone put order above ours or beaten order was canceled - elif order_type == 'buy' and not self.get_order(self.beaten_buy_order): - self.log.debug('No beaten buy order on market') - need_cancel = True - elif order_type == 'buy' and order['price'] < self.top_buy_price: - self.log.debug('Detected an order above ours') - need_cancel = True - elif order_type == 'sell' and not self.get_order(self.beaten_sell_order): - self.log.debug('No beaten sell order on market') - need_cancel = True - elif order_type == 'sell' and order['price'] ** -1 > self.top_sell_price: - self.log.debug('Detected an order above ours') - need_cancel = True + elif order_type == 'buy': + diff = abs(order['price'] - self.top_buy_price) + if order['price'] < self.top_buy_price: + self.log.debug('Detected an order above ours') + need_cancel = True + elif diff > self.buy_gap: + self.log.debug('Too much gap between our top buy order and next further order: %s', diff) + need_cancel = True + elif order_type == 'sell': + diff = abs(order['price'] ** -1 - self.top_sell_price) + if order['price'] ** -1 > self.top_sell_price: + self.log.debug('Detected an order above ours') + need_cancel = True + elif diff > self.sell_gap: + self.log.debug('Too much gap between our top sell order and further order: %s', diff) + need_cancel = True # Own order is not there else: @@ -157,7 +187,6 @@ def get_top_prices(self): for order in sell_orders: if order['quote']['amount'] > sell_order_size_threshold: self.top_sell_price = order['price'] - self.sell_order_to_beat = order['id'] if self.top_sell_price < self.lower_bound: self.log.debug( 'Top sell price to be higher {:.8f} < lower bound {:.8f}'.format( @@ -172,7 +201,6 @@ def get_top_prices(self): for order in buy_orders: if order['base']['amount'] > buy_order_size_threshold: self.top_buy_price = order['price'] - self.buy_order_to_beat = order['id'] if self.top_buy_price > self.upper_bound: self.log.debug( 'Top buy price to be higher {:.8f} > upper bound {:.8f}'.format( @@ -184,6 +212,23 @@ def get_top_prices(self): self.log.debug('Top buy price to be higher: {:.8f}'.format(self.top_buy_price)) break + if self.call_orders_expected: + call_order = self.get_cumulative_call_order(self.debt_asset) + if self.debt_asset == self.market['base'] and call_order['base']['amount'] > sell_order_size_threshold: + call_price = call_order['price'] ** -1 + self.log.debug('Margin call on market {} at price {:.8f}'.format(self.worker['market'], call_price)) + # If no orders on market, set price to Inf (default is 0 to indicate no orders + self.top_sell_price = self.top_sell_price or float('Inf') + if call_price < self.top_sell_price: + self.log.debug('Correcting top sell price to {:.8f}'.format(call_price)) + self.top_sell_price = call_price + elif self.debt_asset == self.market['quote'] and call_order['base']['amount'] > buy_order_size_threshold: + call_price = call_order['price'] + self.log.debug('Margin call on market {} at price {:.8f}'.format(self.worker['market'], call_price)) + if call_price > self.top_buy_price: + self.log.debug('Correcting top buy price to {:.8f}'.format(call_price)) + self.top_buy_price = call_price + # Fill top prices from orderbook because we need to keep in mind own orders too # FYI: getting price from self.ticker() doesn't work in local testnet orderbook = self.get_orderbook_orders(depth=1) @@ -193,6 +238,25 @@ def get_top_prices(self): except IndexError: self.log.info('Market has empty orderbook') + def get_cumulative_call_order(self, asset): + """ Get call orders, compound them and return as it was a single limit order + + :param Asset asset: bitshares asset + :return: dict representing an order + """ + # TODO: move this method to price engine to use for center price detection etc + call_orders = asset.get_call_orders() + collateral = debt = 0 + for call in call_orders: + collateral += call['collateral']['amount'] + debt += call['debt']['amount'] + + settlement_price = Price(asset['bitasset_data']['current_feed']['settlement_price']) + maximum_short_squeeze_ratio = asset['bitasset_data']['current_feed']['maximum_short_squeeze_ratio'] / 100 + call_price = settlement_price / maximum_short_squeeze_ratio + order = {'base': {'amount': collateral}, 'quote': {'amount': debt}, 'price': float(call_price)} + return order + def is_too_small_amounts(self, amount_quote, amount_base): """ Check whether amounts are within asset precision limits :param Decimal amount_quote: QUOTE asset amount @@ -254,7 +318,6 @@ def place_order(self, order_type): return new_order = self.place_market_buy_order(float(amount_quote), float(price)) - self.beaten_buy_order = self.buy_order_to_beat elif order_type == 'sell': if not self.top_sell_price: self.log.error('Cannot determine top sell price, correct your bounds and/or ignore thresholds') @@ -295,11 +358,14 @@ def place_order(self, order_type): return new_order = self.place_market_sell_order(float(amount_quote), float(price)) - self.beaten_sell_order = self.sell_order_to_beat if new_order: # Store own order into dict {order_type: id} to perform checks later self.orders[order_type] = new_order['id'] + if order_type == 'buy': + self.buy_gap = new_order['price'] - self.top_buy_price + elif order_type == 'sell': + self.sell_gap = self.top_sell_price - new_order['price'] ** -1 else: self.log.error('Failed to place {} order'.format(order_type)) diff --git a/tests/strategies/king_of_the_hill/conftest.py b/tests/strategies/king_of_the_hill/conftest.py index 8519f3c82..55022e5f6 100644 --- a/tests/strategies/king_of_the_hill/conftest.py +++ b/tests/strategies/king_of_the_hill/conftest.py @@ -3,6 +3,10 @@ import time import pytest +from bitshares.amount import Amount +from bitshares.asset import Asset +from bitshares.dex import Dex +from bitshares.price import Price from dexbot.strategies.base import StrategyBase from dexbot.strategies.king_of_the_hill import Strategy @@ -87,6 +91,36 @@ def config_variable_modes(request, config, kh_worker_name): return config +@pytest.fixture(params=[0, 1]) +def config_bitasset_market(request, kh_worker_name, bitasset_local, bitshares, account): + """ Produces a config with market MPA:COLLATERAL or COLLATERAL:MPA + """ + worker_name = kh_worker_name + bitasset = bitasset_local + market = f'{bitasset.symbol}/TEST' if request.param == 0 else f'TEST/{bitasset.symbol}' + config = { + 'node': '{}'.format(bitshares.rpc.url), + 'workers': { + worker_name: { + 'account': '{}'.format(account), + 'buy_order_amount': 1.0, + 'buy_order_size_threshold': 0.0, + 'fee_asset': 'TEST', + 'lower_bound': 1.8, + 'market': market, + 'min_order_lifetime': 60, + 'mode': 'both', + 'module': 'dexbot.strategies.king_of_the_hill', + 'relative_order_size': False, + 'sell_order_amount': 1.0, + 'sell_order_size_threshold': 0.0, + 'upper_bound': 1.2, + } + }, + } + return config + + @pytest.fixture def config_other_account(config, base_account, kh_worker_name): """ Config for other account which simulates foreign trader @@ -133,6 +167,14 @@ def worker2(base_worker, config_variable_modes): return worker +@pytest.fixture +def worker_bitasset(base_worker, config_bitasset_market): + """ Worker operating on MPA/COLLATERAL market + """ + worker = base_worker(config_bitasset_market) + return worker + + @pytest.fixture def orders1(worker): """ Place buy and sell order using worker account @@ -195,3 +237,23 @@ def other_orders_zero_spread(other_worker): buy_order = worker.get_order(orderid) log.debug('Other orders after match: {}'.format(worker.own_orders)) return worker + + +@pytest.fixture(scope="session") +def bitasset_local(bitshares, base_bitasset, default_account): + asset = base_bitasset() + dex = Dex(blockchain_instance=bitshares) + + # Set initial price feed + price = Price(1.5, base=asset, quote=Asset("TEST")) + bitshares.publish_price_feed(asset.symbol, price, account=default_account) + + # Borrow some amount + to_borrow = Amount(100, asset) + dex.borrow(to_borrow, collateral_ratio=2.1, account=default_account) + + # Drop pricefeed to cause margin call + price = Price(1.0, base=asset, quote=Asset("TEST")) + bitshares.publish_price_feed(asset.symbol, price, account=default_account) + + return asset diff --git a/tests/strategies/king_of_the_hill/test_king_of_the_hill.py b/tests/strategies/king_of_the_hill/test_king_of_the_hill.py index a25e9133d..d84aa07a1 100644 --- a/tests/strategies/king_of_the_hill/test_king_of_the_hill.py +++ b/tests/strategies/king_of_the_hill/test_king_of_the_hill.py @@ -42,6 +42,16 @@ def test_get_top_prices(other_orders, worker): assert pytest.approx(worker.top_sell_price) == top_price_ask +def test_get_top_prices_margin_call(worker_bitasset): + worker = worker_bitasset + worker.get_top_prices() + call_order = worker.get_cumulative_call_order(worker.debt_asset) + if worker.debt_asset == worker.market['base']: + assert worker.top_sell_price == call_order['price'] ** -1 + elif worker.debt_asset == worker.market['quote']: + assert worker.top_buy_price == call_order['price'] + + def test_place_order_correct_price(worker, other_orders): """ Test that buy order is placed at correct price. Similar to test_get_top_prices(), but with actual order placement @@ -71,12 +81,12 @@ def test_place_order_correct_price(worker, other_orders): def test_place_order_zero_price(worker): """ Check that worker goes into error if no prices are calculated """ - worker.sell_price = 0 + worker.top_sell_price = 0 worker.place_order('sell') assert worker.disabled worker.disabled = False - worker.buy_price = 0 + worker.top_buy_price = 0 worker.place_order('buy') assert worker.disabled @@ -293,3 +303,26 @@ def test_zero_spread(worker, other_orders_zero_spread): # Own orders not partially filled for order in worker.own_orders: assert order['base']['amount'] == order['for_sale']['amount'] + + +def test_check_bitasset_market_non_bitasset(worker): + """ Worker market is not MPA/COLLATERAL + """ + worker.check_bitasset_market() + assert worker.call_orders_expected is False + + +def test_check_bitasset_market_bitasset(worker_bitasset): + """ Correctly determine if worker market is MPA/COLLATERAL + """ + worker = worker_bitasset + worker.check_bitasset_market() + assert worker.call_orders_expected is True + + +def test_get_cumulative_call_order(worker_bitasset): + worker = worker_bitasset + call_order = worker.get_cumulative_call_order(worker.debt_asset) + assert call_order['base']['amount'] > 0 + assert call_order['quote']['amount'] > 0 + assert call_order['price'] > 0 From a0dabe9c4015d9a69ac5966df8bd312e43216438 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 31 Mar 2020 14:12:35 +0300 Subject: [PATCH 1784/1846] Change dexbot version number to 0.24.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 406877129..253e29e88 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.23.0' +VERSION = '0.24.0' AUTHOR = 'Codaone Oy' __version__ = VERSION From 5ccf509d3ebe7df7cf74851e0fb6405d801b05f5 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 31 Mar 2020 14:15:37 +0300 Subject: [PATCH 1785/1846] Change dexbot version number to 0.24.1 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 253e29e88..dbb6149eb 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.24.0' +VERSION = '0.24.1' AUTHOR = 'Codaone Oy' __version__ = VERSION From bf5048e587532587240f1637e581a8f75d6646f4 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 31 Mar 2020 14:17:29 +0300 Subject: [PATCH 1786/1846] Change dexbot version number to 0.24.2 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index dbb6149eb..77344bce9 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.24.1' +VERSION = '0.24.2' AUTHOR = 'Codaone Oy' __version__ = VERSION From a300ff418fa171e19a128610bcc176fd1b8b0772 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 5 Apr 2020 22:26:25 +0500 Subject: [PATCH 1787/1846] Add initial test for BitsharesOrderEngine --- tests/primitives/conftest.py | 65 +++++++++++++++++++ .../primitives/test_bitshares_orderengine.py | 13 ++++ 2 files changed, 78 insertions(+) create mode 100644 tests/primitives/conftest.py create mode 100644 tests/primitives/test_bitshares_orderengine.py diff --git a/tests/primitives/conftest.py b/tests/primitives/conftest.py new file mode 100644 index 000000000..7682d2628 --- /dev/null +++ b/tests/primitives/conftest.py @@ -0,0 +1,65 @@ +import time + +import pytest +from dexbot.orderengines.bitshares_engine import BitsharesOrderEngine + + +@pytest.fixture(scope='module') +def worker_name(): + return 'primitive' + + +@pytest.fixture(scope='session') +def assets(create_asset): + """ Create some assets with different precision + """ + create_asset('BASEA', 3) + create_asset('QUOTEA', 8) + + +@pytest.fixture(scope='module') +def base_account(assets, prepare_account): + """ Factory to generate random account with pre-defined balances + """ + + def func(): + account = prepare_account({'BASEA': 10000, 'QUOTEA': 100}) + return account + + return func + + +@pytest.fixture(scope='module') +def account(base_account): + """ Prepare worker account with some balance + """ + return base_account() + + +@pytest.fixture() +def config(bitshares, account, worker_name): + """ Define worker's config with variable assets + + This fixture should be function-scoped to use new fresh bitshares account for each test + """ + worker_name = worker_name + config = { + 'node': '{}'.format(bitshares.rpc.url), + 'workers': { + worker_name: { + 'account': '{}'.format(account), + 'fee_asset': 'TEST', + 'market': 'QUOTEA/BASEA', + 'module': 'dexbot.strategies.base', + } + }, + } + return config + + +@pytest.fixture() +def worker(worker_name, config, bitshares): + worker = BitsharesOrderEngine(worker_name, config=config, bitshares_instance=bitshares) + yield worker + worker.cancel_all_orders() + time.sleep(1.1) diff --git a/tests/primitives/test_bitshares_orderengine.py b/tests/primitives/test_bitshares_orderengine.py new file mode 100644 index 000000000..fef1a97ba --- /dev/null +++ b/tests/primitives/test_bitshares_orderengine.py @@ -0,0 +1,13 @@ +import logging + +log = logging.getLogger("dexbot") +log.setLevel(logging.DEBUG) + + +def test_init(worker): + pass + + +def test_place_market_sell_order(worker): + worker.place_market_sell_order(1, 1) + assert len(worker.own_orders) == 1 From a9e578076edde0bca95f969960ac1989f92c0ee3 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 5 Apr 2020 22:48:33 +0500 Subject: [PATCH 1788/1846] Refactor __init__ of StrategyBase and BitsharesOrderEngine --- dexbot/orderengines/bitshares_engine.py | 24 ++++++-- dexbot/strategies/base.py | 58 +------------------ tests/primitives/conftest.py | 13 ++++- .../primitives/test_bitshares_orderengine.py | 7 +++ tests/primitives/test_strategybase.py | 15 +++++ 5 files changed, 53 insertions(+), 64 deletions(-) create mode 100644 tests/primitives/test_strategybase.py diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index 516165d94..b6e1ce428 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -6,6 +6,7 @@ import bitshares.exceptions import bitsharesapi import bitsharesapi.exceptions +from bitshares.account import Account from bitshares.amount import Amount, Asset from bitshares.dex import Dex from bitshares.instance import shared_bitshares_instance @@ -43,7 +44,6 @@ def __init__( _market=None, fee_asset_symbol=None, bitshares_instance=None, - bitshares_bundle=None, *args, **kwargs ): @@ -68,8 +68,10 @@ def __init__( else: self.config = Config.get_worker_config_file(name) - self._market = _market - self._account = _account + # Get worker's parameters from the config + self.worker = config["workers"][name] + self._market = _market or Market(self.worker["market"], bitshares_instance=self.bitshares) + self._account = _account or Account(self.worker["account"], full=True, bitshares_instance=self.bitshares) # Recheck flag - Tell the strategy to check for updated orders self.recheck_orders = False @@ -77,7 +79,17 @@ def __init__( # Count of orders to be fetched from the API self.fetch_depth = 8 - self.fee_asset = fee_asset_symbol + # Set fee asset + fee_asset_symbol = fee_asset_symbol or self.worker.get('fee_asset') + + if fee_asset_symbol: + try: + self.fee_asset = Asset(fee_asset_symbol, bitshares_instance=self.bitshares) + except bitshares.exceptions.AssetDoesNotExistsException: + self.fee_asset = Asset('1.3.0', bitshares_instance=self.bitshares) + else: + # If there is no fee asset, use BTS + self.fee_asset = Asset('1.3.0', bitshares_instance=self.bitshares) # CER cache self.core_exchange_rate = None @@ -86,7 +98,7 @@ def __init__( self.ticker = self._market.ticker # Settings for bitshares instance - self.bitshares.bundle = bitshares_bundle + self.bitshares.bundle = bool(self.worker.get("bundle", False)) # Disabled flag - this flag can be flipped to True by a worker and will be reset to False after reset only self.disabled = False @@ -97,6 +109,8 @@ def __init__( # buy/sell actions will return order id by default self.returnOrderId = 'head' + self.log = logging.LoggerAdapter(logging.getLogger('dexbot.orderengine'), {}) + def _callbackPlaceFillOrders(self, d): """ This method distinguishes notifications caused by Matched orders from those caused by placed orders """ diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 1b1b8b5f4..15d76d9cb 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -3,18 +3,12 @@ import time from datetime import datetime -import bitshares.exceptions -from bitshares.account import Account -from bitshares.amount import Asset -from bitshares.instance import shared_bitshares_instance -from bitshares.market import Market from dexbot.config import Config from dexbot.orderengines.bitshares_engine import BitsharesOrderEngine from dexbot.pricefeeds.bitshares_feed import BitsharesPriceFeed from dexbot.qt_queue.idle_queue import idle_add from dexbot.storage import Storage from dexbot.strategies.config_parts.base_config import BaseConfig -from events import Events # Number of maximum retries used to retry action before failing MAX_TRIES = 3 @@ -104,14 +98,7 @@ def __init__( **kwargs ): - # BitShares instance - self.bitshares = bitshares_instance or shared_bitshares_instance() - - # Storage - Storage.__init__(self, name) - - # Events - Events.__init__(self) + BitsharesOrderEngine.__init__(self, name, config=config, *args, **kwargs) if ontick: self.ontick += ontick @@ -136,56 +123,13 @@ def __init__( else: self.config = config = Config.get_worker_config_file(name) - # Get worker's parameters from the config - self.worker = config["workers"][name] - - # Recheck flag - Tell the strategy to check for updated orders - self.recheck_orders = False - - # Count of orders to be fetched from the API - self.fetch_depth = 8 - # What percent of balance the worker should use self.operational_percent_quote = self.worker.get('operational_percent_quote', 0) / 100 self.operational_percent_base = self.worker.get('operational_percent_base', 0) / 100 - # Get Bitshares account and market for this worker - self._account = Account(self.worker["account"], full=True, bitshares_instance=self.bitshares) - self._market = Market(config["workers"][name]["market"], bitshares_instance=self.bitshares) - - # Set fee asset - fee_asset_symbol = self.worker.get('fee_asset') - - if fee_asset_symbol: - try: - self.fee_asset = Asset(fee_asset_symbol, bitshares_instance=self.bitshares) - except bitshares.exceptions.AssetDoesNotExistsException: - self.fee_asset = Asset('1.3.0', bitshares_instance=self.bitshares) - else: - # If there is no fee asset, use BTS - self.fee_asset = Asset('1.3.0', bitshares_instance=self.bitshares) - - # CER cache - self.core_exchange_rate = None - - # Ticker - self.ticker = self._market.ticker - - # 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 will be reset to False after reset only - self.disabled = False - # Initial value for check_last_run decorator in dexbot/decorators.py self.last_check = datetime(1970, 1, 1) - # Order expiration time in seconds - self.expiration = 60 * 60 * 24 * 365 * 5 - - # buy/sell actions will return order id by default - self.returnOrderId = 'head' - # A private logger that adds worker identify data to the LogRecord self.log = logging.LoggerAdapter( logging.getLogger('dexbot.per_worker'), diff --git a/tests/primitives/conftest.py b/tests/primitives/conftest.py index 7682d2628..f5e0911c7 100644 --- a/tests/primitives/conftest.py +++ b/tests/primitives/conftest.py @@ -2,6 +2,7 @@ import pytest from dexbot.orderengines.bitshares_engine import BitsharesOrderEngine +from dexbot.strategies.base import StrategyBase @pytest.fixture(scope='module') @@ -23,7 +24,7 @@ def base_account(assets, prepare_account): """ def func(): - account = prepare_account({'BASEA': 10000, 'QUOTEA': 100}) + account = prepare_account({'BASEA': 10000, 'QUOTEA': 100, 'TEST': 1000}) return account return func @@ -58,8 +59,16 @@ def config(bitshares, account, worker_name): @pytest.fixture() -def worker(worker_name, config, bitshares): +def orderengine(worker_name, config, bitshares): worker = BitsharesOrderEngine(worker_name, config=config, bitshares_instance=bitshares) yield worker worker.cancel_all_orders() time.sleep(1.1) + + +@pytest.fixture() +def strategybase(worker_name, config, bitshares): + worker = StrategyBase(worker_name, config=config, bitshares_instance=bitshares) + yield worker + worker.cancel_all_orders() + time.sleep(1.1) diff --git a/tests/primitives/test_bitshares_orderengine.py b/tests/primitives/test_bitshares_orderengine.py index fef1a97ba..38bc63350 100644 --- a/tests/primitives/test_bitshares_orderengine.py +++ b/tests/primitives/test_bitshares_orderengine.py @@ -1,9 +1,16 @@ import logging +import pytest + log = logging.getLogger("dexbot") log.setLevel(logging.DEBUG) +@pytest.fixture() +def worker(orderengine): + return orderengine + + def test_init(worker): pass diff --git a/tests/primitives/test_strategybase.py b/tests/primitives/test_strategybase.py new file mode 100644 index 000000000..08b0fd36d --- /dev/null +++ b/tests/primitives/test_strategybase.py @@ -0,0 +1,15 @@ +import logging + +import pytest + +log = logging.getLogger("dexbot") +log.setLevel(logging.DEBUG) + + +@pytest.fixture() +def worker(strategybase): + return strategybase + + +def test_init(worker): + pass From ded7e447cf080ed66f8d15cd24b60bbdaa538e14 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 5 Apr 2020 22:52:02 +0500 Subject: [PATCH 1789/1846] Mark primitives __init__ tests as mandatory --- tests/primitives/test_bitshares_orderengine.py | 1 + tests/primitives/test_strategybase.py | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/primitives/test_bitshares_orderengine.py b/tests/primitives/test_bitshares_orderengine.py index 38bc63350..f38daaaa0 100644 --- a/tests/primitives/test_bitshares_orderengine.py +++ b/tests/primitives/test_bitshares_orderengine.py @@ -11,6 +11,7 @@ def worker(orderengine): return orderengine +@pytest.mark.mandatory def test_init(worker): pass diff --git a/tests/primitives/test_strategybase.py b/tests/primitives/test_strategybase.py index 08b0fd36d..dc5227478 100644 --- a/tests/primitives/test_strategybase.py +++ b/tests/primitives/test_strategybase.py @@ -11,5 +11,6 @@ def worker(strategybase): return strategybase +@pytest.mark.mandatory def test_init(worker): pass From 2bced964616b43f307c5479d2fdbd49992a176e9 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 5 Apr 2020 23:12:03 +0500 Subject: [PATCH 1790/1846] Add test for invert= in place_market_sell_order() --- tests/primitives/test_bitshares_orderengine.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/primitives/test_bitshares_orderengine.py b/tests/primitives/test_bitshares_orderengine.py index f38daaaa0..1fdfc62c7 100644 --- a/tests/primitives/test_bitshares_orderengine.py +++ b/tests/primitives/test_bitshares_orderengine.py @@ -19,3 +19,6 @@ def test_init(worker): def test_place_market_sell_order(worker): worker.place_market_sell_order(1, 1) assert len(worker.own_orders) == 1 + + order = worker.place_market_sell_order(1, 10, returnOrderId=True, invert=True) + assert order['price'] == 10 From 776c4d778313401774be653619a7d7e7fc23e2f4 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 6 Apr 2020 23:06:02 +0500 Subject: [PATCH 1791/1846] Add more pre-commit hooks --- .isort.cfg | 1 + .pre-commit-config.yaml | 50 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/.isort.cfg b/.isort.cfg index a7fcfaf1e..fd3f22f66 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -2,3 +2,4 @@ line_length = 120 multi_line_output = 3 include_trailing_comma = True +known_third_party = Crypto,PyQt5,alembic,appdirs,bitshares,bitsharesapi,bitsharesbase,ccxt,click,docker,events,grapheneapi,graphenecommon,pytest,pywaves,requests,ruamel,setuptools,sqlalchemy,uptick,websocket diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 16b527296..3220eebef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,15 +13,55 @@ repos: - id: check-yaml - id: end-of-file-fixer - id: no-commit-to-branch - - id: flake8 + - id: debug-statements + - id: check-merge-conflict + +- repo: https://github.com/ambv/black + rev: 19.10b0 + hooks: + - id: black + language_version: python3 + +- repo: https://github.com/myint/docformatter + rev: v1.3.1 + hooks: + - id: docformatter + args: [-i, --wrap-summaries=120, --wrap-descriptions=120, --pre-summary-newline] + +- repo: https://github.com/humitos/mirrors-autoflake + rev: v1.1 + hooks: + - id: autoflake + args: ['--in-place', '--remove-all-unused-imports', '--remove-unused-variable'] + +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.7.9 + hooks: + - id: flake8 + additional_dependencies: [ + 'pep8-naming', + 'flake8-comprehensions', + 'flake8-bugbear', + 'flake8-mutable', + 'flake8-pytest-style', + 'flake8-variables-names', + 'flake8-class-attributes-order', + 'dlint', + ] + +- repo: https://github.com/asottile/seed-isort-config + rev: v2.1.0 + hooks: + - id: seed-isort-config - repo: https://github.com/timothycrosley/isort rev: 4.3.21 hooks: - id: isort -- repo: https://github.com/ambv/black - rev: 19.10b0 +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.770 hooks: - - id: black - language_version: python3 + - id: mypy + args: [--ignore-missing-imports, --disallow-incomplete-defs] + exclude: 'tests' From 94b9d1b0bb285858e35226ed49102a9612f6d6b8 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 6 Apr 2020 23:55:00 +0500 Subject: [PATCH 1792/1846] Apply autoflake --- dexbot/strategies/staggered_orders.py | 2 -- dexbot/strategies/strategy_template.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c9860c6d9..c8fcf7518 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -2196,13 +2196,11 @@ def error(self, *args, **kwargs): def pause(self): """ Override pause() """ - pass def purge(self): """ We are not cancelling orders on save/remove worker from the GUI TODO: don't work yet because worker removal is happening via BaseStrategy staticmethod """ - pass def tick(self, d): """ Ticks come in on every block """ diff --git a/dexbot/strategies/strategy_template.py b/dexbot/strategies/strategy_template.py index 1b55069d4..5263b657f 100644 --- a/dexbot/strategies/strategy_template.py +++ b/dexbot/strategies/strategy_template.py @@ -123,7 +123,6 @@ def maintain_strategy(self): def check_orders(self, *args, **kwargs): """ """ - pass def error(self, *args, **kwargs): """ Defines what happens when error occurs """ @@ -131,7 +130,6 @@ def error(self, *args, **kwargs): def pause(self): """ Override pause() in StrategyBase """ - pass def tick(self, d): """ Ticks come in on every block """ From 63bc7992c13c6b01718daa6f04e57663a29fa67c Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 6 Apr 2020 23:56:35 +0500 Subject: [PATCH 1793/1846] Run docformatter on all files --- dexbot/cli.py | 26 +- dexbot/cli_conf.py | 73 +-- dexbot/config.py | 63 +- dexbot/config_validator.py | 55 +- dexbot/controllers/main_controller.py | 23 +- dexbot/controllers/settings_controller.py | 33 +- dexbot/controllers/strategy_controller.py | 6 +- dexbot/controllers/worker_controller.py | 20 +- .../controllers/worker_details_controller.py | 13 +- dexbot/decorators.py | 6 +- dexbot/helper.py | 36 +- dexbot/migrations/env.py | 11 +- .../d1e6672520b2_extend_orders_table.py | 4 +- dexbot/node_manager.py | 17 +- dexbot/orderengines/bitshares_engine.py | 295 +++++---- dexbot/pricefeeds/bitshares_feed.py | 147 +++-- dexbot/storage.py | 85 +-- dexbot/strategies/base.py | 105 ++-- dexbot/strategies/config_parts/base_config.py | 30 +- .../config_parts/relative_config.py | 30 +- .../config_parts/staggered_config.py | 29 +- .../config_parts/strategy_config.py | 19 +- dexbot/strategies/echo.py | 65 +- dexbot/strategies/external_feeds/ccxt_feed.py | 2 +- .../strategies/external_feeds/price_feed.py | 4 +- dexbot/strategies/king_of_the_hill.py | 66 +- dexbot/strategies/relative_orders.py | 88 +-- dexbot/strategies/staggered_orders.py | 575 +++++++++--------- dexbot/strategies/strategy_template.py | 67 +- dexbot/styles.py | 3 +- dexbot/views/errors.py | 1 + dexbot/views/strategy_form.py | 6 +- dexbot/views/worker_item.py | 3 +- dexbot/whiptail.py | 10 +- dexbot/worker.py | 18 +- tests/conftest.py | 61 +- tests/gecko_test.py | 4 +- tests/migrations/test_migrations.py | 6 +- tests/strategies/king_of_the_hill/conftest.py | 47 +- .../king_of_the_hill/test_king_of_the_hill.py | 62 +- tests/strategies/relative_orders/conftest.py | 35 +- .../relative_orders/test_relative_orders.py | 39 +- tests/strategies/staggered_orders/conftest.py | 92 ++- .../staggered_orders/test_pybitshares.py | 3 +- .../test_staggered_orders_complex.py | 239 ++++---- .../test_staggered_orders_highlevel.py | 97 ++- .../test_staggered_orders_init.py | 6 +- .../test_staggered_orders_lowlevel.py | 3 +- .../test_staggered_orders_mwsa.py | 12 +- .../test_staggered_orders_unittests.py | 3 +- tests/test_measure_latency.py | 6 +- tests/test_worker_infrastructure.py | 3 +- 52 files changed, 1369 insertions(+), 1383 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index e70b43c69..1c9cb7c75 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -64,9 +64,7 @@ def main(ctx, **kwargs): @click.pass_context @reset_nodes def resetnodes(ctx): - """ - Reset nodes to the default list, use -s option to sort - """ + """Reset nodes to the default list, use -s option to sort.""" log.info("Resetting node list in config.yml to default list") log.info("To sort nodes by timeout, use: `dexbot-cli -s 2 resetnodes`") @@ -78,8 +76,7 @@ def resetnodes(ctx): @unlock @verbose def run(ctx): - """ Continuously run the worker - """ + """Continuously run the worker.""" if ctx.obj['pidfile']: with open(ctx.obj['pidfile'], 'w') as fd: fd.write(str(os.getpid())) @@ -120,8 +117,7 @@ def run(ctx): @chain @unlock def runservice(): - """ Continuously run the worker as a service - """ + """Continuously run the worker as a service.""" if dexbot_service_running(): click.echo("Stopping dexbot daemon") os.system('systemctl --user stop dexbot') @@ -139,8 +135,7 @@ def runservice(): @chain @unlock def configure(ctx): - """ Interactively configure dexbot - """ + """Interactively configure dexbot.""" # Make sure the dexbot service isn't running while we do the config edits if dexbot_service_running(): click.echo("Stopping dexbot daemon") @@ -190,8 +185,7 @@ def cancel(ctx, market, account): @click.argument('worker_name') def drop_state(worker_name): - """ Drop state of the worker (sqlite data) - """ + """Drop state of the worker (sqlite data)""" click.echo('Dropping state for {}'.format(worker_name)) Storage.clear_worker_data(worker_name) time.sleep(1) @@ -202,10 +196,12 @@ def worker_job(worker, job): if __name__ == '__main__': - """ Add freeze_support for when a program which uses multiprocessing (node_manager) has been - frozen to produce a Windows executable. If the freeze_support() line is omitted - then trying to run the frozen executable will raise RuntimeError. Calling - freeze_support() has no effect when invoked on any operating system other than Windows. + """ + Add freeze_support for when a program which uses multiprocessing (node_manager) has been frozen to produce a Windows + executable. + + If the freeze_support() line is omitted then trying to run the frozen executable will raise RuntimeError. Calling + freeze_support() has no effect when invoked on any operating system other than Windows. """ freeze_support() main() diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index a946b4a2b..3394fb887 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -1,8 +1,7 @@ """ -A module to provide an interactive text-based tool for dexbot configuration -The result is dexbot can be run without having to hand-edit config files. -If systemd is detected it will offer to install a user service unit (under ~/.local/share/systemd) -This requires a per-user systemd process to be running +A module to provide an interactive text-based tool for dexbot configuration The result is dexbot can be run without +having to hand-edit config files. If systemd is detected it will offer to install a user service unit (under +~/.local/share/systemd) This requires a per-user systemd process to be running. Requires the 'whiptail' tool for text-based configuration (so UNIX only) if not available, falls back to a line-based configurator ("NoWhiptail") @@ -66,17 +65,17 @@ def select_choice(current, choices): - """ For the radiolist, get us a list with the current value selected - """ + """For the radiolist, get us a list with the current value selected.""" return [(tag, text, (current == tag and "ON") or "OFF") for tag, text in choices] def process_config_element(element, whiptail, worker_config): - """ Process an item of configuration metadata, display a widget as appropriate + """ + Process an item of configuration metadata, display a widget as appropriate. - :param base_config.ConfigElement element: config element - :param whiptail.Whiptail whiptail: instance of Whiptail or NoWhiptail - :param collections.OrderedDict worker_config: the config dictionary for this worker + :param base_config.ConfigElement element: config element + :param whiptail.Whiptail whiptail: instance of Whiptail or NoWhiptail + :param collections.OrderedDict worker_config: the config dictionary for this worker """ if element.description: title = '{} - {}'.format(element.title, element.description) @@ -125,8 +124,7 @@ def process_config_element(element, whiptail, worker_config): def dexbot_service_running(): - """ Return True if dexbot service is running - """ + """Return True if dexbot service is running.""" cmd = 'systemctl --user status dexbot' output = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) for line in output.stdout.readlines(): @@ -136,10 +134,11 @@ def dexbot_service_running(): def setup_systemd(whiptail, config): - """ Setup systemd unit to auto-start dexbot + """ + Setup systemd unit to auto-start dexbot. - :param whiptail.Whiptail whiptail: instance of Whiptail or NoWhiptail - :param dexbot.config.Config config: dexbot config + :param whiptail.Whiptail whiptail: instance of Whiptail or NoWhiptail + :param dexbot.config.Config config: dexbot config """ if not os.path.exists("/etc/systemd"): return # No working systemd @@ -175,11 +174,12 @@ def setup_systemd(whiptail, config): def get_strategy_tag(strategy_class): - """ Obtain tag for a strategy + """ + Obtain tag for a strategy. - :param str strategy_class: strategy class name, example: dexbot.strategies.foo_bar + :param str strategy_class: strategy class name, example: dexbot.strategies.foo_bar - It may seems that tags may be common across strategies, but it is not. Every strategy must use unique tag. + It may seems that tags may be common across strategies, but it is not. Every strategy must use unique tag. """ for strategy in STRATEGIES: if strategy_class == strategy['class']: @@ -188,11 +188,12 @@ def get_strategy_tag(strategy_class): def configure_worker(whiptail, worker_config, validator): - """ Single worker configurator + """ + Single worker configurator. - :param whiptail.Whiptail whiptail: instance of Whiptail or NoWhiptail - :param collections.OrderedDict worker_config: tohe config dictionary for this worker - :param dexbot.config_validator.ConfigValidator validator: dexbot config validator + :param whiptail.Whiptail whiptail: instance of Whiptail or NoWhiptail + :param collections.OrderedDict worker_config: tohe config dictionary for this worker + :param dexbot.config_validator.ConfigValidator validator: dexbot config validator """ # By default always editing editing = True @@ -263,9 +264,10 @@ def configure_worker(whiptail, worker_config, validator): def configure_dexbot(config, ctx): - """ Main `cli configure` entrypoint + """ + Main `cli configure` entrypoint. - :param dexbot.config.Config config: dexbot config + :param dexbot.config.Config config: dexbot config """ whiptail = get_whiptail('DEXBot configure') workers = config.get('workers', {}) @@ -391,11 +393,12 @@ def configure_dexbot(config, ctx): def add_account(validator, whiptail): - """ "Add account" dialog + """ + "Add account" dialog. - :param dexbot.config_validator.ConfigValidator validator: dexbot config validator - :param whiptail.Whiptail whiptail: instance of Whiptail or NoWhiptail - :return str: user-supplied account name + :param dexbot.config_validator.ConfigValidator validator: dexbot config validator + :param whiptail.Whiptail whiptail: instance of Whiptail or NoWhiptail + :return str: user-supplied account name """ account = whiptail.prompt("Your Account Name") @@ -420,10 +423,11 @@ def add_account(validator, whiptail): def del_account(whiptail, bitshares_instance): - """ Delete account from the wallet + """ + Delete account from the wallet. - :param whiptail.Whiptail whiptail: instance of Whiptail or NoWhiptail - :param bitshares.BitShares bitshares_instance: an instance of BitShares class + :param whiptail.Whiptail whiptail: instance of Whiptail or NoWhiptail + :param bitshares.BitShares bitshares_instance: an instance of BitShares class """ account = whiptail.prompt("Account Name") wallet = bitshares_instance.wallet @@ -431,11 +435,12 @@ def del_account(whiptail, bitshares_instance): def list_accounts(bitshares_instance): - """ Get all accounts installed in local wallet in format suitable for Whiptail.menu() + """ + Get all accounts installed in local wallet in format suitable for Whiptail.menu() - Returning format is compatible both with Whiptail and NoWhiptail. + Returning format is compatible both with Whiptail and NoWhiptail. - :return: list of tuples (int, 'account_name - key_type') + :return: list of tuples (int, 'account_name - key_type') """ accounts = [] pubkeys = bitshares_instance.wallet.getPublicKeys(current=True) diff --git a/dexbot/config.py b/dexbot/config.py index f44247db9..e73b93d9b 100644 --- a/dexbot/config.py +++ b/dexbot/config.py @@ -13,9 +13,11 @@ class Config(dict): def __init__(self, config=None, path=None): - """ Creates or loads the config file based on if it exists. - :param dict config: data used to create the config file - :param str path: path to the config file + """ + Creates or loads the config file based on if it exists. + + :param dict config: data used to create the config file + :param str path: path to the config file """ super().__init__() if path: @@ -64,13 +66,11 @@ def default_data(self): @property def workers_data(self): - """ Returns dict of all the workers data - """ + """Returns dict of all the workers data.""" return self._config['workers'] def dict(self): - """ Returns a dict instance of the stored data - """ + """Returns a dict instance of the stored data.""" return self._config @staticmethod @@ -105,8 +105,10 @@ def refresh_config(self): @staticmethod def get_worker_config_file(worker_name, path=None): - """ Returns config file data with only the data from a specific worker. - Config loaded from a file + """ + Returns config file data with only the data from a specific worker. + + Config loaded from a file """ if not path: path = DEFAULT_CONFIG_FILE @@ -118,8 +120,10 @@ def get_worker_config_file(worker_name, path=None): return config def get_worker_config(self, worker_name): - """ Returns config file data with only the data from a specific worker. - Config loaded from memory + """ + Returns config file data with only the data from a specific worker. + + Config loaded from memory """ config = self._config.copy() config['workers'] = OrderedDict({worker_name: config['workers'][worker_name]}) @@ -167,30 +171,31 @@ def construct_mapping(mapping_loader, node): @staticmethod def assets_intersections(config): - """ Collect intersections of assets on the same account across multiple workers + """ + Collect intersections of assets on the same account across multiple workers. - :return: defaultdict instance representing dict with intersections + :return: defaultdict instance representing dict with intersections - The goal of calculating assets intersections is to be able to use single account on multiple workers and - trade some common assets. For example, trade BTS/USD, BTC/BTS, ETH/BTC markets on same account. + The goal of calculating assets intersections is to be able to use single account on multiple workers and + trade some common assets. For example, trade BTS/USD, BTC/BTS, ETH/BTC markets on same account. - Configuration variable `operational_percent_xxx` defines what percent of total account balance should be - available for the worker. It may be set or omitted. + Configuration variable `operational_percent_xxx` defines what percent of total account balance should be + available for the worker. It may be set or omitted. - The logic of splitting balance is following: workers who define `operational_percent_xxx` will take this - defined percent, and remaining workers will just split the remaining balance between each other. For - example, 3 workers with 30% 30% 30%, and 2 workers with 0. These 2 workers will take the remaining `(100 - - 3*30) / 2 = 5`. + The logic of splitting balance is following: workers who define `operational_percent_xxx` will take this + defined percent, and remaining workers will just split the remaining balance between each other. For + example, 3 workers with 30% 30% 30%, and 2 workers with 0. These 2 workers will take the remaining `(100 - + 3*30) / 2 = 5`. - Example return as a dict + Example return as a dict - .. code-block:: python + .. code-block:: python - {'foo': {'RUBLE': {'sum_pct': 0, 'zero_workers': 0}, - 'USD': {'sum_pct': 0, 'zero_workers': 0}, - 'CNY': {'sum_pct': 0, 'zero_workers': 0} - } - } + {'foo': {'RUBLE': {'sum_pct': 0, 'zero_workers': 0}, + 'USD': {'sum_pct': 0, 'zero_workers': 0}, + 'CNY': {'sum_pct': 0, 'zero_workers': 0} + } + } """ def update_data(asset, operational_percent): @@ -229,7 +234,7 @@ def tree(): @property def node_list(self): - """ A pre-defined list of Bitshares nodes. """ + """A pre-defined list of Bitshares nodes.""" return [ "wss://bitshares.openledger.info/ws", "wss://openledger.hk/ws", diff --git a/dexbot/config_validator.py b/dexbot/config_validator.py index aca0c0075..829a3aed5 100644 --- a/dexbot/config_validator.py +++ b/dexbot/config_validator.py @@ -6,10 +6,11 @@ class ConfigValidator: - """ Config validation methods + """ + Config validation methods. - :param dexbot.config.Config config: dexbot config - :param bitshares.BitShares: BitShares instance + :param dexbot.config.Config config: dexbot config + :param bitshares.BitShares: BitShares instance """ def __init__(self, config, bitshares_instance): @@ -17,9 +18,10 @@ def __init__(self, config, bitshares_instance): self.bitshares = bitshares_instance or shared_bitshares_instance() def validate_account_name(self, account): - """ Check whether bitshares account exists + """ + Check whether bitshares account exists. - :param str account: bitshares account name + :param str account: bitshares account name """ if not account: return False @@ -30,10 +32,11 @@ def validate_account_name(self, account): return False def validate_private_key(self, account, private_key): - """ Check whether private key is associated with account + """ + Check whether private key is associated with account. - :param str account: bitshares account name - :param str private_key: private key + :param str account: bitshares account name + :param str private_key: private key """ wallet = self.bitshares.wallet if not private_key: @@ -59,10 +62,11 @@ def validate_private_key(self, account, private_key): return False def validate_private_key_type(self, account, private_key): - """ Check whether private key type is "active" or "owner" + """ + Check whether private key type is "active" or "owner". - :param str account: bitshares account name - :param str private_key: private key + :param str account: bitshares account name + :param str private_key: private key """ account = Account(account) pubkey = format(PrivateKey(private_key).pubkey, self.bitshares.prefix) @@ -72,10 +76,11 @@ def validate_private_key_type(self, account, private_key): return True def validate_worker_name(self, worker_name, old_worker_name=None): - """ Check whether worker name is unique or not + """ + Check whether worker name is unique or not. - :param str worker_name: name of the new worker - :param str old_worker_name: old name of the worker + :param str worker_name: name of the new worker + :param str old_worker_name: old name of the worker """ if old_worker_name != worker_name: worker_names = self.config.workers_data.keys() @@ -86,9 +91,10 @@ def validate_worker_name(self, worker_name, old_worker_name=None): return True def validate_account_not_in_use(self, account): - """ Check whether account is already used for another worker or not + """ + Check whether account is already used for another worker or not. - :param str account: bitshares account name + :param str account: bitshares account name """ workers = self.config.workers_data for worker_name, worker in workers.items(): @@ -97,9 +103,10 @@ def validate_account_not_in_use(self, account): return True def validate_asset(self, asset): - """ Check whether asset is exists on the network + """ + Check whether asset is exists on the network. - :param str asset: asset name + :param str asset: asset name """ try: Asset(asset, bitshares_instance=self.bitshares) @@ -109,17 +116,19 @@ def validate_asset(self, asset): @staticmethod def validate_market(base_asset, quote_asset): - """ Check whether market tickers is not the same + """ + Check whether market tickers is not the same. - :param str base_asset: BASE asset ticker - :param str quote_asset: QUOTE asset ticker + :param str base_asset: BASE asset ticker + :param str quote_asset: QUOTE asset ticker """ return base_asset.lower() != quote_asset.lower() def add_private_key(self, private_key): - """ Add private key into local wallet + """ + Add private key into local wallet. - :param str private_key: private key + :param str private_key: private key """ wallet = self.bitshares.wallet try: diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 1eeb962b4..78a5a834c 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -46,19 +46,21 @@ def __init__(self, config): initialize_data_folders() def set_bitshares_instance(self, bitshares_instance): - """ Set bitshares instance + """ + Set bitshares instance. - :param bitshares_instance: A bitshares instance + :param bitshares_instance: A bitshares instance """ self.bitshares_instance = bitshares_instance set_shared_bitshares_instance(bitshares_instance) def new_bitshares_instance(self, node, retries=-1, expiration=60): - """ Create bitshares instance + """ + Create bitshares instance. - :param retries: Number of retries to connect, -1 default to infinity - :param expiration: Delay in seconds until transactions are supposed to expire - :param list node: Node or a list of nodes + :param retries: Number of retries to connect, -1 default to infinity + :param expiration: Delay in seconds until transactions are supposed to expire + :param list node: Node or a list of nodes """ self.bitshares_instance = BitShares(node, num_retries=retries, expiration=expiration) set_shared_bitshares_instance(self.bitshares_instance) @@ -99,11 +101,12 @@ def remove_worker(self, worker_name): @staticmethod def measure_latency(nodes): - """ Measures latency of first alive node from given nodes in milliseconds + """ + Measures latency of first alive node from given nodes in milliseconds. - :param str,list nodes: Bitshares node address(-es) - :return: int: latency in milliseconds - :raises grapheneapi.exceptions.NumRetriesReached: if failed to find a working node + :param str,list nodes: Bitshares node address(-es) + :return: int: latency in milliseconds + :raises grapheneapi.exceptions.NumRetriesReached: if failed to find a working node """ if isinstance(nodes, str): nodes = [nodes] diff --git a/dexbot/controllers/settings_controller.py b/dexbot/controllers/settings_controller.py index 3ca924109..e70e767dd 100644 --- a/dexbot/controllers/settings_controller.py +++ b/dexbot/controllers/settings_controller.py @@ -9,8 +9,7 @@ def __init__(self, view): self.view = view def add_node(self): - """ Add item in the widget tree list - """ + """Add item in the widget tree list.""" item = QTreeWidgetItem(self.view.nodes_tree_widget) item.setText(0, '') item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) @@ -22,8 +21,7 @@ def add_node(self): self.view.notification_label.setText('Unsaved changes detected; Node added.') def move_up(self): - """ Move item up in the widget tree list - """ + """Move item up in the widget tree list.""" current_index = self.view.nodes_tree_widget.indexOfTopLevelItem(self.view.nodes_tree_widget.currentItem()) # This prevents moving item out of the list @@ -39,8 +37,7 @@ def move_up(self): self.view.notification_label.setText('Unsaved changes detected; List order has changed.') def move_down(self): - """ Move item down in the widget tree list - """ + """Move item down in the widget tree list.""" current_index = self.view.nodes_tree_widget.indexOfTopLevelItem(self.view.nodes_tree_widget.currentItem()) # This prevents moving item out of the list @@ -56,9 +53,10 @@ def move_down(self): self.view.notification_label.setText('Unsaved changes detected; List order has changed.') def save_settings(self): - """ Save items in the tree widget list into the config file and close window + """ + Save items in the tree widget list into the config file and close window. - :returns int: 1 settings saved (accepted) + :returns int: 1 settings saved (accepted) """ nodes = [] @@ -74,8 +72,7 @@ def save_settings(self): self.view.accept() def remove_node(self): - """ Remove item from the widget tree list - """ + """Remove item from the widget tree list.""" node = self.view.nodes_tree_widget.currentItem() if node: @@ -85,9 +82,10 @@ def remove_node(self): self.view.notification_label.setText('Unsaved changes detected; Node removed.') def initialize_node_list(self, nodes=None): - """ Populates Tree Widget with nodes + """ + Populates Tree Widget with nodes. - :param nodes: List of nodes that can be applied to the widget instead of getting them from the config file. + :param nodes: List of nodes that can be applied to the widget instead of getting them from the config file. """ # Make sure there are no widgets in the list self.view.nodes_tree_widget.clear() @@ -105,8 +103,7 @@ def initialize_node_list(self, nodes=None): item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) def save_nodes_to_config(self, nodes): - """ Save nodes to the config file - """ + """Save nodes to the config file.""" # Remove empty nodes before saving, this is just to make sure no empty strings end up in config file nodes = self.remove_empty_items(nodes) @@ -126,14 +123,14 @@ def restore_defaults(self): @staticmethod def remove_empty_items(items): - """ Removes empty strings from a list - """ + """Removes empty strings from a list.""" return list(filter(None, items)) @property def nodes(self): - """ Returns nodes list from the config file + """ + Returns nodes list from the config file. - :return: Nodes list + :return: Nodes list """ return self.config.get('node') diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index 220c37f48..10b87eff5 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -2,8 +2,7 @@ class StrategyController: - """ Parent controller for strategies that don't have a custom controller - """ + """Parent controller for strategies that don't have a custom controller.""" def __init__(self, view, configure, worker_controller, worker_data): self.view = view @@ -57,8 +56,7 @@ def values(self): @property def elements(self): - """ Use ConfigElements of the strategy to find the input elements - """ + """Use ConfigElements of the strategy to find the input elements.""" elements = {} types = ( QtWidgets.QDoubleSpinBox, diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index 4a2b38181..71b9b2554 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -20,13 +20,14 @@ def __init__(self, view, bitshares_instance, mode): @property def strategies(self): - """ Defines strategies that are configurable from the GUI. + """ + Defines strategies that are configurable from the GUI. - key: Strategy location in the project - name: The name that is shown in the GUI for user - form_module: If there is custom form module created with QTDesigner + key: Strategy location in the project + name: The name that is shown in the GUI for user + form_module: If there is custom form module created with QTDesigner - :return: List of strategies + :return: List of strategies """ strategies = collections.OrderedDict() strategies['dexbot.strategies.relative_orders'] = { @@ -42,14 +43,15 @@ def strategies(self): @classmethod def get_strategies(cls): - """ Class method for getting the strategies - """ + """Class method for getting the strategies.""" return cls(None, None, None).strategies @staticmethod def get_unique_worker_name(): - """ Returns unique worker name "Worker %n" - %n is the next available index + """ + Returns unique worker name "Worker %n". + + %n is the next available index """ index = 1 workers = Config().workers_data.keys() diff --git a/dexbot/controllers/worker_details_controller.py b/dexbot/controllers/worker_details_controller.py index 1791a1488..4a89bcdbb 100644 --- a/dexbot/controllers/worker_details_controller.py +++ b/dexbot/controllers/worker_details_controller.py @@ -7,20 +7,19 @@ class WorkerDetailsController: def __init__(self, view, worker_name, config): - """ Initializes controller + """ + Initializes controller. - :param view: WorkerDetailsView - :param worker_name: Worker's name - :param config: Worker's config + :param view: WorkerDetailsView + :param worker_name: Worker's name + :param config: Worker's config """ self.view = view self.worker_name = worker_name self.config = config def initialize_worker_data(self): - """ Initializes details view with worker's data - - """ + """Initializes details view with worker's data.""" # Worker information self.view.worker_name.setText(self.worker_name) self.view.worker_account.setText(self.config.get('account')) diff --git a/dexbot/decorators.py b/dexbot/decorators.py index 8d5b2447d..cb9ccbe94 100644 --- a/dexbot/decorators.py +++ b/dexbot/decorators.py @@ -3,8 +3,10 @@ def check_last_run(func): - """ This decorator is intended to be used for control maintain_strategy() execution. It requires self.last_check and - self.check_interval to be set in calling class. + """ + This decorator is intended to be used for control maintain_strategy() execution. + + It requires self.last_check and self.check_interval to be set in calling class. """ @wraps(func) diff --git a/dexbot/helper.py b/dexbot/helper.py index cc6827607..1ffcc4b62 100644 --- a/dexbot/helper.py +++ b/dexbot/helper.py @@ -18,8 +18,7 @@ def mkdir(d): def remove(path): - """ Removes a file or a directory even if they don't exist - """ + """Removes a file or a directory even if they don't exist.""" if os.path.isfile(path): try: os.remove(path) @@ -34,23 +33,23 @@ def remove(path): def truncate(number, decimals): - """ Change the decimal point of a number without rounding + """ + Change the decimal point of a number without rounding. - :param float | number: A float number to be cut down - :param int | decimals: Number of decimals to be left to the float number - :return: Price with specified precision + :param float | number: A float number to be cut down + :param int | decimals: Number of decimals to be left to the float number + :return: Price with specified precision """ return math.floor(number * 10 ** decimals) / 10 ** decimals def get_user_data_directory(): - """ Returns the user data directory path which contains history, sql and logs - """ + """Returns the user data directory path which contains history, sql and logs.""" return user_data_dir(APP_NAME, AUTHOR) def initialize_data_folders(): - """ Creates folders for strategies to store files """ + """Creates folders for strategies to store files.""" user_data_directory = get_user_data_directory() mkdir(os.path.join(user_data_directory, 'graphs')) mkdir(os.path.join(user_data_directory, 'data')) @@ -58,8 +57,7 @@ def initialize_data_folders(): def initialize_orders_log(): - """ Creates .csv log file, adds the headers first time only - """ + """Creates .csv log file, adds the headers first time only.""" data_dir = get_user_data_directory() filename = os.path.join(data_dir, 'history.csv') file = os.path.isfile(filename) @@ -84,13 +82,15 @@ def initialize_orders_log(): import pkg_resources def find_external_strategies(): - """Use setuptools introspection to find third-party strategies the user may have installed. - Packages that provide a strategy should export a setuptools "entry point" (see setuptools docs) - with group "dexbot.strategy", "name" is the display name of the strategy. - Only set the module not any attribute (because it would always be a class called "Strategy") - If you want a handwritten graphical UI, define "Ui_Form" and "StrategyController" in the same module - - yields a 2-tuple: description, module name""" + """ + Use setuptools introspection to find third-party strategies the user may have installed. Packages that provide a + strategy should export a setuptools "entry point" (see setuptools docs) with group "dexbot.strategy", "name" is + the display name of the strategy. Only set the module not any attribute (because it would always be a class + called "Strategy") If you want a handwritten graphical UI, define "Ui_Form" and "StrategyController" in the same + module. + + yields a 2-tuple: description, module name + """ for entry_point in pkg_resources.iter_entry_points("dexbot.strategy"): yield (entry_point.name, entry_point.module_name) diff --git a/dexbot/migrations/env.py b/dexbot/migrations/env.py index 131e3422a..9905a4f47 100644 --- a/dexbot/migrations/env.py +++ b/dexbot/migrations/env.py @@ -22,7 +22,8 @@ def run_migrations_offline(): - """Run migrations in 'offline' mode. + """ + Run migrations in 'offline' mode. This configures the context with just a URL and not an Engine, though an Engine is acceptable @@ -31,7 +32,6 @@ def run_migrations_offline(): Calls to context.execute() here emit the given string to the script output. - """ url = config.get_main_option("sqlalchemy.url") context.configure(url=url, target_metadata=target_metadata, literal_binds=True) @@ -41,11 +41,10 @@ def run_migrations_offline(): def run_migrations_online(): - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. + """ + Run migrations in 'online' mode. + In this scenario we need to create an Engine and associate a connection with the context. """ connectable = engine_from_config( config.get_section(config.config_ini_section), prefix="sqlalchemy.", poolclass=pool.NullPool, diff --git a/dexbot/migrations/versions/d1e6672520b2_extend_orders_table.py b/dexbot/migrations/versions/d1e6672520b2_extend_orders_table.py index 84339fa32..d4137b5af 100644 --- a/dexbot/migrations/versions/d1e6672520b2_extend_orders_table.py +++ b/dexbot/migrations/versions/d1e6672520b2_extend_orders_table.py @@ -1,9 +1,9 @@ -"""extend orders table +""" +extend orders table. Revision ID: d1e6672520b2 Revises: Create Date: 2019-07-29 17:38:09.136485 - """ import sqlalchemy as sa from alembic import op diff --git a/dexbot/node_manager.py b/dexbot/node_manager.py index 929ab5623..e51cad3eb 100644 --- a/dexbot/node_manager.py +++ b/dexbot/node_manager.py @@ -16,8 +16,10 @@ def ping(host, network_timeout=3): - """ Send a ping packet to the specified host, using the system "ping" command. - Covers the Windows, Unix and OSX + """ + Send a ping packet to the specified host, using the system "ping" command. + + Covers the Windows, Unix and OSX """ args = ['ping'] platform_os = platform.system().lower() @@ -44,8 +46,7 @@ def ping(host, network_timeout=3): def wss_test(node, timeout): - """ Test websocket connection to a node - """ + """Test websocket connection to a node.""" try: start = time() wss_create(node, timeout=timeout) @@ -57,8 +58,7 @@ def wss_test(node, timeout): def check_node(node, timeout): - """ Check latency of an individual node - """ + """Check latency of an individual node.""" log.info('# pinging {}'.format(node)) latency = wss_test(node, timeout) node_info = {'Node': node, 'Latency': latency} @@ -66,9 +66,8 @@ def check_node(node, timeout): def get_sorted_nodelist(nodelist, timeout=2): - """ Check all nodes and poll for latency, eliminate nodes with no response, then sort - nodes by increasing latency and return as a list - """ + """Check all nodes and poll for latency, eliminate nodes with no response, then sort nodes by increasing latency and + return as a list.""" print('get_sorted_nodelist max timeout: {}'.format(timeout)) pool_size = mp.cpu_count() * 2 diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index 516165d94..c6972ac2a 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -23,16 +23,16 @@ class BitsharesOrderEngine(Storage, Events): """ - All prices are passed and returned as BASE/QUOTE. - (In the BREAD:USD market that would be USD/BREAD, 2.5 USD / 1 BREAD). - - Buy orders reserve BASE - - Sell orders reserve QUOTE + All prices are passed and returned as BASE/QUOTE. (In the BREAD:USD market that would be USD/BREAD, 2.5 USD / 1 + BREAD). - OrderEngine inherits: - * :class:`dexbot.storage.Storage` : Stores data to sqlite database - * ``Events`` :The websocket endpoint of BitShares has notifications that are subscribed to - and dispatched by dexbot. This uses python's native Events + - Buy orders reserve BASE + - Sell orders reserve QUOTE + OrderEngine inherits: + * :class:`dexbot.storage.Storage` : Stores data to sqlite database + * ``Events`` :The websocket endpoint of BitShares has notifications that are subscribed to + and dispatched by dexbot. This uses python's native Events """ def __init__( @@ -98,8 +98,7 @@ def __init__( self.returnOrderId = 'head' def _callbackPlaceFillOrders(self, d): - """ This method distinguishes notifications caused by Matched orders from those caused by placed orders - """ + """This method distinguishes notifications caused by Matched orders from those caused by placed orders.""" if isinstance(d, FilledOrder): self.onOrderMatched(d) elif isinstance(d, Order): @@ -127,10 +126,11 @@ def _cancel_orders(self, orders): return True def account_total_value(self, return_asset): - """ Returns the total value of the account in given asset + """ + Returns the total value of the account in given asset. - :param string | return_asset: Balance is returned as this asset - :return: float: Value of the account in one asset + :param string | return_asset: Balance is returned as this asset + :return: float: Value of the account in one asset """ total_value = 0 @@ -158,11 +158,12 @@ def account_total_value(self, return_asset): return total_value def balance(self, asset, fee_reservation=0): - """ Return the balance of your worker's account in a specific asset. + """ + Return the balance of your worker's account in a specific asset. - :param string | asset: In what asset the balance is wanted to be returned - :param float | fee_reservation: How much is saved in reserve for the fees - :return: Balance of specific asset + :param string | asset: In what asset the balance is wanted to be returned + :param float | fee_reservation: How much is saved in reserve for the fees + :return: Balance of specific asset """ balance = self._account.balance(asset) @@ -172,8 +173,7 @@ def balance(self, asset, fee_reservation=0): return balance def calculate_order_data(self, order_type, order, amount, price): - """ Reconstructs order data using price and amount - """ + """Reconstructs order data using price and amount.""" quote_asset = Amount(amount, self._market['quote']['symbol'], bitshares_instance=self.bitshares) base_asset = Amount(amount * price, self._market['base']['symbol'], bitshares_instance=self.bitshares) if order_type == 'buy': @@ -190,11 +190,12 @@ def calculate_order_data(self, order_type, order, amount, price): return order def calculate_worker_value(self, unit_of_measure): - """ Returns the combined value of allocated and available BASE and QUOTE. Total value is - measured in "unit_of_measure", which is either BASE or QUOTE symbol. + """ + Returns the combined value of allocated and available BASE and QUOTE. Total value is measured in + "unit_of_measure", which is either BASE or QUOTE symbol. - :param string | unit_of_measure: Asset symbol - :return: Value of the worker as float + :param string | unit_of_measure: Asset symbol + :return: Value of the worker as float """ base_total = 0 quote_total = 0 @@ -226,9 +227,10 @@ def calculate_worker_value(self, unit_of_measure): return base_total + quote_total def cancel_all_orders(self, all_markets=False): - """ Cancel all orders of the worker's market or all markets + """ + Cancel all orders of the worker's market or all markets. - :param bool all_markets: True = cancel orders on all markets, False = cancel only own market + :param bool all_markets: True = cancel orders on all markets, False = cancel only own market """ orders_to_cancel = [] @@ -249,11 +251,12 @@ def cancel_all_orders(self, all_markets=False): self.log.info("Orders canceled") def cancel_orders(self, orders, batch_only=False): - """ Cancel specific order(s) + """ + Cancel specific order(s) - :param list | orders: List of orders to cancel - :param bool | batch_only: Try cancel orders only in batch mode without one-by-one fallback - :return: + :param list | orders: List of orders to cancel + :param bool | batch_only: Try cancel orders only in batch mode without one-by-one fallback + :return: """ if not isinstance(orders, (list, set, tuple)): orders = [orders] @@ -272,14 +275,15 @@ def cancel_orders(self, orders, batch_only=False): return success def count_asset(self, order_ids=None, return_asset=False): - """ Returns the combined amount of the given order ids and the account balance - The amounts are returned in quote and base assets of the market + """ + Returns the combined amount of the given order ids and the account balance The amounts are returned in quote and + base assets of the market. - :param list | order_ids: list of order ids to be added to the balance - :param bool | return_asset: true if returned values should be Amount instances - :return: dict with keys quote and base + :param list | order_ids: list of order ids to be added to the balance + :param bool | return_asset: true if returned values should be Amount instances + :return: dict with keys quote and base - Todo: When would we want the sum of a subset of orders? Why order_ids? Maybe just specify asset? + Todo: When would we want the sum of a subset of orders? Why order_ids? Maybe just specify asset? """ quote = 0 base = 0 @@ -308,11 +312,12 @@ def count_asset(self, order_ids=None, return_asset=False): return {'quote': quote, 'base': base} def get_allocated_assets(self, order_ids=None, return_asset=False): - """ Returns the amount of QUOTE and BASE allocated in orders, and that do not show up in available balance + """ + Returns the amount of QUOTE and BASE allocated in orders, and that do not show up in available balance. - :param list | order_ids: - :param bool | return_asset: - :return: Dictionary of QUOTE and BASE amounts + :param list | order_ids: + :param bool | return_asset: + :return: Dictionary of QUOTE and BASE amounts """ if not order_ids: order_ids = [] @@ -342,10 +347,11 @@ def get_allocated_assets(self, order_ids=None, return_asset=False): return {'quote': quote, 'base': base} def get_highest_own_buy_order(self, orders=None): - """ Returns highest own buy order. + """ + Returns highest own buy order. - :param list | orders: - :return: Highest own buy order by price at the market or None + :param list | orders: + :return: Highest own buy order by price at the market or None """ if not orders: orders = self.get_own_buy_orders() @@ -356,10 +362,11 @@ def get_highest_own_buy_order(self, orders=None): return None def get_lowest_own_sell_order(self, orders=None): - """ Returns lowest own sell order. + """ + Returns lowest own sell order. - :param list | orders: - :return: Lowest own sell order by price at the market + :param list | orders: + :return: Lowest own sell order by price at the market """ if not orders: orders = self.get_own_sell_orders() @@ -370,14 +377,15 @@ def get_lowest_own_sell_order(self, orders=None): return None def get_market_orders(self, depth=1, updated=True): - """ Returns orders from the current market. Orders are sorted by price. + """ + Returns orders from the current market. Orders are sorted by price. - get_limit_orders() call does not have any depth limit. + get_limit_orders() call does not have any depth limit. - :param int | depth: Amount of orders per side will be fetched, default=1 - :param bool | updated: Return updated orders. "Updated" means partially filled orders will represent - remainders and not just initial amounts - :return: Returns a list of orders or None + :param int | depth: Amount of orders per side will be fetched, default=1 + :param bool | updated: Return updated orders. "Updated" means partially filled orders will represent + remainders and not just initial amounts + :return: Returns a list of orders or None """ orders = self.bitshares.rpc.get_limit_orders(self._market['base']['id'], self._market['quote']['id'], depth) if updated: @@ -386,10 +394,11 @@ def get_market_orders(self, depth=1, updated=True): return orders def get_order_cancellation_fee(self, fee_asset): - """ Returns the order cancellation fee in the specified asset. + """ + Returns the order cancellation fee in the specified asset. - :param string | fee_asset: Asset in which the fee is wanted - :return: Cancellation fee as fee asset + :param string | fee_asset: Asset in which the fee is wanted + :return: Cancellation fee as fee asset """ # Get fee fees = self.dex.returnFees() @@ -397,10 +406,11 @@ def get_order_cancellation_fee(self, fee_asset): return self.convert_fee(limit_order_cancel['fee'], fee_asset) def get_order_creation_fee(self, fee_asset): - """ Returns the cost of creating an order in the asset specified + """ + Returns the cost of creating an order in the asset specified. - :param fee_asset: QUOTE, BASE, BTS, or any other - :return: + :param fee_asset: QUOTE, BASE, BTS, or any other + :return: """ # Get fee fees = self.dex.returnFees() @@ -408,9 +418,10 @@ def get_order_creation_fee(self, fee_asset): return self.convert_fee(limit_order_create['fee'], fee_asset) def get_own_buy_orders(self, orders=None): - """ Get own buy orders from current market, or from a set of orders passed for this function. + """ + Get own buy orders from current market, or from a set of orders passed for this function. - :return: List of buy orders + :return: List of buy orders """ if not orders: # List of orders was not given so fetch everything from the market @@ -419,9 +430,10 @@ def get_own_buy_orders(self, orders=None): return self.filter_buy_orders(orders) def get_own_sell_orders(self, orders=None): - """ Get own sell orders from current market + """ + Get own sell orders from current market. - :return: List of sell orders + :return: List of sell orders """ if not orders: # List of orders was not given so fetch everything from the market @@ -430,9 +442,10 @@ def get_own_sell_orders(self, orders=None): return self.filter_sell_orders(orders) def get_own_spread(self): - """ Returns the difference between own closest opposite orders. + """ + Returns the difference between own closest opposite orders. - :return: float or None: Own spread + :return: float or None: Own spread """ try: # Try fetching own orders @@ -446,9 +459,10 @@ def get_own_spread(self): return actual_spread def get_updated_order(self, order_id): - """ Tries to get the updated order from the API. Returns None if the order doesn't exist + """ + Tries to get the updated order from the API. Returns None if the order doesn't exist. - :param str|dict order_id: blockchain Order object or id of the order + :param str|dict order_id: blockchain Order object or id of the order """ if isinstance(order_id, dict): order_id = order_id['id'] @@ -472,9 +486,10 @@ def get_updated_order(self, order_id): return Order(updated_order, bitshares_instance=self.bitshares) def execute(self): - """ Execute a bundle of operations + """ + Execute a bundle of operations. - :return: dict: transaction + :return: dict: transaction """ self.bitshares.blocking = "head" r = self.bitshares.txbuffer.broadcast() @@ -482,10 +497,11 @@ def execute(self): return r def is_buy_order(self, order): - """ Check whether an order is buy order + """ + Check whether an order is buy order. - :param dict | order: dict or Order object - :return bool + :param dict | order: dict or Order object + :return bool """ # Check if the order is buy order, by comparing asset symbol of the order and the market if order['base']['symbol'] == self._market['base']['symbol']: @@ -494,9 +510,10 @@ def is_buy_order(self, order): return False def is_current_market(self, base_asset_id, quote_asset_id): - """ Returns True if given asset id's are of the current market + """ + Returns True if given asset id's are of the current market. - :return: bool: True = Current market, False = Not current market + :return: bool: True = Current market, False = Not current market """ if quote_asset_id == self._market['quote']['id']: if base_asset_id == self._market['base']['id']: @@ -512,10 +529,11 @@ def is_current_market(self, base_asset_id, quote_asset_id): return False def is_sell_order(self, order): - """ Check whether an order is sell order + """ + Check whether an order is sell order. - :param dict | order: dict or Order object - :return bool + :param dict | order: dict or Order object + :return bool """ # Check if the order is sell order, by comparing asset symbol of the order and the market if order['base']['symbol'] == self._market['quote']['symbol']: @@ -524,14 +542,15 @@ def is_sell_order(self, order): return False def place_market_buy_order(self, amount, price, return_none=False, *args, **kwargs): - """ Places a buy order in the market + """ + Places a buy order in the market. - :param float | amount: Order amount in QUOTE - :param float | price: Order price in BASE - :param bool | return_none: - :param args: - :param kwargs: - :return: + :param float | amount: Order amount in QUOTE + :param float | price: Order price in BASE + :param bool | return_none: + :param args: + :param kwargs: + :return: """ symbol = self._market['base']['symbol'] precision = self._market['base']['precision'] @@ -581,15 +600,16 @@ def place_market_buy_order(self, amount, price, return_none=False, *args, **kwar return True def place_market_sell_order(self, amount, price, return_none=False, invert=False, *args, **kwargs): - """ Places a sell order in the market + """ + Places a sell order in the market. - :param float | amount: Order amount in QUOTE - :param float | price: Order price in BASE - :param bool | return_none: - :param bool | invert: True = return inverted sell order - :param args: - :param kwargs: - :return: + :param float | amount: Order amount in QUOTE + :param float | price: Order price in BASE + :param bool | return_none: + :param bool | invert: True = return inverted sell order + :param args: + :param kwargs: + :return: """ symbol = self._market['quote']['symbol'] precision = self._market['quote']['precision'] @@ -640,12 +660,13 @@ def place_market_sell_order(self, amount, price, return_none=False, invert=False return True def retry_action(self, action, *args, **kwargs): - """ Perform an action, and if certain suspected-to-be-spurious grapheme 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 + """ + Perform an action, and if certain suspected-to-be-spurious grapheme 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. - :param action: - :return: + :param action: + :return: """ tries = 0 while True: @@ -709,16 +730,19 @@ def retry_action(self, action, *args, **kwargs): @property def balances(self): - """ Returns all the balances of the account assigned for the worker. - :return: Balances in list where each asset is in their own Amount object + """ + Returns all the balances of the account assigned for the worker. + + :return: Balances in list where each asset is in their own Amount object """ return self._account.balances def get_own_orders(self, refresh=True): - """ Return the account's open orders in the current market + """ + Return the account's open orders in the current market. - :param bool refresh: Use most recent data - :return: List of Order objects + :param bool refresh: Use most recent data + :return: List of Order objects """ orders = [] @@ -734,10 +758,11 @@ def get_own_orders(self, refresh=True): return orders def get_all_own_orders(self, refresh=True): - """ Return the worker's open orders in all markets + """ + Return the worker's open orders in all markets. - :param bool refresh: Use most recent data - :return: List of Order objects + :param bool refresh: Use most recent data + :return: List of Order objects """ # Refresh account data if refresh: @@ -751,17 +776,16 @@ def get_all_own_orders(self, refresh=True): @property def account(self): - """ Return the full account as :class:`bitshares.account.Account` object! - Can be refreshed by using ``x.refresh()`` + """ + Return the full account as :class:`bitshares.account.Account` object! Can be refreshed by using ``x.refresh()`` - :return: object | Account + :return: object | Account """ return self._account @property def market(self): - """ Return the market object as :class:`bitshares.market.Market` - """ + """Return the market object as :class:`bitshares.market.Market`""" return self._market @property @@ -774,23 +798,22 @@ def quote_asset(self): @property def all_own_orders(self): - """ Return the worker's open orders in all markets - """ + """Return the worker's open orders in all markets.""" return self.get_all_own_orders() @property def own_orders(self): - """ Return the account's open orders in the current market - """ + """Return the account's open orders in the current market.""" return self.get_own_orders() @staticmethod def get_updated_limit_order(limit_order): - """ Returns a modified limit_order so that when passed to Order class, - will return an Order object with updated amount values + """ + Returns a modified limit_order so that when passed to Order class, will return an Order object with updated + amount values. - :param limit_order: an item of Account['limit_orders'] or bitshares.rpc.get_limit_orders() - :return: Order + :param limit_order: an item of Account['limit_orders'] or bitshares.rpc.get_limit_orders() + :return: Order """ order = copy.deepcopy(limit_order) price = float(order['sell_price']['base']['amount']) / float(order['sell_price']['quote']['amount']) @@ -802,12 +825,13 @@ def get_updated_limit_order(limit_order): @staticmethod def convert_asset(from_value, from_asset, to_asset): - """ Converts asset to another based on the latest market value + """ + Converts asset to another based on the latest market value. - :param float | from_value: Amount of the input asset - :param string | from_asset: Symbol of the input asset - :param string | to_asset: Symbol of the output asset - :return: float Asset converted to another asset as float value + :param float | from_value: Amount of the input asset + :param string | from_asset: Symbol of the input asset + :param string | to_asset: Symbol of the output asset + :return: float Asset converted to another asset as float value """ market = Market('{}/{}'.format(from_asset, to_asset)) ticker = market.ticker() @@ -817,11 +841,12 @@ def convert_asset(from_value, from_asset, to_asset): return truncate((from_value * latest_price), precision) def convert_fee(self, fee_amount, fee_asset): - """ Convert fee amount in BTS to fee in fee_asset + """ + Convert fee amount in BTS to fee in fee_asset. - :param float | fee_amount: fee amount paid in BTS - :param Asset | fee_asset: fee asset to pay fee in - :return: float | amount of fee_asset to pay fee + :param float | fee_amount: fee amount paid in BTS + :param Asset | fee_asset: fee asset to pay fee in + :return: float | amount of fee_asset to pay fee """ if isinstance(fee_asset, str): fee_asset = Asset(fee_asset, bitshares_instance=self.bitshares) @@ -837,11 +862,12 @@ def convert_fee(self, fee_amount, fee_asset): return fee_amount * self.core_exchange_rate['base']['amount'] def get_order(self, order_id, return_none=True): - """ Get Order object with order_id + """ + Get Order object with order_id. - :param str | dict order_id: blockchain object id of the order can be an order 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 - :return: Order object + :param str | dict order_id: blockchain object id of the order can be an order 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 + :return: Order object """ if not order_id: return None @@ -857,12 +883,13 @@ def get_order(self, order_id, return_none=True): return order def is_partially_filled(self, order, threshold=0.3): - """ Checks whether order was partially filled + """ + Checks whether order was partially filled. - :param dict order: Order instance - :param float fill_threshold: Order fill threshold, relative - :return: bool True = Order is filled more than threshold - False = Order is not partially filled + :param dict order: Order instance + :param float fill_threshold: Order fill threshold, relative + :return: bool True = Order is filled more than threshold + False = Order is not partially filled """ if self.is_buy_order(order): order_type = 'buy' diff --git a/dexbot/pricefeeds/bitshares_feed.py b/dexbot/pricefeeds/bitshares_feed.py index 42e63c0b4..56958c0c0 100644 --- a/dexbot/pricefeeds/bitshares_feed.py +++ b/dexbot/pricefeeds/bitshares_feed.py @@ -6,14 +6,14 @@ class BitsharesPriceFeed: - """ This Price Feed class enables usage of Bitshares DEX for market center and order - book pricing, without requiring a registered account. It may be used for both - strategy and indicator analysis tools. - - All prices are passed and returned as BASE/QUOTE. - (In the BREAD/USD market that would be USD/BREAD, 2.5 USD / 1 BREAD). - - Buy orders reserve BASE - - Sell orders reserve QUOTE + """ + This Price Feed class enables usage of Bitshares DEX for market center and order book pricing, without requiring a + registered account. It may be used for both strategy and indicator analysis tools. + + All prices are passed and returned as BASE/QUOTE. + (In the BREAD/USD market that would be USD/BREAD, 2.5 USD / 1 BREAD). + - Buy orders reserve BASE + - Sell orders reserve QUOTE """ def __init__(self, market, bitshares_instance=None): @@ -30,37 +30,40 @@ def __init__(self, market, bitshares_instance=None): self.log = logging.LoggerAdapter(logging.getLogger('dexbot.pricefeed_log'), {}) def get_limit_orders(self, depth=1): - """ Returns orders from the current market. Orders are sorted by price. Does not require account info. + """ + Returns orders from the current market. Orders are sorted by price. Does not require account info. - get_limit_orders() call does not have any depth limit. + get_limit_orders() call does not have any depth limit. - :param int depth: Amount of orders per side will be fetched, default=1 - :return: Returns a list of orders or None + :param int depth: Amount of orders per side will be fetched, default=1 + :return: Returns a list of orders or None """ orders = self.bitshares.rpc.get_limit_orders(self.market['base']['id'], self.market['quote']['id'], depth) orders = [Order(o, bitshares_instance=self.bitshares) for o in orders] return orders def get_orderbook_orders(self, depth=1): - """ Returns orders from the current market split in bids and asks. Orders are sorted by price. + """ + Returns orders from the current market split in bids and asks. Orders are sorted by price. - Market.orderbook() call has hard-limit of depth=50 enforced by bitshares node. + Market.orderbook() call has hard-limit of depth=50 enforced by bitshares node. - bids = buy orders - asks = sell orders + bids = buy orders + asks = sell orders - :param int | depth: Amount of orders per side will be fetched, default=1 - :return: Returns a dictionary of orders or None + :param int | depth: Amount of orders per side will be fetched, default=1 + :return: Returns a dictionary of orders or None """ return self.market.orderbook(depth) def filter_buy_orders(self, orders, sort=None): - """ Return own buy orders from list of orders. Can be used to pick buy orders from a list - that is not up to date with the blockchain data. + """ + Return own buy orders from list of orders. Can be used to pick buy orders from a list that is not up to date + with the blockchain data. - :param list | orders: List of orders - :param string | sort: DESC or ASC will sort the orders accordingly, default None - :return list | buy_orders: List of buy orders only + :param list | orders: List of orders + :param string | sort: DESC or ASC will sort the orders accordingly, default None + :return list | buy_orders: List of buy orders only """ buy_orders = [] @@ -76,13 +79,14 @@ def filter_buy_orders(self, orders, sort=None): return buy_orders def filter_sell_orders(self, orders, sort=None, invert=True): - """ Return sell orders from list of orders. Can be used to pick sell orders from a list - that is not up to date with the blockchain data. + """ + Return sell orders from list of orders. Can be used to pick sell orders from a list that is not up to date with + the blockchain data. - :param list | orders: List of orders - :param string | sort: DESC or ASC will sort the orders accordingly, default None - :param bool | invert: return inverted orders or not - :return list | sell_orders: List of sell orders only + :param list | orders: List of orders + :param string | sort: DESC or ASC will sort the orders accordingly, default None + :param bool | invert: return inverted orders or not + :return list | sell_orders: List of sell orders only """ sell_orders = [] @@ -101,10 +105,11 @@ def filter_sell_orders(self, orders, sort=None, invert=True): return sell_orders def get_highest_market_buy_order(self, orders=None): - """ Returns the highest buy order that is not own, regardless of order size. + """ + Returns the highest buy order that is not own, regardless of order size. - :param list | orders: Optional list of orders, if none given fetch newest from market - :return: Highest market buy order or None + :param list | orders: Optional list of orders, if none given fetch newest from market + :return: Highest market buy order or None """ if not orders: orders = self.get_market_buy_orders(1) @@ -118,10 +123,11 @@ def get_highest_market_buy_order(self, orders=None): return order def get_lowest_market_sell_order(self, orders=None): - """ Returns the lowest sell order that is not own, regardless of order size. + """ + Returns the lowest sell order that is not own, regardless of order size. - :param list | orders: Optional list of orders, if none given fetch newest from market - :return: Lowest market sell order or None + :param list | orders: Optional list of orders, if none given fetch newest from market + :return: Lowest market sell order or None """ if not orders: orders = self.get_market_sell_orders(1) @@ -135,20 +141,22 @@ def get_lowest_market_sell_order(self, orders=None): return order def get_market_buy_orders(self, depth=10): - """ Fetches most recent data and returns list of buy orders. + """ + Fetches most recent data and returns list of buy orders. - :param int | depth: Amount of buy orders returned, Default=10 - :return: List of market sell orders + :param int | depth: Amount of buy orders returned, Default=10 + :return: List of market sell orders """ orders = self.get_limit_orders(depth=depth) buy_orders = self.filter_buy_orders(orders) return buy_orders def get_market_sell_orders(self, depth=10): - """ Fetches most recent data and returns list of sell orders. + """ + Fetches most recent data and returns list of sell orders. - :param int | depth: Amount of sell orders returned, Default=10 - :return: List of market sell orders + :param int | depth: Amount of sell orders returned, Default=10 + :return: List of market sell orders """ orders = self.get_limit_orders(depth=depth) sell_orders = self.filter_sell_orders(orders) @@ -156,13 +164,14 @@ def get_market_sell_orders(self, depth=10): def get_market_buy_price(self, quote_amount=0, base_amount=0, **kwargs): # TODO: refactor to use orders instead of exclude_own_orders - """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with - moving average or weighted moving average + """ + Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with moving average or + weighted moving average. - :param float | quote_amount: - :param float | base_amount: - :param dict | kwargs: - :return: price as float + :param float | quote_amount: + :param float | base_amount: + :param dict | kwargs: + :return: price as float """ market_buy_orders = [] @@ -222,15 +231,16 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, **kwargs): def get_market_sell_price(self, quote_amount=0, base_amount=0, **kwargs): # TODO: refactor to use orders instead of exclude_own_orders - """ Returns the BASE/QUOTE price for which [quote_amount] worth of QUOTE could be bought, - enhanced with moving average or weighted moving average. + """ + Returns the BASE/QUOTE price for which [quote_amount] worth of QUOTE could be bought, enhanced with moving + average or weighted moving average. - [quote/base]_amount = 0 means lowest regardless of size + [quote/base]_amount = 0 means lowest regardless of size - :param float | quote_amount: - :param float | base_amount: - :param dict | kwargs: - :return: + :param float | quote_amount: + :param float | base_amount: + :param dict | kwargs: + :return: """ market_sell_orders = [] @@ -288,12 +298,13 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, **kwargs): return base_amount / quote_amount def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors=False): - """ Returns the center price of market including own orders. + """ + Returns the center price of market including own orders. - :param float base_amount: - :param float quote_amount: - :param bool suppress_errors: True = return None on errors, False = disable worker - :return: Market center price as float + :param float base_amount: + :param float quote_amount: + :param bool suppress_errors: True = return None on errors, False = disable worker + :return: Market center price as float """ center_price = None buy_price = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount) @@ -317,11 +328,12 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors return center_price def get_market_spread(self, quote_amount=0, base_amount=0): - """ Returns the market spread %, including own orders, from specified depth. + """ + Returns the market spread %, including own orders, from specified depth. - :param float | quote_amount: - :param float | base_amount: - :return: Market spread as float or None + :param float | quote_amount: + :param float | base_amount: + :return: Market spread as float or None """ ask = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount) bid = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount) @@ -334,11 +346,12 @@ def get_market_spread(self, quote_amount=0, base_amount=0): @staticmethod def sort_orders_by_price(orders, sort='DESC'): - """ Return list of orders sorted ascending or descending by price + """ + Return list of orders sorted ascending or descending by price. - :param list | orders: list of orders to be sorted - :param string | sort: ASC or DESC. Default DESC - :return list: Sorted list of orders + :param list | orders: list of orders to be sorted + :param string | sort: ASC or DESC. Default DESC + :return list: Sorted list of orders """ if sort.upper() == 'ASC': reverse = False diff --git a/dexbot/storage.py b/dexbot/storage.py index 4b76152c0..579512272 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -79,10 +79,11 @@ def __init__(self, account, worker, base_total, base_symbol, quote_total, quote_ class Storage(dict): - """ Storage class + """ + Storage class. - :param string category: The category to distinguish - different storage namespaces + :param string category: The category to distinguish + different storage namespaces """ def __init__(self, category): @@ -107,25 +108,26 @@ def clear(self): db_worker.clear(self.category) def save_order(self, order): - """ Save the order to the database - """ + """Save the order to the database.""" order_id = order['id'] db_worker.save_order(self.category, order_id, order) def save_order_extended(self, order, virtual=None, custom=None): - """ Save the order to the database providing additional data + """ + Save the order to the database providing additional data. - :param dict order: - :param bool virtual: True = order is virtual order - :param str custom: any additional data + :param dict order: + :param bool virtual: True = order is virtual order + :param str custom: any additional data """ order_id = order['id'] db_worker.save_order_extended(self.category, order_id, order, virtual, custom) def remove_order(self, order): - """ Removes an order from the database + """ + Removes an order from the database. - :param dict,str order: order to remove, could be an Order instance or just order id + :param dict,str order: order to remove, could be an Order instance or just order id """ if isinstance(order, dict): order_id = order['id'] @@ -134,17 +136,17 @@ def remove_order(self, order): db_worker.remove_order(self.category, order_id) def clear_orders(self): - """ Removes all worker's orders from the database - """ + """Removes all worker's orders from the database.""" db_worker.clear_orders(self.category) def clear_orders_extended(self, worker=None, only_virtual=False, only_real=False, custom=None): - """ Removes worker's orders matching a criteria from the database + """ + Removes worker's orders matching a criteria from the database. - :param str worker: worker name (None means current worker name will be used) - :param bool only_virtual: True = only virtual orders - :param bool only_real: True = only real orders - :param str custom: filter orders by custom field + :param str worker: worker name (None means current worker name will be used) + :param bool only_virtual: True = only virtual orders + :param bool only_real: True = only real orders + :param str custom: filter orders by custom field """ if only_virtual and only_real: raise ValueError('only_virtual and only_real are mutually exclusive') @@ -153,9 +155,10 @@ def clear_orders_extended(self, worker=None, only_virtual=False, only_real=False return db_worker.clear_orders_extended(worker, only_virtual, only_real, custom) def fetch_orders(self, worker=None): - """ Get all the orders (or just specific worker's orders) from the database + """ + Get all the orders (or just specific worker's orders) from the database. - :param str worker: worker name (None means current worker name will be used) + :param str worker: worker name (None means current worker name will be used) """ if not worker: worker = self.category @@ -164,16 +167,17 @@ def fetch_orders(self, worker=None): def fetch_orders_extended( self, worker=None, only_virtual=False, only_real=False, custom=None, return_ids_only=False ): - """ Get orders from the database in extended format (returning all columns) - - :param str worker: worker name (None means current worker name will be used) - :param bool only_virtual: True = fetch only virtual orders - :param bool only_real: True = fetch only real orders - :param str custom: filter orders by custom field - :param bool return_ids_only: instead of returning full row data, return only order ids - :rtype: list - :return: list of dicts in format [{order_id: '', order: '', virtual: '', custom: ''}], or [order_id] if - return_ids_only used + """ + Get orders from the database in extended format (returning all columns) + + :param str worker: worker name (None means current worker name will be used) + :param bool only_virtual: True = fetch only virtual orders + :param bool only_real: True = fetch only real orders + :param str custom: filter orders by custom field + :param bool return_ids_only: instead of returning full row data, return only order ids + :rtype: list + :return: list of dicts in format [{order_id: '', order: '', virtual: '', custom: ''}], or [order_id] if + return_ids_only used """ if only_virtual and only_real: raise ValueError('only_virtual and only_real are mutually exclusive') @@ -204,8 +208,7 @@ def get_recent_balance_entry(account, worker, base_asset, quote_asset): class DatabaseWorker(threading.Thread): - """ Thread safe database worker - """ + """Thread safe database worker.""" def __init__(self, **kwargs): super().__init__() @@ -247,11 +250,12 @@ def __init__(self, **kwargs): @staticmethod def run_migrations(script_location, dsn, stamp_only=False): - """ Apply database migrations using alembic + """ + Apply database migrations using alembic. - :param str script_location: path to migration scripts - :param str dsn: database URL - :param bool stamp_only: True = only mark the db as "head" without applying migrations + :param str script_location: path to migration scripts + :param str dsn: database URL + :param bool stamp_only: True = only mark the db as "head" without applying migrations """ alembic_cfg = alembic.config.Config() alembic_cfg.set_main_option('script_location', script_location) @@ -265,8 +269,7 @@ def run_migrations(script_location, dsn, stamp_only=False): @staticmethod def get_filter_by(worker, only_virtual, only_real, custom): - """ Make filter_by for sqlalchemy query based on args - """ + """Make filter_by for sqlalchemy query based on args.""" filter_by = {'worker': worker} if only_virtual: filter_by['virtual'] = True @@ -463,8 +466,7 @@ def get_balance(self, account, worker, timestamp, base_asset, quote_asset): return self.execute(self._get_balance, account, worker, timestamp, base_asset, quote_asset) def _get_balance(self, account, worker, timestamp, base_asset, quote_asset, token): - """ Get first item that has bigger time as given timestamp and matches account and worker name - """ + """Get first item that has bigger time as given timestamp and matches account and worker name.""" result = ( self.session.query(Balances) .filter( @@ -483,8 +485,7 @@ def get_recent_balance_entry(self, account, worker, base_asset, quote_asset): return self.execute(self._get_recent_balance_entry, account, worker, base_asset, quote_asset) def _get_recent_balance_entry(self, account, worker, base_asset, quote_asset, token): - """ Get most recent balance history item that matches account and worker name - """ + """Get most recent balance history item that matches account and worker name.""" result = ( self.session.query(Balances) .filter( diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 1b1b8b5f4..53faae43e 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -21,52 +21,53 @@ class StrategyBase(BitsharesOrderEngine, BitsharesPriceFeed): - """ A strategy based on this class is intended to work in one market. This class contains - most common methods needed by the strategy. + """ + A strategy based on this class is intended to work in one market. This class contains most common methods needed by + the strategy. - NOTE: StrategyBase currently requires BitsharesOrderEngine inheritance - as all configuration from Worker is located here. + NOTE: StrategyBase currently requires BitsharesOrderEngine inheritance + as all configuration from Worker is located here. - Post Core-refactor, in the future it should not be this way. + Post Core-refactor, in the future it should not be this way. - TODO: The StrategyBase should be able to select any {N} OrderEngine(s) and {M} PriceFeed(s) - and not be tied to the BitsharesOrderEngine only. (where N and M are integers) - This would allow for cross dex or cex strategy flexibility + TODO: The StrategyBase should be able to select any {N} OrderEngine(s) and {M} PriceFeed(s) + and not be tied to the BitsharesOrderEngine only. (where N and M are integers) + This would allow for cross dex or cex strategy flexibility - In process: make StrategyBase an ABC. + In process: make StrategyBase an ABC. - Unit tests should take above into consideration + Unit tests should take above into consideration - All prices are passed and returned as BASE/QUOTE. - (In the BREAD:USD market that would be USD/BREAD, 2.5 USD / 1 BREAD). - - Buy orders reserve BASE - - Sell orders reserve QUOTE + All prices are passed and returned as BASE/QUOTE. + (In the BREAD:USD market that would be USD/BREAD, 2.5 USD / 1 BREAD). + - Buy orders reserve BASE + - Sell orders reserve QUOTE - Strategy inherits: - * :class:`dexbot.storage.Storage` : Stores data to sqlite database - * ``Events`` + Strategy inherits: + * :class:`dexbot.storage.Storage` : Stores data to sqlite database + * ``Events`` - Available attributes: - * ``worker.bitshares``: instance of ´`bitshares.BitShares()`` - * ``worker.account``: The Account object of this worker - * ``worker.market``: The market used by this worker - * ``worker.orders``: List of open orders of the worker's account in the worker's market - * ``worker.balance``: List of assets and amounts available in the worker's account - * ``worker.log``: a per-worker logger (actually LoggerAdapter) adds worker-specific context: - worker name & account (Because some UIs might want to display per-worker logs) + Available attributes: + * ``worker.bitshares``: instance of ´`bitshares.BitShares()`` + * ``worker.account``: The Account object of this worker + * ``worker.market``: The market used by this worker + * ``worker.orders``: List of open orders of the worker's account in the worker's market + * ``worker.balance``: List of assets and amounts available in the worker's account + * ``worker.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, Worker inherits :class:`dexbot.storage.Storage` - which allows to permanently store data in a sqlite database - using: + Also, Worker inherits :class:`dexbot.storage.Storage` + which allows to permanently store data in a sqlite database + using: - ``worker["key"] = "value"`` + ``worker["key"] = "value"`` - .. note:: This applies a ``json.loads(json.dumps(value))``! + .. note:: This applies a ``json.loads(json.dumps(value))``! - 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. + 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. """ @classmethod @@ -200,9 +201,10 @@ def __init__( self.orders_log = logging.LoggerAdapter(logging.getLogger('dexbot.orders_log'), {}) def pause(self): - """ Pause the worker + """ + Pause the worker. - Note: By default pause cancels orders, but this can be overridden by strategy + Note: By default pause cancels orders, but this can be overridden by strategy """ # Cancel all orders from the market self.cancel_all_orders() @@ -211,8 +213,7 @@ def pause(self): self.clear_orders() def clear_all_worker_data(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.""" # Removes worker's orders from local database self.clear_orders() @@ -223,11 +224,12 @@ def clear_all_worker_data(self): self.clear() def get_worker_share_for_asset(self, asset): - """ Returns operational percent of asset available to the worker + """ + Returns operational percent of asset available to the worker. - :param str asset: Which asset should be checked - :return: a value between 0-1 representing a percent - :rtype: float + :param str asset: Which asset should be checked + :return: a value between 0-1 representing a percent + :rtype: float """ intersections_data = self.assets_intersections_data[self.account.name][asset] @@ -245,8 +247,7 @@ def get_worker_share_for_asset(self, asset): self.log.error('Got asset which is not used by this worker') def store_profit_estimation_data(self): - """ Save total quote, total base, center_price, and datetime in to the database - """ + """Save total quote, total base, center_price, and datetime in to the database.""" assets = self.count_asset() account = self.config['workers'][self.worker_name].get('account') base_amount = assets['base'] @@ -264,17 +265,17 @@ def store_profit_estimation_data(self): ) def get_profit_estimation_data(self, seconds): - """ Get balance history closest to the given time + """ + Get balance history closest to the given time. - :returns The data as dict from the first timestamp going backwards from seconds argument + :returns The data as dict from the first timestamp going backwards from seconds argument """ return self.get_balance_history( self.config['workers'][self.worker_name].get('account'), self.worker_name, seconds ) def calc_profit(self): - """ Calculate relative profit for the current worker - """ + """Calculate relative profit for the current worker.""" profit = 0 time_range = 60 * 60 * 24 * 7 # 7 days current_time = time.time() @@ -321,17 +322,19 @@ def calc_profit(self): @property def balances(self): - """ Returns all the balances of the account assigned for the worker. + """ + Returns all the balances of the account assigned for the worker. - :return: Balances in list where each asset is in their own Amount object + :return: Balances in list where each asset is in their own Amount object """ return self._account.balances @staticmethod def purge_all_local_worker_data(worker_name): - """ Removes worker's data and orders from local sqlite database + """ + Removes worker's data and orders from local sqlite database. - :param worker_name: Name of the worker to be removed + :param worker_name: Name of the worker to be removed """ Storage.clear_worker_data(worker_name) diff --git a/dexbot/strategies/config_parts/base_config.py b/dexbot/strategies/config_parts/base_config.py index 686f3fd05..157408852 100644 --- a/dexbot/strategies/config_parts/base_config.py +++ b/dexbot/strategies/config_parts/base_config.py @@ -49,16 +49,17 @@ class BaseConfig: @classmethod def configure(cls, return_base_config=True): - """ Return a list of ConfigElement objects defining the configuration values for this class. + """ + Return a list of ConfigElement objects defining the configuration values for this class. - User interfaces should then generate widgets based on these values, gather data and save back to - the config dictionary for the worker. + User interfaces should then generate widgets based on these values, gather data and save back to + the config dictionary for the worker. - NOTE: When overriding you almost certainly will want to call the ancestor and then - add your config values to the list. + NOTE: When overriding you almost certainly will want to call the ancestor and then + add your config values to the list. - :param return_base_config: bool: - :return: Returns a list of config elements + :param return_base_config: bool: + :return: Returns a list of config elements """ # Common configs @@ -99,16 +100,17 @@ def configure(cls, return_base_config=True): @classmethod def configure_details(cls, include_default_tabs=True): - """ Return a list of ConfigElement objects defining the configuration values for this class. + """ + Return a list of ConfigElement objects defining the configuration values for this class. - User interfaces should then generate widgets based on these values, gather data and save back to - the config dictionary for the worker. + User interfaces should then generate widgets based on these values, gather data and save back to + the config dictionary for the worker. - NOTE: When overriding you almost certainly will want to call the ancestor and then - add your config values to the list. + NOTE: When overriding you almost certainly will want to call the ancestor and then + add your config values to the list. - :param include_default_tabs: bool: - :return: Returns a list of Detail elements + :param include_default_tabs: bool: + :return: Returns a list of Detail elements """ # Common configs diff --git a/dexbot/strategies/config_parts/relative_config.py b/dexbot/strategies/config_parts/relative_config.py index f03cf3abc..a1ca2fe63 100644 --- a/dexbot/strategies/config_parts/relative_config.py +++ b/dexbot/strategies/config_parts/relative_config.py @@ -4,16 +4,17 @@ class RelativeConfig(BaseConfig): @classmethod def configure(cls, return_base_config=True): - """ Return a list of ConfigElement objects defining the configuration values for this class. + """ + Return a list of ConfigElement objects defining the configuration values for this class. - User interfaces should then generate widgets based on these values, gather data and save back to - the config dictionary for the worker. + User interfaces should then generate widgets based on these values, gather data and save back to + the config dictionary for the worker. - NOTE: When overriding you almost certainly will want to call the ancestor and then - add your config values to the list. + NOTE: When overriding you almost certainly will want to call the ancestor and then + add your config values to the list. - :param return_base_config: bool: - :return: Returns a list of config elements + :param return_base_config: bool: + :return: Returns a list of config elements """ # External exchanges used to calculate center price EXCHANGES = [ @@ -188,15 +189,16 @@ def configure(cls, return_base_config=True): @classmethod def configure_details(cls, include_default_tabs=True): - """ Return a list of ConfigElement objects defining the configuration values for this class. + """ + Return a list of ConfigElement objects defining the configuration values for this class. - User interfaces should then generate widgets based on these values, gather data and save back to - the config dictionary for the worker. + User interfaces should then generate widgets based on these values, gather data and save back to + the config dictionary for the worker. - NOTE: When overriding you almost certainly will want to call the ancestor and then - add your config values to the list. + NOTE: When overriding you almost certainly will want to call the ancestor and then + add your config values to the list. - :param include_default_tabs: bool: - :return: Returns a list of Detail elements + :param include_default_tabs: bool: + :return: Returns a list of Detail elements """ return BaseConfig.configure_details(include_default_tabs) + [] diff --git a/dexbot/strategies/config_parts/staggered_config.py b/dexbot/strategies/config_parts/staggered_config.py index 926a86fd4..5d37a15b7 100644 --- a/dexbot/strategies/config_parts/staggered_config.py +++ b/dexbot/strategies/config_parts/staggered_config.py @@ -4,25 +4,26 @@ class StaggeredConfig(BaseConfig): @classmethod def configure(cls, return_base_config=True): - """ Modes description: + """ + Modes description: - Mountain: - - Buy orders same QUOTE - - Sell orders same BASE + Mountain: + - Buy orders same QUOTE + - Sell orders same BASE - Neutral: - - Buy orders lower_order_base * sqrt(1 + increment) - - Sell orders higher_order_quote * sqrt(1 + increment) + Neutral: + - Buy orders lower_order_base * sqrt(1 + increment) + - Sell orders higher_order_quote * sqrt(1 + increment) - Valley: - - Buy orders same BASE - - Sell orders same QUOTE + Valley: + - Buy orders same BASE + - Sell orders same QUOTE - Buy slope: - - All orders same BASE (profit comes in QUOTE) + Buy slope: + - All orders same BASE (profit comes in QUOTE) - Sell slope: - - All orders same QUOTE (profit made in BASE) + Sell slope: + - All orders same QUOTE (profit made in BASE) """ modes = [ ('mountain', 'Mountain'), diff --git a/dexbot/strategies/config_parts/strategy_config.py b/dexbot/strategies/config_parts/strategy_config.py index cbf03eafa..a7e5f15d3 100644 --- a/dexbot/strategies/config_parts/strategy_config.py +++ b/dexbot/strategies/config_parts/strategy_config.py @@ -2,15 +2,15 @@ class StrategyConfig(BaseConfig): - """ this is the configuration template for the strategy_template class - """ + """this is the configuration template for the strategy_template class.""" @classmethod def configure(cls, return_base_config=True): - """ This function is used to auto generate fields for GUI + """ + This function is used to auto generate fields for GUI. - :param return_base_config: If base config is used in addition to this configuration. - :return: List of ConfigElement(s) + :param return_base_config: If base config is used in addition to this configuration. + :return: List of ConfigElement(s) """ """ As a demonstration this template has two fields in the worker configuration. Upper and lower bound. @@ -27,12 +27,13 @@ def configure(cls, return_base_config=True): @classmethod def configure_details(cls, include_default_tabs=True): - """ This function defines the tabs for detailed view of the worker. Further documentation is found in base.py + """ + This function defines the tabs for detailed view of the worker. Further documentation is found in base.py. - :param include_default_tabs: If default tabs are included as well - :return: List of DetailElement(s) + :param include_default_tabs: If default tabs are included as well + :return: List of DetailElement(s) - NOTE: Add files to user data folders to see how they behave as an example. + NOTE: Add files to user data folders to see how they behave as an example. """ return BaseConfig.configure_details(include_default_tabs) + [ DetailElement('graph', 'Graph', 'Graph', 'graph.jpg'), diff --git a/dexbot/strategies/echo.py b/dexbot/strategies/echo.py index 264f1027c..397bccc3f 100644 --- a/dexbot/strategies/echo.py +++ b/dexbot/strategies/echo.py @@ -2,9 +2,7 @@ class Strategy(StrategyBase): - """ Echo strategy - Strategy that logs all events within the blockchain - """ + """Echo strategy Strategy that logs all events within the blockchain.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -21,67 +19,72 @@ def __init__(self, *args, **kwargs): self.error_onAccount = self.error def error(self, *args, **kwargs): - """ What to do on an error - """ + """What to do on an error.""" # Cancel all future execution self.disabled = True def print_orderMatched(self, i): - """ Is called when an order in the market is matched + """ + Is called when an order in the market is matched. - A developer may want to filter those to identify - own orders. + A developer may want to filter those to identify + own orders. - :param bitshares.price.FilledOrder i: Filled order details + :param bitshares.price.FilledOrder i: Filled order details """ self.log.info("Order matched: {}".format(i)) def print_orderPlaced(self, i): - """ Is called when a new order in the market is placed + """ + Is called when a new order in the market is placed. - A developer may want to filter those to identify - own orders. + A developer may want to filter those to identify + own orders. - :param bitshares.price.Order i: Order details + :param bitshares.price.Order i: Order details """ self.log.info("Order placed: {}".format(i)) def print_UpdateCallOrder(self, i): - """ Is called when a call order for a market pegged asset is updated + """ + Is called when a call order for a market pegged asset is updated. - A developer may want to filter those to identify - own orders. + A developer may want to filter those to identify + own orders. - :param bitshares.price.CallOrder i: Call order details + :param bitshares.price.CallOrder i: Call order details """ self.log.info("Call update: {}".format(i)) def print_marketUpdate(self, i): - """ Is called when Something happens in your market. + """ + Is called when Something happens in your market. - This method is actually called by the backend and is - dispatched to ``onOrderMatched``, ``onOrderPlaced`` and - ``onUpdateCallOrder``. + This method is actually called by the backend and is + dispatched to ``onOrderMatched``, ``onOrderPlaced`` and + ``onUpdateCallOrder``. - :param object i: Can be instance of ``FilledOrder``, ``Order``, or ``CallOrder`` + :param object i: Can be instance of ``FilledOrder``, ``Order``, or ``CallOrder`` """ self.log.info("Market update: {}".format(i)) def print_newBlock(self, i): - """ Is called when a block is received + """ + Is called when a block is received. - :param str i: The hash of the block + :param str i: The hash of the block - .. note:: Unfortunately, it is currently not possible to - identify the block number for ``i`` alone. If you - need to know the most recent block number, you - need to use ``bitshares.blockchain.Blockchain`` + .. note:: Unfortunately, it is currently not possible to + identify the block number for ``i`` alone. If you + need to know the most recent block number, you + need to use ``bitshares.blockchain.Blockchain`` """ self.log.info("New block: {}".format(i)) def print_accountUpdate(self, i): - """ 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. + """ + 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. """ self.log.info("Account: {}".format(i)) diff --git a/dexbot/strategies/external_feeds/ccxt_feed.py b/dexbot/strategies/external_feeds/ccxt_feed.py index 14009e4b1..00d1e5abf 100644 --- a/dexbot/strategies/external_feeds/ccxt_feed.py +++ b/dexbot/strategies/external_feeds/ccxt_feed.py @@ -34,7 +34,7 @@ async def fetch_ticker(exchange, symbol): def get_ccxt_price(symbol, exchange_name): - """ Get all tickers from multiple exchanges using async """ + """Get all tickers from multiple exchanges using async.""" center_price = None async_loop = asyncio.new_event_loop() diff --git a/dexbot/strategies/external_feeds/price_feed.py b/dexbot/strategies/external_feeds/price_feed.py index 2f83853ea..83089d417 100644 --- a/dexbot/strategies/external_feeds/price_feed.py +++ b/dexbot/strategies/external_feeds/price_feed.py @@ -14,9 +14,7 @@ class PriceFeed: - """ - price feed class, which handles all data requests for external center price - """ + """price feed class, which handles all data requests for external center price.""" def __init__(self, exchange, symbol): self._alt_exchanges = ['gecko', 'waves'] # assume all other exchanges are ccxt diff --git a/dexbot/strategies/king_of_the_hill.py b/dexbot/strategies/king_of_the_hill.py index 4a252a747..44b09340a 100644 --- a/dexbot/strategies/king_of_the_hill.py +++ b/dexbot/strategies/king_of_the_hill.py @@ -10,18 +10,19 @@ class Strategy(StrategyBase): - """ King of the Hill strategy + """ + King of the Hill strategy. - This worker will place a buy or sell order for an asset and update so that the users order stays closest to the - opposing order book. + This worker will place a buy or sell order for an asset and update so that the users order stays closest to the + opposing order book. - Moving forward: If any other orders are placed closer to the opposing order book the worker will cancel the - users order and replace it with one that is the smallest possible increment closer to the opposing order book - than any other orders. + Moving forward: If any other orders are placed closer to the opposing order book the worker will cancel the + users order and replace it with one that is the smallest possible increment closer to the opposing order book + than any other orders. - Moving backward: If the users order is the closest to the opposing order book but a gap opens up on the order - book behind the users order the worker will cancel the order and place it at the smallest possible increment - closer to the opposing order book than any other order. + Moving backward: If the users order is the closest to the opposing order book but a gap opens up on the order + book behind the users order the worker will cancel the order and place it at the smallest possible increment + closer to the opposing order book than any other order. """ @classmethod @@ -88,8 +89,7 @@ def __init__(self, *args, **kwargs): self.log.info("{} initialized.".format(STRATEGY_NAME)) def check_bitasset_market(self): - """ Check if worker market is MPA:COLLATERAL market - """ + """Check if worker market is MPA:COLLATERAL market.""" if not (self.market['base'].is_bitasset or self.market['quote'].is_bitasset): # One of the assets must be a bitasset return @@ -114,8 +114,7 @@ def check_bitasset_market(self): @check_last_run def maintain_strategy(self, *args): - """ Strategy main logic - """ + """Strategy main logic.""" if self.orders: self.check_orders() @@ -123,8 +122,7 @@ def maintain_strategy(self, *args): self.place_orders() def check_orders(self): - """ Check whether own orders needs intervention - """ + """Check whether own orders needs intervention.""" self.get_top_prices() orders = copy.deepcopy(self.orders) @@ -165,8 +163,7 @@ def check_orders(self): self.place_order(order_type) def get_top_prices(self): - """ Get current top prices (foreign orders) - """ + """Get current top prices (foreign orders)""" # Obtain orderbook orders excluding our orders market_orders = self.get_market_orders(depth=100) own_orders_ids = [order['id'] for order in self.own_orders] @@ -239,10 +236,11 @@ def get_top_prices(self): self.log.info('Market has empty orderbook') def get_cumulative_call_order(self, asset): - """ Get call orders, compound them and return as it was a single limit order + """ + Get call orders, compound them and return as it was a single limit order. - :param Asset asset: bitshares asset - :return: dict representing an order + :param Asset asset: bitshares asset + :return: dict representing an order """ # TODO: move this method to price engine to use for center price detection etc call_orders = asset.get_call_orders() @@ -258,11 +256,13 @@ def get_cumulative_call_order(self, asset): return order def is_too_small_amounts(self, amount_quote, amount_base): - """ Check whether amounts are within asset precision limits - :param Decimal amount_quote: QUOTE asset amount - :param Decimal amount_base: BASE asset amount - :return: bool True = amounts are too small - False = amounts are within limits + """ + Check whether amounts are within asset precision limits. + + :param Decimal amount_quote: QUOTE asset amount + :param Decimal amount_base: BASE asset amount + :return: bool True = amounts are too small + False = amounts are within limits """ if ( amount_quote < Decimal(10) ** -self.market['quote']['precision'] @@ -273,8 +273,7 @@ def is_too_small_amounts(self, amount_quote, amount_base): return False def place_order(self, order_type): - """ Place single order - """ + """Place single order.""" new_order = None if order_type == 'buy': @@ -376,8 +375,7 @@ def place_order(self, order_type): self.log.error('Failed to place {} order'.format(order_type)) def place_orders(self): - """ Place new orders - """ + """Place new orders.""" place_buy = False place_sell = False @@ -398,8 +396,7 @@ def place_orders(self): @property def amount_quote(self): - """ Get quote amount, calculate if order size is relative - """ + """Get quote amount, calculate if order size is relative.""" amount = self.sell_order_amount if self.is_relative_order_size: quote_balance = float(self.balance(self.market['quote'])) @@ -409,8 +406,7 @@ 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.""" amount = self.buy_order_amount if self.is_relative_order_size: base_balance = float(self.balance(self.market['base'])) @@ -419,11 +415,11 @@ def amount_base(self): return amount def error(self, *args, **kwargs): - """ Defines what happens when error occurs """ + """Defines what happens when error occurs.""" self.disabled = True def tick(self, d): - """ Ticks come in on every block """ + """Ticks come in on every block.""" if not (self.counter or 0) % 4: self.maintain_strategy() self.counter += 1 diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index 037846468..f55be01dc 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -7,8 +7,7 @@ class Strategy(StrategyBase): - """ Relative Orders strategy - """ + """Relative Orders strategy.""" @classmethod def configure(cls, return_base_config=True): @@ -123,8 +122,10 @@ def error(self, *args, **kwargs): self.disabled = True def tick(self, d): - """ Ticks come in on every block. We need to periodically check orders because cancelled orders - do not triggers a market_update event + """ + Ticks come in on every block. + + We need to periodically check orders because cancelled orders do not triggers a market_update event """ if self.is_reset_on_price_change and not self.counter % 8: self.log.debug('Checking orders by tick threshold') @@ -133,8 +134,7 @@ def tick(self, d): @property def amount_to_sell(self): - """ Get quote amount, calculate if order size is relative - """ + """Get quote amount, calculate if order size is relative.""" amount = self.order_size if self.is_relative_order_size: quote_balance = float(self.balance(self.market["quote"])) @@ -150,8 +150,7 @@ def amount_to_sell(self): @property def amount_to_buy(self): - """ Get base amount, calculate if order size is relative - """ + """Get base amount, calculate if order size is relative.""" amount = self.order_size if self.is_relative_order_size: base_balance = float(self.balance(self.market["base"])) @@ -167,10 +166,11 @@ def amount_to_buy(self): return amount def get_external_market_center_price(self, external_price_source): - """ Get center price from an external market for current market pair + """ + Get center price from an external market for current market pair. - :param external_price_source: External market name - :return: Center price as float + :param external_price_source: External market name + :return: Center price as float """ self.log.debug('inside get_external_mcp, exchange: {} '.format(external_price_source)) market = self.market.get_string('/') @@ -294,14 +294,15 @@ def update_orders(self): self.update_orders() def get_market_buy_price(self, quote_amount=0, base_amount=0, **kwargs): - """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with - moving average or weighted moving average - - :param float | quote_amount: - :param float | base_amount: - :param dict | kwargs: - bool | exclude_own_orders: Exclude own orders when calculating a price - :return: price as float + """ + Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with moving average or + weighted moving average. + + :param float | quote_amount: + :param float | base_amount: + :param dict | kwargs: + bool | exclude_own_orders: Exclude own orders when calculating a price + :return: price as float """ exclude_own_orders = kwargs.get('exclude_own_orders', True) market_buy_orders = [] @@ -373,16 +374,17 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, **kwargs): return base_amount / quote_amount def get_market_sell_price(self, quote_amount=0, base_amount=0, **kwargs): - """ Returns the BASE/QUOTE price for which [quote_amount] worth of QUOTE could be bought, - enhanced with moving average or weighted moving average. + """ + Returns the BASE/QUOTE price for which [quote_amount] worth of QUOTE could be bought, enhanced with moving + average or weighted moving average. - [quote/base]_amount = 0 means lowest regardless of size + [quote/base]_amount = 0 means lowest regardless of size - :param float | quote_amount: - :param float | base_amount: - :param dict | kwargs: - bool | exclude_own_orders: Exclude own orders when calculating a price - :return: + :param float | quote_amount: + :param float | base_amount: + :param dict | kwargs: + bool | exclude_own_orders: Exclude own orders when calculating a price + :return: """ exclude_own_orders = kwargs.get('exclude_own_orders', True) market_sell_orders = [] @@ -472,8 +474,7 @@ def _calculate_center_price(self, suppress_errors=False): def calculate_center_price( self, center_price=None, asset_offset=False, spread=None, order_ids=None, manual_offset=0, suppress_errors=False ): - """ Calculate center price which shifts based on available funds - """ + """Calculate center price which shifts based on available funds.""" if center_price is None: # No center price was given so we simply calculate the center price calculated_center_price = self._calculate_center_price(suppress_errors) @@ -497,12 +498,13 @@ def calculate_center_price( return calculated_center_price def calculate_asset_offset(self, center_price, order_ids, spread): - """ Adds offset based on the asset balance of the worker to the center price + """ + Adds offset based on the asset balance of the worker to the center price. - :param float | center_price: Center price - :param list | order_ids: List of order ids that are used to calculate balance - :param float | spread: Spread percentage as float (eg. 0.01) - :return: Center price with asset offset + :param float | center_price: Center price + :param list | order_ids: List of order ids that are used to calculate balance + :param float | spread: Spread percentage as float (eg. 0.01) + :return: Center price with asset offset """ total_balance = self.count_asset(order_ids) total = (total_balance['quote'] * center_price) + total_balance['base'] @@ -528,14 +530,15 @@ def calculate_asset_offset(self, center_price, order_ids, spread): @staticmethod def calculate_manual_offset(center_price, manual_offset): - """ Adds manual offset to given center price + """ + Adds manual offset to given center price. - :param float | center_price: - :param float | manual_offset: - :return: Center price with manual offset + :param float | center_price: + :param float | manual_offset: + :return: Center price with manual offset - Adjust center price by given percent in symmetrical way. Thus, -1% adjustement on BTS:USD market will be - same as adjusting +1% on USD:BTS market. + Adjust center price by given percent in symmetrical way. Thus, -1% adjustement on BTS:USD market will be + same as adjusting +1% on USD:BTS market. """ if manual_offset < 0: return center_price / (1 + abs(manual_offset)) @@ -544,8 +547,7 @@ def calculate_manual_offset(center_price, manual_offset): @check_last_run def check_orders(self, *args, **kwargs): - """ Tests if the orders need updating - """ + """Tests if the orders need updating.""" # Store current available balance and balance in orders to the database for profit calculation purpose self.store_profit_estimation_data() @@ -611,7 +613,7 @@ def check_orders(self, *args, **kwargs): self.update_gui_profit() def get_own_last_trade(self): - """ Returns dict with amounts and price of last trade """ + """Returns dict with amounts and price of last trade.""" history = self.account.history(only_ops=['fill_order']) for entry in history: trade = entry['op'][1] diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c8fcf7518..734ca9f9d 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -12,7 +12,7 @@ class Strategy(StrategyBase): - """ Staggered Orders strategy """ + """Staggered Orders strategy.""" @classmethod def configure(cls, return_base_config=True): @@ -121,9 +121,11 @@ def __init__(self, *args, **kwargs): @check_last_run def maintain_strategy(self, *args, **kwargs): - """ Logic of the strategy - :param args: - :param kwargs: + """ + Logic of the strategy. + + :param args: + :param kwargs: """ # Get all user's orders on current market self.refresh_orders() @@ -193,8 +195,11 @@ def maintain_strategy(self, *args, **kwargs): try: trx = self.retry_action(self.execute) except bitsharesapi.exceptions.RPCError as exception: - """ Handle exception without stopping the worker. The goal is to handle race condition when partially - filled order was further filled before we actually replaced them. + """ + Handle exception without stopping the worker. + + The goal is to handle race condition when partially filled order was further filled before we actually + replaced them. """ if str(exception).startswith('Assert Exception: maybe_found != nullptr: Unable to find Object'): self.log.warning(exception) @@ -281,8 +286,7 @@ def maintain_strategy(self, *args, **kwargs): self.update_gui_profit() def get_actual_spread(self, buy_price=None, sell_price=None): - """ Calculates current spread on own orders using cached orders - """ + """Calculates current spread on own orders using cached orders.""" if buy_price and sell_price: highest_buy_price = buy_price lowest_sell_price = sell_price @@ -298,14 +302,12 @@ def get_actual_spread(self, buy_price=None, sell_price=None): return spread def calculate_min_amounts(self): - """ Calculate minimal order amounts depending on defined increment - """ + """Calculate minimal order amounts depending on defined increment.""" self.order_min_base = 2 * 10 ** -self.market['base']['precision'] / self.increment self.order_min_quote = 2 * 10 ** -self.market['quote']['precision'] / self.increment def stop_loss_check(self): - """ Check for Stop Loss condition and execute SL if needed - """ + """Check for Stop Loss condition and execute SL if needed.""" if self.buy_orders: return @@ -329,12 +331,13 @@ def stop_loss_check(self): self.error() def refresh_balances(self, use_cached_orders=False): - """ This function is used to refresh account balances + """ + This function is used to refresh account balances. - :param bool use_cached_orders (optional): when calculating orders - balance, use cached orders from self.cached_orders + :param bool use_cached_orders (optional): when calculating orders + balance, use cached orders from self.cached_orders - This version supports usage of same bitshares account across multiple workers with assets intersections. + This version supports usage of same bitshares account across multiple workers with assets intersections. """ # Balances in orders on all related markets orders = self.get_all_own_orders(refresh=not use_cached_orders) @@ -418,8 +421,7 @@ def refresh_balances(self, use_cached_orders=False): self.quote_balance['amount'] -= fee_reserve def refresh_orders(self): - """ Updates buy and sell orders - """ + """Updates buy and sell orders.""" orders = self.get_own_orders() # Sort virtual orders @@ -436,8 +438,7 @@ def refresh_orders(self): self.sell_orders = self.real_sell_orders + self.virtual_sell_orders def sync_current_orders(self): - """ Sync current orders to the db - """ + """Sync current orders to the db.""" current_real_orders = self.real_buy_orders + self.real_sell_orders current_all_orders = self.buy_orders + self.sell_orders current_real_ids = set([order['id'] for order in current_real_orders]) @@ -456,8 +457,7 @@ def sync_current_orders(self): self.save_order_extended(order, custom='current') def dump_initial_orders(self): - """ Save orders after initial placement for later use (visualization and so on) - """ + """Save orders after initial placement for later use (visualization and so on)""" self.refresh_orders() orders = self.buy_orders + self.sell_orders self.log.info('Dumping initial orders into db') @@ -470,15 +470,16 @@ def dump_initial_orders(self): self.save_order_extended(order, virtual=False, custom='initial') def drop_initial_orders(self): - """ Drop old "initial" orders from the db - """ + """Drop old "initial" orders from the db.""" self.log.debug('Removing initial orders from the db') self.clear_orders_extended(custom='initial') def remove_outside_orders(self, sell_orders, buy_orders): - """ Remove orders that exceed boundaries - :param list | sell_orders: User's sell orders - :param list | buy_orders: User's buy orders + """ + Remove orders that exceed boundaries. + + :param list | sell_orders: User's sell orders + :param list | buy_orders: User's buy orders """ orders_to_cancel = [] @@ -513,10 +514,11 @@ def remove_outside_orders(self, sell_orders, buy_orders): return True def restore_virtual_orders(self): - """ Create virtual further orders in batch manner. This helps to place further orders quickly on startup. + """ + Create virtual further orders in batch manner. This helps to place further orders quickly on startup. - If we have both buy and sell real orders, restore both. If we have only one type of orders, restore - corresponding virtual orders and purge opposite orders. + If we have both buy and sell real orders, restore both. If we have only one type of orders, restore + corresponding virtual orders and purge opposite orders. """ def place_further_buy_orders(): @@ -584,10 +586,11 @@ def place_further_sell_orders(): self.virtual_orders_restored = True def check_operational_depth(self, real_orders, virtual_orders): - """ Ensure proper operational depth. Replace excessive real orders or put real orders if needed. + """ + Ensure proper operational depth. Replace excessive real orders or put real orders if needed. - :param list real_orders: list of real orders - :param list virtual_orders: list of virtual orders + :param list real_orders: list of real orders + :param list virtual_orders: list of virtual orders """ num_real_orders = len(real_orders) num_virtual_orders = len(virtual_orders) @@ -613,16 +616,17 @@ def check_operational_depth(self, real_orders, virtual_orders): self.refresh_balances(use_cached_orders=True) def replace_real_order_with_virtual(self, order): - """ Replace real limit order with virtual order + """ + Replace real limit order with virtual order. - :param Order | order: market order to replace - :return bool | True = order replace success - False = order replace failed + :param Order | order: market order to replace + :return bool | True = order replace success + False = order replace failed - Logic: - 1. Cancel real order - 2. Wait until transaction included in head block - 3. Place virtual order + Logic: + 1. Cancel real order + 2. Wait until transaction included in head block + 3. Place virtual order """ success = self.cancel_orders(order) if success and order['base']['symbol'] == self.market['base']['symbol']: @@ -639,16 +643,17 @@ def replace_real_order_with_virtual(self, order): return False def replace_virtual_order_with_real(self, order): - """ Replace virtual order with real one + """ + Replace virtual order with real one. - :param Order | order: market order to replace - :return bool | True = order replace success - False = order replace failed + :param Order | order: market order to replace + :return bool | True = order replace success + False = order replace failed - Logic: - 1. Place real order instead of virtual - 2. Wait until transaction included in head block - 3. Remove existing virtual order + Logic: + 1. Place real order instead of virtual + 2. Wait until transaction included in head block + 3. Remove existing virtual order """ if order['base']['symbol'] == self.market['base']['symbol']: quote_amount = order['quote']['amount'] @@ -676,11 +681,12 @@ def replace_virtual_order_with_real(self, order): return False def store_profit_estimation_data(self, force=False): - """ Stores balance history entry if center price moved enough + """ + Stores balance history entry if center price moved enough. - :param bool | force: True = force store data, False = store data only on center price change + :param bool | force: True = force store data, False = store data only on center price change - Todo: this method is inaccurate when using single account accross multiple workers + Todo: this method is inaccurate when using single account accross multiple workers """ need_store = False account = self.config['workers'][self.worker_name].get('account') @@ -720,10 +726,11 @@ def store_profit_estimation_data(self, force=False): self.old_center_price = self.market_center_price def allocate_asset(self, asset, asset_balance): - """ Allocates available asset balance as buy or sell orders. + """ + Allocates available asset balance as buy or sell orders. - :param str | asset: 'base' or 'quote' - :param Amount | asset_balance: Amount of the asset available to use + :param str | asset: 'base' or 'quote' + :param Amount | asset_balance: Amount of the asset available to use """ self.log.debug('Need to allocate {}: {}'.format(asset, asset_balance)) closest_opposite_order = None @@ -783,10 +790,12 @@ def allocate_asset(self, asset, asset_balance): if actual_spread >= self.target_spread + self.increment: if not self.check_partial_fill(closest_own_order, fill_threshold=0): # Replace closest order if it was partially filled for any % - """ Note on partial filled orders handling: if target spread is not reached and we need to place - closer order, we need to make sure current closest order is 100% unfilled. When target spread is - reached, we are replacing order only if it was filled no less than `self.fill_threshold`. This - helps to avoid too often replacements. + """ + Note on partial filled orders handling: if target spread is not reached and we need to place closer + order, we need to make sure current closest order is 100% unfilled. + + When target spread is reached, we are replacing order only if it was filled no less than + `self.fill_threshold`. This helps to avoid too often replacements. """ self.replace_partially_filled_order(closest_own_order) return @@ -822,9 +831,8 @@ def allocate_asset(self, asset, asset_balance): if self['bootstrapping']: self.place_closer_order(asset, closest_own_order) elif opposite_orders and actual_spread - self.increment < self.target_spread + self.increment: - """ Place max-sized closer order if only one order needed to reach target spread (avoid unneeded - increases) - """ + """Place max-sized closer order if only one order needed to reach target spread (avoid unneeded + increases)""" self.place_closer_order(asset, closest_own_order, allow_partial=True) elif opposite_orders: # Place order limited by size of the opposite-side order @@ -888,9 +896,8 @@ def allocate_asset(self, asset, asset_balance): else: # Target spread is reached, let's allocate remaining funds if not self.check_partial_fill(closest_own_order, fill_threshold=0): - """ Detect partially filled order on the own side and reserve funds to replace order in case - opposite order will be fully filled. - """ + """Detect partially filled order on the own side and reserve funds to replace order in case opposite + order will be fully filled.""" funds_to_reserve = closest_own_order['base']['amount'] self.log.debug( 'Partially filled order on own side, reserving funds to replace: ' @@ -899,9 +906,12 @@ def allocate_asset(self, asset, asset_balance): asset_balance -= funds_to_reserve if not self.check_partial_fill(closest_opposite_order, fill_threshold=0): - """ Detect partially filled order on the opposite side and reserve appropriate amount to place - closer order. We adding some additional reserve to be able to place next order whether - new allocation round will be started, this is mostly for valley-like modes. + """ + Detect partially filled order on the opposite side and reserve appropriate amount to place closer + order. + + We adding some additional reserve to be able to place next order whether new allocation round will + be started, this is mostly for valley-like modes. """ funds_to_reserve = 0 additional_reserve = max(1 + self.increment, self.min_increase_factor) * 1.05 @@ -939,17 +949,18 @@ def allocate_asset(self, asset, asset_balance): and not self.check_partial_fill(closest_own_order) and not self.check_partial_fill(closest_opposite_order, fill_threshold=0) ): - """ Replace partially filled closest orders only when allocation of excess funds was finished. This - would prevent an abuse case when we are operating inactive market. An attacker can massively dump - the price and then he can buy back the asset cheaper. Similar case may happen on the "normal" market - on significant price drops or spikes. + """ + Replace partially filled closest orders only when allocation of excess funds was finished. This would + prevent an abuse case when we are operating inactive market. An attacker can massively dump the price + and then he can buy back the asset cheaper. Similar case may happen on the "normal" market on + significant price drops or spikes. - The logic how it works is following: - 1. If we have partially filled closest orders, reserve fuds to replace them later - 2. If we have excess funds, allocate them by increasing order sizes or expand bounds if needed - 3. When increase is finished, replace partially filled closest orders + The logic how it works is following: + 1. If we have partially filled closest orders, reserve fuds to replace them later + 2. If we have excess funds, allocate them by increasing order sizes or expand bounds if needed + 3. When increase is finished, replace partially filled closest orders - Thus we are don't need to precisely count how much was filled on closest orders. + Thus we are don't need to precisely count how much was filled on closest orders. """ # Refresh balances to make "reserved" funds available self.refresh_balances(use_cached_orders=True) @@ -990,15 +1001,16 @@ def allocate_asset(self, asset, asset_balance): self.refresh_orders() def _increase_single_order(self, asset, asset_balance, order, new_order_amount): - """ To avoid code doubling, use this unified function to increase single order - - :param str asset: 'base' or 'quote', depending if checking sell or buy - :param Amount asset_balance: asset balance available for increase - :param order order: order needed to be increased - :param float new_order_amount: BASE or QUOTE amount of a new order (depending on asset) - :return: True = available funds were allocated, cannot allocate remainder - False = not all funds were allocated, can increase more orders next time - :rtype: bool + """ + To avoid code doubling, use this unified function to increase single order. + + :param str asset: 'base' or 'quote', depending if checking sell or buy + :param Amount asset_balance: asset balance available for increase + :param order order: order needed to be increased + :param float new_order_amount: BASE or QUOTE amount of a new order (depending on asset) + :return: True = available funds were allocated, cannot allocate remainder + False = not all funds were allocated, can increase more orders next time + :rtype: bool """ quote_amount = 0 base_amount = 0 @@ -1067,15 +1079,16 @@ def _increase_single_order(self, asset, asset_balance, order, new_order_amount): return False def _calc_increase(self, asset, asset_balance, orders): - """ Calculate increased order sizes for specified orders with inplace replacement of order amounts. - Only one increase is performed at a time. - - :param str asset: 'base' or 'quote', depending if checking sell or buy - :param Amount asset_balance: Balance of the account - :param list orders: List of buy or sell orders - :return: True = all available funds were allocated - False = not all funds was allocated, can increase more orders next time - :rtype: bool + """ + Calculate increased order sizes for specified orders with inplace replacement of order amounts. Only one + increase is performed at a time. + + :param str asset: 'base' or 'quote', depending if checking sell or buy + :param Amount asset_balance: Balance of the account + :param list orders: List of buy or sell orders + :return: True = all available funds were allocated + False = not all funds was allocated, can increase more orders next time + :rtype: bool """ new_order_amount = 0 @@ -1093,20 +1106,19 @@ def _calc_increase(self, asset, asset_balance, orders): or (self.mode == 'buy_slope' and asset == 'quote') or (self.mode == 'sell_slope' and asset == 'base') ): - """ Starting from the furthest order. For each order, see if it is approximately - maximum size. - If it is, move on to next. - If not, cancel it and replace with maximum size order. Then return. - If highest_sell_order is reached, increase it to maximum size + """ + Starting from the furthest order. For each order, see if it is approximately maximum size. If it is, move on + to next. If not, cancel it and replace with maximum size order. Then return. If highest_sell_order is + reached, increase it to maximum size. - Maximum size is: - 1. As many "amount * (1 + increment)" as the order further (further_bound) - AND - 2. As many "amount" as the order closer to center (closer_bound) + Maximum size is: + 1. As many "amount * (1 + increment)" as the order further (further_bound) + AND + 2. As many "amount" as the order closer to center (closer_bound) - Note: for buy orders "amount" is BASE asset amount, and for sell order "amount" is QUOTE. + Note: for buy orders "amount" is BASE asset amount, and for sell order "amount" is QUOTE. - Also when making an order it's size always will be limited by available free balance + Also when making an order it's size always will be limited by available free balance """ # Get orders and amounts to be compared. Note: orders are sorted from low price to high for order in orders: @@ -1139,18 +1151,20 @@ def _calc_increase(self, asset, asset_balance, orders): and further_bound - order_amount >= order_amount * self.increment / 2 ): # Calculate new order size and place the order to the market - """ To prevent moving liquidity away from center, let new order be no more than `order_amount * - increase_factor`. This is for situations when we increasing order on side which was previously - bigger. Example: buy side, amounts in QUOTE: - [1000 1000 1000 100 100 100
] - - Without increase_factor: - [1000 1000 1000 1000 100 100
] - - With increase_factor: - [1000 1000 1000 200 100 100
] - [1000 1000 1000 200 200 100
] - [1000 1000 1000 200 200 200
] + """ + To prevent moving liquidity away from center, let new order be no more than `order_amount * + increase_factor`. This is for situations when we increasing order on side which was previously + bigger. Example: buy side, amounts in QUOTE: + + [1000 1000 1000 100 100 100
] + + Without increase_factor: + [1000 1000 1000 1000 100 100
] + + With increase_factor: + [1000 1000 1000 200 100 100
] + [1000 1000 1000 200 200 100
] + [1000 1000 1000 200 200 200
] """ new_order_amount = further_bound increase_factor = max(1 + self.increment, self.min_increase_factor) @@ -1164,16 +1178,14 @@ def _calc_increase(self, asset, asset_balance, orders): or (self.mode == 'buy_slope' and asset == 'base') or (self.mode == 'sell_slope' and asset == 'quote') ): - """ Starting from the furthest order, for each order, see if it is approximately - maximum size. - If it is, move on to next. - If not, cancel it and replace with maximum size order. Maximum order size will be a - size of closer-to-center order. Then return. - If furthest is reached, increase it to maximum size. - - Maximum size is (example for buy orders): - 1. As many "base" as the further order (further_order_bound) - 2. As many "base" as the order closer to center (closer_order_bound) + """ + Starting from the furthest order, for each order, see if it is approximately maximum size. If it is, move on + to next. If not, cancel it and replace with maximum size order. Maximum order size will be a size of closer- + to-center order. Then return. If furthest is reached, increase it to maximum size. + + Maximum size is (example for buy orders): + 1. As many "base" as the further order (further_order_bound) + 2. As many "base" as the order closer to center (closer_order_bound) """ orders_count = len(orders) orders = list(reversed(orders)) @@ -1202,10 +1214,11 @@ def _calc_increase(self, asset, asset_balance, orders): closer_order = orders[order_index + 1] closer_order_bound = closer_order['base']['amount'] else: - """ Special processing for the closest order. + """ + Special processing for the closest order. - Calculate new order amount based on orders count, but do not allow to perform too small - increase rounds. New lowest buy / highest sell should be higher by at least one increment. + Calculate new order amount based on orders count, but do not allow to perform too small increase + rounds. New lowest buy / highest sell should be higher by at least one increment. """ closer_order_bound = closest_order_bound new_amount = (total_balance / orders_count) / (1 + self.increment / 100) @@ -1247,20 +1260,21 @@ def _calc_increase(self, asset, asset_balance, orders): and order_amount_normalized < closest_order_bound and closer_order_bound - order_amount >= order_amount * self.increment / 2 ): - """ Check whether order amount is less than closer or order and the diff is more than 50% of one - increment. Note: we can use only 50% or less diffs. Bigger will not work. For example, with - diff 80% an order may have an actual difference like 30% from closer and 70% from further. - - Also prevent moving liqudity away from closer-to-center orders. Instead of increasing "80" - orders, increase closer-to-center orders first: - - [80 80 80 100 100 100 60 50 40 40] - [80 80 80 100 100 100 60 50 50 40] - [80 80 80 100 100 100 60 50 50 50] - ... - [80 80 80 100 100 100 60 60 60 60] - ... - [80 80 80 100 100 100 80 80 80 80] + """ + Check whether order amount is less than closer or order and the diff is more than 50% of one + increment. Note: we can use only 50% or less diffs. Bigger will not work. For example, with diff 80% + an order may have an actual difference like 30% from closer and 70% from further. + + Also prevent moving liqudity away from closer-to-center orders. Instead of increasing "80" + orders, increase closer-to-center orders first: + + [80 80 80 100 100 100 60 50 40 40] + [80 80 80 100 100 100 60 50 50 40] + [80 80 80 100 100 100 60 50 50 50] + ... + [80 80 80 100 100 100 60 60 60 60] + ... + [80 80 80 100 100 100 80 80 80 80] """ new_order_amount = min(closest_order_bound, closer_order_bound) need_increase = True @@ -1269,16 +1283,14 @@ def _calc_increase(self, asset, asset_balance, orders): return self._increase_single_order(asset, asset_balance, order, new_order_amount) elif self.mode == 'neutral': - """ Starting from the furthest order, for each order, see if it is approximately - maximum size. - If it is, move on to next. - If not, cancel it and replace with maximum size order. Maximum order size will be a - size of closer-to-center order. Then return. - If furthest is reached, increase it to maximum size. - - Maximum size is (example for buy orders): - 1. As many "base * sqrt(1 + increment)" as the further order (further_order_bound) - 2. As many "base / sqrt(1 + increment)" as the order closer to center (closer_order_bound) + """ + Starting from the furthest order, for each order, see if it is approximately maximum size. If it is, move on + to next. If not, cancel it and replace with maximum size order. Maximum order size will be a size of closer- + to-center order. Then return. If furthest is reached, increase it to maximum size. + + Maximum size is (example for buy orders): + 1. As many "base * sqrt(1 + increment)" as the further order (further_order_bound) + 2. As many "base / sqrt(1 + increment)" as the order closer to center (closer_order_bound) """ orders_count = len(orders) @@ -1347,38 +1359,38 @@ def _calc_increase(self, asset, asset_balance, orders): return self._increase_single_order(asset, asset_balance, order, new_order_amount) def increase_order_sizes(self, asset, asset_balance, orders): - """ Checks which order should be increased in size and replaces it - with a maximum size order, according to global limits. Logic - depends on mode in question. - - Mountain: - Maximize order size as close to center as possible. When all orders are max, the new increase round is - started from the furthest order. - - Neutral: - Try to flatten everything by increasing order sizes to neutral. When everything is correct, maximize - closest orders and then increase other orders to match that. - - Valley: - Maximize order sizes as far as possible from center first. When all orders are max, the new increase round - is started from the closest-to-center order. - - Buy slope: - Maximize order size as low as possible. Buy orders maximized as far as possible (same as valley), and sell - orders as close as possible to cp (same as mountain). - - Sell slope: - Maximize order size as high as possible. Buy orders as close (same as mountain), and sell orders as far as - possible from cp (same as valley). - - :param str asset: 'base' or 'quote', depending if checking sell or buy - :param Amount asset_balance: Balance of the account - :param list orders: List of buy or sell orders - :return: status of funds allocation - done = all funds were allocated - done_with_ops = all funds were allocated, operations are pending in txbuffer - in_progress = not all funds were allocated, can increase more orders next time - :rtype: str + """ + Checks which order should be increased in size and replaces it with a maximum size order, according to global + limits. Logic depends on mode in question. + + Mountain: + Maximize order size as close to center as possible. When all orders are max, the new increase round is + started from the furthest order. + + Neutral: + Try to flatten everything by increasing order sizes to neutral. When everything is correct, maximize + closest orders and then increase other orders to match that. + + Valley: + Maximize order sizes as far as possible from center first. When all orders are max, the new increase round + is started from the closest-to-center order. + + Buy slope: + Maximize order size as low as possible. Buy orders maximized as far as possible (same as valley), and sell + orders as close as possible to cp (same as mountain). + + Sell slope: + Maximize order size as high as possible. Buy orders as close (same as mountain), and sell orders as far as + possible from cp (same as valley). + + :param str asset: 'base' or 'quote', depending if checking sell or buy + :param Amount asset_balance: Balance of the account + :param list orders: List of buy or sell orders + :return: status of funds allocation + done = all funds were allocated + done_with_ops = all funds were allocated, operations are pending in txbuffer + in_progress = not all funds were allocated, can increase more orders next time + :rtype: str """ # Create temp order list (copy.deepcopy() doesn't work here) @@ -1474,12 +1486,13 @@ def increase_order_sizes(self, asset, asset_balance, orders): return 'done' def check_partial_fill(self, order, fill_threshold=None): - """ Checks whether order was partially filled it needs to be replaced + """ + Checks whether order was partially filled it needs to be replaced. - :param dict | order: Order closest to the center price from buy or sell side - :param float | fill_threshold: Order fill threshold, relative - :return: bool | True = Order is correct size or within the threshold - False = Order is not right size + :param dict | order: Order closest to the center price from buy or sell side + :param float | fill_threshold: Order fill threshold, relative + :return: bool | True = Order is correct size or within the threshold + False = Order is not right size """ if fill_threshold is None: fill_threshold = self.partial_fill_threshold @@ -1504,9 +1517,10 @@ def check_partial_fill(self, order, fill_threshold=None): return True def replace_partially_filled_order(self, order): - """ Replace partially filled order + """ + Replace partially filled order. - :param order: Order instance + :param order: Order instance """ if order['base']['symbol'] == self.market['base']['symbol']: @@ -1541,15 +1555,15 @@ def replace_partially_filled_order(self, order): def place_closer_order( self, asset, order, place_order=True, allow_partial=False, own_asset_limit=None, opposite_asset_limit=None ): - """ Place order closer to the center - - :param asset: - :param order: Previously closest order - :param bool | place_order: True = Places order to the market, False = returns amount and price - :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance - :param float | own_asset_limit: order should be limited in size by amount of order's "base" - :param float | opposite_asset_limit: order should be limited in size by order's "quote" amount - + """ + Place order closer to the center. + + :param asset: + :param order: Previously closest order + :param bool | place_order: True = Places order to the market, False = returns amount and price + :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance + :param float | own_asset_limit: order should be limited in size by amount of order's "base" + :param float | opposite_asset_limit: order should be limited in size by order's "quote" amount """ if own_asset_limit and opposite_asset_limit: self.log.error('Only own_asset_limit or opposite_asset_limit should be specified') @@ -1707,13 +1721,14 @@ def place_closer_order( return new_order def place_further_order(self, asset, order, place_order=True, allow_partial=False, virtual=False): - """ Place order further from specified order + """ + Place order further from specified order. - :param asset: - :param order: furthest buy or sell order - :param bool | place_order: True = Places order to the market, False = returns amount and price - :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance - :param bool | virtual: True = Force place a virtual order + :param asset: + :param order: furthest buy or sell order + :param bool | place_order: True = Places order to the market, False = returns amount and price + :param bool | allow_partial: True = Allow to downsize order whether there is not enough balance + :param bool | virtual: True = Force place a virtual order """ balance = 0 order_type = '' @@ -1829,12 +1844,13 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals return new_order def place_highest_sell_order(self, quote_balance, place_order=True, market_center_price=None): - """ Places sell order furthest to the market center price + """ + Places sell order furthest to the market center price. - :param Amount | quote_balance: Available QUOTE asset balance - :param bool | place_order: True = Places order to the market, False = returns amount and price - :param float | market_center_price: Optional market center price, used to to check order - :return dict | order: Returns highest sell order + :param Amount | quote_balance: Available QUOTE asset balance + :param bool | place_order: True = Places order to the market, False = returns amount and price + :param float | market_center_price: Optional market center price, used to to check order + :return dict | order: Returns highest sell order """ if not market_center_price: market_center_price = self.market_center_price @@ -1925,42 +1941,43 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente return order def place_lowest_buy_order(self, base_balance, place_order=True, market_center_price=None): - """ Places buy order furthest to the market center price - - Turn BASE amount into QUOTE amount (we will buy this QUOTE amount). - QUOTE = BASE / price - - Furthest order amount calculations: - ----------------------------------- - - Mountain: - For asset to be allocated (base for buy and quote for sell orders) - First order (furthest) = balance * increment - Next order = previous order / (1 + increment) - Repeat until last order. - - Neutral: - For asset to be allocated (base for buy and quote for sell orders) - First order (furthest) = balance * (sqrt(1 + increment) - 1) - Next order = previous order / sqrt(1 + increment) - Repeat until last order - - Valley: - For asset to be allocated (base for buy and quote for sell orders) - All orders = balance / number of orders (per side) - - Buy slope: - Buy orders same as valley - Sell orders same as mountain - - Sell slope: - Buy orders same as mountain - Sell orders same as valley - - :param Amount | base_balance: Available BASE asset balance - :param bool | place_order: True = Places order to the market, False = returns amount and price - :param float | market_center_price: Optional market center price, used to to check order - :return dict | order: Returns lowest buy order + """ + Places buy order furthest to the market center price. + + Turn BASE amount into QUOTE amount (we will buy this QUOTE amount). + QUOTE = BASE / price + + Furthest order amount calculations: + ----------------------------------- + + Mountain: + For asset to be allocated (base for buy and quote for sell orders) + First order (furthest) = balance * increment + Next order = previous order / (1 + increment) + Repeat until last order. + + Neutral: + For asset to be allocated (base for buy and quote for sell orders) + First order (furthest) = balance * (sqrt(1 + increment) - 1) + Next order = previous order / sqrt(1 + increment) + Repeat until last order + + Valley: + For asset to be allocated (base for buy and quote for sell orders) + All orders = balance / number of orders (per side) + + Buy slope: + Buy orders same as valley + Sell orders same as mountain + + Sell slope: + Buy orders same as mountain + Sell orders same as valley + + :param Amount | base_balance: Available BASE asset balance + :param bool | place_order: True = Places order to the market, False = returns amount and price + :param float | market_center_price: Optional market center price, used to to check order + :return dict | order: Returns lowest buy order """ if not market_center_price: market_center_price = self.market_center_price @@ -2054,11 +2071,12 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p return order def calc_buy_orders_count(self, price_high, price_low): - """ Calculate number of buy orders to place between high price and low price + """ + Calculate number of buy orders to place between high price and low price. - :param float | price_high: Highest buy price bound - :param float | price_low: Lowest buy price bound - :return int | count: Returns number of orders + :param float | price_high: Highest buy price bound + :param float | price_low: Lowest buy price bound + :return int | count: Returns number of orders """ orders_count = 0 while price_high >= price_low: @@ -2067,11 +2085,12 @@ def calc_buy_orders_count(self, price_high, price_low): return orders_count def calc_sell_orders_count(self, price_low, price_high): - """ Calculate number of sell orders to place between low price and high price + """ + Calculate number of sell orders to place between low price and high price. - :param float | price_low: Lowest sell price bound - :param float | price_high: Highest sell price bound - :return int | count: Returns number of orders + :param float | price_low: Lowest sell price bound + :param float | price_high: Highest sell price bound + :return int | count: Returns number of orders """ orders_count = 0 while price_low <= price_high: @@ -2080,11 +2099,12 @@ def calc_sell_orders_count(self, price_low, price_high): return orders_count def check_min_order_size(self, amount, price): - """ Check if order size is less than minimal allowed size + """ + Check if order size is less than minimal allowed size. - :param float | amount: Order amount in QUOTE - :param float | price: Order price in BASE - :return float | new_amount: passed amount or minimal allowed amount + :param float | amount: Order amount in QUOTE + :param float | price: Order price in BASE + :return float | new_amount: passed amount or minimal allowed amount """ # Calculate minimal orders amounts based on asset precision if not self.order_min_base or not self.order_min_quote: @@ -2100,11 +2120,12 @@ def check_min_order_size(self, amount, price): return amount def place_virtual_buy_order(self, amount, price): - """ Place a virtual buy order + """ + Place a virtual buy order. - :param float | amount: Order amount in QUOTE - :param float | price: Order price in BASE - :return dict | order: Returns virtual order instance + :param float | amount: Order amount in QUOTE + :param float | price: Order price in BASE + :return dict | order: Returns virtual order instance """ symbol = self.market['base']['symbol'] order = VirtualOrder() @@ -2135,11 +2156,12 @@ def place_virtual_buy_order(self, amount, price): return order def place_virtual_sell_order(self, amount, price): - """ Place a virtual sell order + """ + Place a virtual sell order. - :param float | amount: Order amount in QUOTE - :param float | price: Order price in BASE - :return dict | order: Returns virtual order instance + :param float | amount: Order amount in QUOTE + :param float | price: Order price in BASE + :return dict | order: Returns virtual order instance """ symbol = self.market['quote']['symbol'] order = VirtualOrder() @@ -2170,8 +2192,10 @@ def place_virtual_sell_order(self, amount, price): return order def cancel_orders_wrapper(self, orders, **kwargs): - """ Cancel specific order(s) - :param list orders: list of orders to cancel + """ + Cancel specific order(s) + + :param list orders: list of orders to cancel """ if not isinstance(orders, (list, set, tuple)): orders = [orders] @@ -2195,7 +2219,7 @@ def error(self, *args, **kwargs): self.disabled = True def pause(self): - """ Override pause() """ + """Override pause()""" def purge(self): """ We are not cancelling orders on save/remove worker from the GUI @@ -2203,15 +2227,14 @@ def purge(self): """ def tick(self, d): - """ Ticks come in on every block """ + """Ticks come in on every block.""" if not (self.counter or 0) % 3: self.maintain_strategy() self.counter += 1 class VirtualOrder(dict): - """ Wrapper class to handle virtual orders comparison in list index() method - """ + """Wrapper class to handle virtual orders comparison in list index() method.""" def __float__(self): return self['price'] diff --git a/dexbot/strategies/strategy_template.py b/dexbot/strategies/strategy_template.py index 5263b657f..356d3bb30 100644 --- a/dexbot/strategies/strategy_template.py +++ b/dexbot/strategies/strategy_template.py @@ -12,33 +12,34 @@ class Strategy(StrategyBase): - """ + """ + - Replace with the name of the strategy. + Replace with the name of the strategy. - This is a template strategy which can be used to create custom strategies easier. The base for the strategy is - ready. It is recommended comment the strategy and functions to help other developers to make changes. + This is a template strategy which can be used to create custom strategies easier. The base for the strategy is + ready. It is recommended comment the strategy and functions to help other developers to make changes. - Adding strategy to GUI - In dexbot.controller.worker_controller add new strategy inside strategies() as show below: + Adding strategy to GUI + In dexbot.controller.worker_controller add new strategy inside strategies() as show below: - strategies['dexbot.strategies.strategy_template'] = { - 'name': '', - 'form_module': '' - } + strategies['dexbot.strategies.strategy_template'] = { + 'name': '', + 'form_module': '' + } - key: Strategy location in the project - name: The name that is shown in the GUI for user - form_module: If there is custom form module created with QTDesigner + key: Strategy location in the project + name: The name that is shown in the GUI for user + form_module: If there is custom form module created with QTDesigner - Adding strategy to CLI - In dexbot.cli_conf add strategy in to the STRATEGIES list + Adding strategy to CLI + In dexbot.cli_conf add strategy in to the STRATEGIES list - {'tag': 'strategy_temp', - 'class': 'dexbot.strategies.strategy_template', - 'name': 'Template Strategy'}, + {'tag': 'strategy_temp', + 'class': 'dexbot.strategies.strategy_template', + 'name': 'Template Strategy'}, - NOTE: Change this comment section to describe the strategy. + NOTE: Change this comment section to describe the strategy. """ @classmethod @@ -102,37 +103,37 @@ def __init__(self, *args, **kwargs): self.log.info("{} initialized.".format(STRATEGY_NAME)) def maintain_strategy(self): - """ Strategy main loop - - This method contains the strategy's logic. Keeping this function as simple as possible is recommended. + """ + Strategy main loop. - Note: All orders are "buy" orders, since they are flipped to be easier to handle. Keep them separated to - avoid confusion on problems. + This method contains the strategy's logic. Keeping this function as simple as possible is recommended. - Placing an order to the market has been made simple. Placing a buy order for example requires two values: - Amount (as QUOTE asset) and price (which is BASE amount divided by QUOTE amount) + Note: All orders are "buy" orders, since they are flipped to be easier to handle. Keep them separated to + avoid confusion on problems. - "Placing to buy 100 ASSET_A with price of 10 ASSET_A / ASSET_B" would be place_market_buy_order(100, 10). - This would then cost 1000 USD to fulfil. + Placing an order to the market has been made simple. Placing a buy order for example requires two values: + Amount (as QUOTE asset) and price (which is BASE amount divided by QUOTE amount) - Further documentation can be found from the function's documentation. + "Placing to buy 100 ASSET_A with price of 10 ASSET_A / ASSET_B" would be place_market_buy_order(100, 10). + This would then cost 1000 USD to fulfil. + Further documentation can be found from the function's documentation. """ # Start writing strategy logic from here. self.log.info("Starting {}".format(STRATEGY_NAME)) def check_orders(self, *args, **kwargs): - """ """ + """""" def error(self, *args, **kwargs): - """ Defines what happens when error occurs """ + """Defines what happens when error occurs.""" self.disabled = True def pause(self): - """ Override pause() in StrategyBase """ + """Override pause() in StrategyBase.""" def tick(self, d): - """ Ticks come in on every block """ + """Ticks come in on every block.""" if not (self.counter or 0) % 3: self.maintain_strategy() self.counter += 1 diff --git a/dexbot/styles.py b/dexbot/styles.py index 36f1755a9..aef2ac79b 100644 --- a/dexbot/styles.py +++ b/dexbot/styles.py @@ -1,5 +1,4 @@ -""" This is helper file to print out strings in different colours -""" +"""This is helper file to print out strings in different colours.""" def style(value, styling): diff --git a/dexbot/views/errors.py b/dexbot/views/errors.py index b4ac0f40b..497e329e2 100644 --- a/dexbot/views/errors.py +++ b/dexbot/views/errors.py @@ -11,6 +11,7 @@ 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) """ diff --git a/dexbot/views/strategy_form.py b/dexbot/views/strategy_form.py index 3f137eb4a..3aeaa7e88 100644 --- a/dexbot/views/strategy_form.py +++ b/dexbot/views/strategy_form.py @@ -42,14 +42,12 @@ def __init__(self, controller, strategy_module, worker_config=None): @property def values(self): - """ Returns all the form values based on the selected strategy - """ + """Returns all the form values based on the selected strategy.""" return self.strategy_controller.values class AutoStrategyFormGenerator: - """ Automatic strategy form UI generator - """ + """Automatic strategy form UI generator.""" def __init__(self, view, configure): self.view = view diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index fcaaf7b37..aa9673a34 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -153,8 +153,7 @@ def remove_widget(self): self.deleteLater() def reload_widget(self, worker_name): - """ Reload the data of the widget - """ + """Reload the data of the widget.""" self.worker_config = self.main_ctrl.config.get_worker_config(worker_name) self.setup_ui_data(self.worker_config) self._pause_worker() diff --git a/dexbot/whiptail.py b/dexbot/whiptail.py index d64fd3bdc..2f699609e 100644 --- a/dexbot/whiptail.py +++ b/dexbot/whiptail.py @@ -103,7 +103,7 @@ def checklist(self, msg='', items=(), prefix=' - '): return self.showlist('checklist', msg, items, prefix) def view_text(self, text, **kwargs): - """Whiptail wants a file but we want to provide a text string""" + """Whiptail wants a file but we want to provide a text string.""" fd, nam = tempfile.mkstemp() f = os.fdopen(fd, 'w') f.write(text) @@ -118,10 +118,9 @@ def clear(self): class NoWhiptail: """ - Imitates the interface of whiptail but uses click only + Imitates the interface of whiptail but uses click only. - This is very basic CLI: real state-of-the-1970s stuff, - but it works *everywhere* + This is very basic CLI: real state-of-the-1970s stuff, but it works *everywhere* """ def prompt(self, msg, default='', password=False): @@ -162,8 +161,7 @@ def radiolist(self, msg='', items=()): return self.menu(msg, [(k, v) for k, v, s in items], default=default) def node_radiolist(self, *args, **kwargs): - """ Proxy stub to maintain compatibility with Whiptail class - """ + """Proxy stub to maintain compatibility with Whiptail class.""" return self.radiolist(*args, **kwargs) def clear(self): diff --git a/dexbot/worker.py b/dexbot/worker.py index b1452002a..8c19ee51d 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -42,8 +42,7 @@ def __init__(self, config, bitshares_instance=None, view=None): sys.path.append(user_worker_path) def init_workers(self, config): - """ Initialize the workers - """ + """Initialize the workers.""" self.config_lock.acquire() for worker_name, worker in config["workers"].items(): if "account" not in worker: @@ -200,8 +199,7 @@ def run(self): self.notify.listen() def check_node_time(self): - """ Check that we're connected to synced node - """ + """Check that we're connected to synced node.""" props = self.bitshares.info() current_time = parse_time(props['time']) @@ -217,10 +215,11 @@ def check_node_time(self): self.block_time = current_time def stop(self, worker_name=None, pause=False): - """ Used to stop the worker(s) + """ + Used to stop the worker(s) - :param str worker_name: name of the worker to stop - :param bool pause: optional argument which tells worker if it was stopped or just paused + :param str worker_name: name of the worker to stop + :param bool pause: optional argument which tells worker if it was stopped or just paused """ if worker_name: try: @@ -267,8 +266,7 @@ def remove_worker(self, worker_name=None): self.workers[worker].purge() def remove_market(self, worker_name): - """ Remove the market only if the worker is the only one using it - """ + """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(): @@ -289,5 +287,5 @@ def remove_offline_worker_data(worker_name): StrategyBase.purge_all_local_worker_data(worker_name) 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) diff --git a/tests/conftest.py b/tests/conftest.py index 2ae5b7cf6..c761bdb7e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,17 +33,18 @@ def default_account(): @pytest.fixture(scope='session') def session_id(): - """ Generate unique session id. This is needed in case testsuite may run in parallel on the same server, for example - if CI/CD is being used. CI/CD infrastructure may run tests for each commit, so these tests should not influence - each other. + """ + Generate unique session id. + + This is needed in case testsuite may run in parallel on the same server, for example if CI/CD is being used. CI/CD + infrastructure may run tests for each commit, so these tests should not influence each other. """ return str(uuid.uuid4()) @pytest.fixture(scope='session') def unused_port(): - """ Obtain unused port to bind some service - """ + """Obtain unused port to bind some service.""" def _unused_port(): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: @@ -55,17 +56,17 @@ def _unused_port(): @pytest.fixture(scope='session') def docker_manager(): - """ Initialize docker management client - """ + """Initialize docker management client.""" return docker.from_env(version='auto') @pytest.fixture(scope='session') def bitshares_testnet(session_id, unused_port, docker_manager): - """ Run bitshares-core inside local docker container + """ + Run bitshares-core inside local docker container. - Manual run example: - $ docker run --name bitshares -p 0.0.0.0:8091:8091 -v `pwd`/cfg:/etc/bitshares/ bitshares/bitshares-core:testnet + Manual run example: $ docker run --name bitshares -p 0.0.0.0:8091:8091 -v `pwd`/cfg:/etc/bitshares/ + bitshares/bitshares-core:testnet """ port = unused_port() container = docker_manager.containers.run( @@ -89,8 +90,7 @@ def bitshares_testnet(session_id, unused_port, docker_manager): @pytest.fixture(scope='session') def bitshares_instance(bitshares_testnet): - """ Initialize BitShares instance connected to a local testnet - """ + """Initialize BitShares instance connected to a local testnet.""" bitshares = BitShares( node='ws://127.0.0.1:{}'.format(bitshares_testnet.service_port), keys=PRIVATE_KEYS, num_retries=10 ) @@ -105,23 +105,20 @@ def bitshares_instance(bitshares_testnet): @pytest.fixture(scope='session') def claim_balance(bitshares_instance, default_account): - """ Transfer balance from genesis into actual account - """ + """Transfer balance from genesis into actual account.""" genesis_balance = GenesisBalance('1.15.0', bitshares_instance=bitshares_instance) genesis_balance.claim(account=default_account) @pytest.fixture(scope='session') def bitshares(bitshares_instance, claim_balance): - """ Prepare the testnet and return BitShares instance - """ + """Prepare the testnet and return BitShares instance.""" return bitshares_instance @pytest.fixture(scope='session') def create_asset(bitshares, default_account): - """ Create a new asset - """ + """Create a new asset.""" def _create_asset(asset, precision, is_bitasset=False): max_supply = 1000000000000000 / 10 ** precision if precision > 0 else 1000000000000000 @@ -134,11 +131,12 @@ def _create_asset(asset, precision, is_bitasset=False): @pytest.fixture(scope='session') def issue_asset(bitshares): - """ Issue asset shares to specified account + """ + Issue asset shares to specified account. - :param str asset: asset symbol to issue - :param float amount: amount to issue - :param str to: account name to receive new shares + :param str asset: asset symbol to issue + :param float amount: amount to issue + :param str to: account name to receive new shares """ def _issue_asset(asset, amount, to): @@ -171,8 +169,7 @@ def func(): @pytest.fixture(scope='session') def create_account(bitshares, default_account): - """ Create new account - """ + """Create new account.""" def _create_account(account): parent_account = Account(default_account, bitshares_instance=bitshares) @@ -193,8 +190,7 @@ def _create_account(account): @pytest.fixture(scope='session') def unused_account(bitshares): - """ Find unexistent account - """ + """Find unexistent account.""" def _unused_account(): _range = 100000 @@ -223,14 +219,15 @@ def func(): @pytest.fixture(scope='session') def prepare_account(bitshares, unused_account, create_account, create_asset, issue_asset, default_account): - """ Ensure an account with specified amounts of assets. Account must not exist! + """ + Ensure an account with specified amounts of assets. Account must not exist! - :param dict assets: assets to credit account balance with - :param str account: (optional) account name to prepare (default: generate random account name) - :return: account name - :rtype: str + :param dict assets: assets to credit account balance with + :param str account: (optional) account name to prepare (default: generate random account name) + :return: account name + :rtype: str - Example assets: {'FOO': 1000, 'BAR': 5000} + Example assets: {'FOO': 1000, 'BAR': 5000} """ def _prepare_account(assets, account=None): diff --git a/tests/gecko_test.py b/tests/gecko_test.py index 6ce64f441..9c0b2f277 100644 --- a/tests/gecko_test.py +++ b/tests/gecko_test.py @@ -23,9 +23,7 @@ def main(): @main.command() @click.argument('symbol') def test_feed(symbol): - """ - [symbol] Symbol example: btc/usd or btc:usd - """ + """[symbol] Symbol example: btc/usd or btc:usd.""" try: price = get_gecko_price(symbol_=symbol) print(price) diff --git a/tests/migrations/test_migrations.py b/tests/migrations/test_migrations.py index e12520c20..91caf2800 100644 --- a/tests/migrations/test_migrations.py +++ b/tests/migrations/test_migrations.py @@ -4,13 +4,11 @@ @pytest.mark.mandatory def test_apply_migrations_fresh(fresh_db): - """ Test fresh installation - """ + """Test fresh installation.""" DatabaseWorker.run_migrations('dexbot/migrations', 'sqlite:///{}'.format(fresh_db)) @pytest.mark.mandatory def test_apply_migrations_historic(historic_db): - """ Test transition of old installation before alembic - """ + """Test transition of old installation before alembic.""" DatabaseWorker.run_migrations('dexbot/migrations', 'sqlite:///{}'.format(historic_db)) diff --git a/tests/strategies/king_of_the_hill/conftest.py b/tests/strategies/king_of_the_hill/conftest.py index 55022e5f6..c64a9a5ae 100644 --- a/tests/strategies/king_of_the_hill/conftest.py +++ b/tests/strategies/king_of_the_hill/conftest.py @@ -17,8 +17,7 @@ @pytest.fixture(scope='session') def assets(create_asset): - """ Create some assets with different precision - """ + """Create some assets with different precision.""" create_asset('BASEA', 3) create_asset('QUOTEA', 8) create_asset('BASEB', 8) @@ -27,15 +26,13 @@ def assets(create_asset): @pytest.fixture(scope='module') def kh_worker_name(): - """ Fixture to share king_of_the_hill Orders worker name - """ + """Fixture to share king_of_the_hill Orders worker name.""" return 'kh-worker' @pytest.fixture(scope='module') def base_account(assets, prepare_account): - """ Factory to generate random account with pre-defined balances - """ + """Factory to generate random account with pre-defined balances.""" def func(): account = prepare_account({'BASEA': 10000, 'QUOTEA': 100, 'BASEB': 10000, 'QUOTEB': 100, 'TEST': 1000}) @@ -46,16 +43,16 @@ def func(): @pytest.fixture(scope='module') def account(base_account): - """ Prepare worker account with some balance - """ + """Prepare worker account with some balance.""" return base_account() @pytest.fixture(params=[('QUOTEA', 'BASEA'), ('QUOTEB', 'BASEB')]) def config(request, bitshares, account, kh_worker_name): - """ Define worker's config with variable assets + """ + Define worker's config with variable assets. - This fixture should be function-scoped to use new fresh bitshares account for each test + This fixture should be function-scoped to use new fresh bitshares account for each test """ worker_name = kh_worker_name config = { @@ -83,8 +80,7 @@ def config(request, bitshares, account, kh_worker_name): @pytest.fixture(params=MODES) def config_variable_modes(request, config, kh_worker_name): - """ Test config which tests all modes - """ + """Test config which tests all modes.""" worker_name = kh_worker_name config = copy.deepcopy(config) config['workers'][worker_name]['mode'] = request.param @@ -93,8 +89,7 @@ def config_variable_modes(request, config, kh_worker_name): @pytest.fixture(params=[0, 1]) def config_bitasset_market(request, kh_worker_name, bitasset_local, bitshares, account): - """ Produces a config with market MPA:COLLATERAL or COLLATERAL:MPA - """ + """Produces a config with market MPA:COLLATERAL or COLLATERAL:MPA.""" worker_name = kh_worker_name bitasset = bitasset_local market = f'{bitasset.symbol}/TEST' if request.param == 0 else f'TEST/{bitasset.symbol}' @@ -123,8 +118,7 @@ def config_bitasset_market(request, kh_worker_name, bitasset_local, bitshares, a @pytest.fixture def config_other_account(config, base_account, kh_worker_name): - """ Config for other account which simulates foreign trader - """ + """Config for other account which simulates foreign trader.""" config = copy.deepcopy(config) worker_name = kh_worker_name config['workers'][worker_name]['account'] = base_account() @@ -133,8 +127,7 @@ def config_other_account(config, base_account, kh_worker_name): @pytest.fixture def base_worker(bitshares, kh_worker_name): - """ Fixture to create KOTH worker - """ + """Fixture to create KOTH worker.""" worker_name = kh_worker_name workers = [] @@ -153,32 +146,28 @@ def _base_worker(config): @pytest.fixture def worker(base_worker, config): - """ Worker to test in single mode (for methods which not required to be tested against all modes) - """ + """Worker to test in single mode (for methods which not required to be tested against all modes)""" worker = base_worker(config) return worker @pytest.fixture def worker2(base_worker, config_variable_modes): - """ Worker to test all modes - """ + """Worker to test all modes.""" worker = base_worker(config_variable_modes) return worker @pytest.fixture def worker_bitasset(base_worker, config_bitasset_market): - """ Worker operating on MPA/COLLATERAL market - """ + """Worker operating on MPA/COLLATERAL market.""" worker = base_worker(config_bitasset_market) return worker @pytest.fixture def orders1(worker): - """ Place buy and sell order using worker account - """ + """Place buy and sell order using worker account.""" worker.place_market_buy_order(1, 100, returnOrderId=True) worker.place_market_sell_order(1, 200, returnOrderId=True) yield worker @@ -196,8 +185,7 @@ def other_worker(bitshares, kh_worker_name, config_other_account): @pytest.fixture def other_orders(other_worker): - """ Place some orders from second account to simulate foreign trader - """ + """Place some orders from second account to simulate foreign trader.""" worker = other_worker worker.place_market_buy_order(10, 0.9) worker.place_market_buy_order(10, 1) @@ -209,8 +197,7 @@ def other_orders(other_worker): @pytest.fixture def other_orders_out_of_bounds(other_orders): - """ Extend other_orders by placing additional orders out of bounds - """ + """Extend other_orders by placing additional orders out of bounds.""" worker = other_orders worker.place_market_buy_order(10, worker.worker['upper_bound'] * 1.2) worker.place_market_sell_order(10, worker.worker['lower_bound'] / 1.2) diff --git a/tests/strategies/king_of_the_hill/test_king_of_the_hill.py b/tests/strategies/king_of_the_hill/test_king_of_the_hill.py index 8effc883e..001a5fb04 100644 --- a/tests/strategies/king_of_the_hill/test_king_of_the_hill.py +++ b/tests/strategies/king_of_the_hill/test_king_of_the_hill.py @@ -7,8 +7,7 @@ def test_amount_quote(worker): - """ Test quote amount calculation - """ + """Test quote amount calculation.""" # config: 'sell_order_amount': 1.0, assert worker.amount_quote == 1 @@ -19,8 +18,7 @@ def test_amount_quote(worker): def test_amount_base(worker): - """ Test base amount calculation - """ + """Test base amount calculation.""" # config: 'buy_order_amount': 1.0, assert worker.amount_base == 1 @@ -31,8 +29,7 @@ def test_amount_base(worker): def test_get_top_prices(other_orders, worker): - """ Test if orders prices are calculated - """ + """Test if orders prices are calculated.""" orderbook = worker.market.orderbook(limit=1) top_price_bid = orderbook['bids'][0]['price'] top_price_ask = orderbook['asks'][0]['price'] @@ -53,8 +50,10 @@ def test_get_top_prices_margin_call(worker_bitasset): def test_place_order_correct_price(worker, other_orders): - """ Test that buy order is placed at correct price. Similar to test_get_top_prices(), but with actual order - placement + """ + Test that buy order is placed at correct price. + + Similar to test_get_top_prices(), but with actual order placement """ worker.get_top_prices() orderbook = worker.market.orderbook(limit=1) @@ -79,8 +78,7 @@ def test_place_order_correct_price(worker, other_orders): def test_place_order_zero_price(worker): - """ Check that worker goes into error if no prices are calculated - """ + """Check that worker goes into error if no prices are calculated.""" worker.top_sell_price = 0 worker.place_order('sell') assert worker.disabled @@ -92,8 +90,7 @@ def test_place_order_zero_price(worker): def test_place_order_zero_amount(worker, other_orders, monkeypatch): - """ Check that worker doesn't try to place an order if amounts are 0 - """ + """Check that worker doesn't try to place an order if amounts are 0.""" worker.get_top_prices() monkeypatch.setattr(worker.__class__, 'amount_quote', 0) @@ -111,8 +108,10 @@ def test_place_order_zero_amount(worker, other_orders, monkeypatch): def test_place_orders(worker2, other_orders): - """ Test that orders are placed according to mode (buy, sell, buy + sell). Simple test, just make sure buy/sell - order gets placed. + """ + Test that orders are placed according to mode (buy, sell, buy + sell). + + Simple test, just make sure buy/sell order gets placed. """ worker = worker2 worker.place_orders() @@ -126,8 +125,7 @@ def test_place_orders(worker2, other_orders): def test_place_orders_check_bounds(worker, other_orders_out_of_bounds): - """ Test that orders aren't going out of bounds - """ + """Test that orders aren't going out of bounds.""" worker.place_orders() own_buy_orders = worker.get_own_buy_orders() own_sell_orders = worker.get_own_sell_orders() @@ -141,8 +139,7 @@ def test_place_orders_check_bounds(worker, other_orders_out_of_bounds): def test_check_orders_fully_filled(worker, other_orders): - """ When our order is fully filled, the strategy should place a new one - """ + """When our order is fully filled, the strategy should place a new one.""" worker2 = other_orders worker.place_orders() @@ -171,8 +168,7 @@ def test_check_orders_fully_filled(worker, other_orders): def test_check_orders_partially_filled(worker, other_orders): - """ When our order is partially filled more than threshold, order should be replaced - """ + """When our order is partially filled more than threshold, order should be replaced.""" worker2 = other_orders worker.place_orders() @@ -202,8 +198,7 @@ def test_check_orders_partially_filled(worker, other_orders): def test_check_orders_beaten_order_cancelled(worker, other_orders): - """ Beaten order was cancelled, own order should be moved - """ + """Beaten order was cancelled, own order should be moved.""" worker2 = other_orders worker.place_orders() @@ -230,8 +225,7 @@ def test_check_orders_beaten_order_cancelled(worker, other_orders): def test_check_orders_new_order_above_our(worker, other_orders): - """ Someone put order above ours, own order must be moved - """ + """Someone put order above ours, own order must be moved.""" worker2 = other_orders worker.place_orders() @@ -263,8 +257,7 @@ def test_check_orders_new_order_above_our(worker, other_orders): def test_check_orders_no_looping(worker, other_orders): - """ Make sure order placement is correct so check_orders() doesn't want to continuously move orders - """ + """Make sure order placement is correct so check_orders() doesn't want to continuously move orders.""" worker.place_orders() ids = [order['id'] for order in worker.own_orders] @@ -275,17 +268,18 @@ def test_check_orders_no_looping(worker, other_orders): def test_maintain_strategy(worker, other_orders): - """ maintain_strategy() should run without errors. - No logic is checked here because it's done inside other tests. - The goal of this test is to make sure maintain_strategy() places orders + """ + maintain_strategy() should run without errors. + + No logic is checked here because it's done inside other tests. The goal of this test is to make sure + maintain_strategy() places orders """ worker.maintain_strategy() assert len(worker.own_orders) == 2 def test_zero_spread(worker, other_orders_zero_spread): - """ Make sure the strategy doesn't crossing opposite side orders when market spread is too close - """ + """Make sure the strategy doesn't crossing opposite side orders when market spread is too close.""" other_worker = other_orders_zero_spread other_orders_before = other_worker.own_orders @@ -313,15 +307,13 @@ def test_zero_spread(worker, other_orders_zero_spread): def test_check_bitasset_market_non_bitasset(worker): - """ Worker market is not MPA/COLLATERAL - """ + """Worker market is not MPA/COLLATERAL.""" worker.check_bitasset_market() assert worker.call_orders_expected is False def test_check_bitasset_market_bitasset(worker_bitasset): - """ Correctly determine if worker market is MPA/COLLATERAL - """ + """Correctly determine if worker market is MPA/COLLATERAL.""" worker = worker_bitasset worker.check_bitasset_market() assert worker.call_orders_expected is True diff --git a/tests/strategies/relative_orders/conftest.py b/tests/strategies/relative_orders/conftest.py index e0dea3178..853a33ed6 100644 --- a/tests/strategies/relative_orders/conftest.py +++ b/tests/strategies/relative_orders/conftest.py @@ -12,8 +12,7 @@ @pytest.fixture(scope='session') def assets(create_asset): - """ Create some assets with different precision - """ + """Create some assets with different precision.""" create_asset('BASEA', 3) create_asset('QUOTEA', 8) create_asset('BASEB', 8) @@ -22,8 +21,7 @@ def assets(create_asset): @pytest.fixture(scope='module') def base_account(assets, prepare_account): - """ Factory to generate random account with pre-defined balances - """ + """Factory to generate random account with pre-defined balances.""" def func(): account = prepare_account({'BASEA': 10000, 'QUOTEA': 100, 'BASEB': 10000, 'QUOTEB': 100, 'TEST': 1000}) @@ -34,23 +32,22 @@ def func(): @pytest.fixture(scope='module') def account(base_account): - """ Prepare worker account with some balance - """ + """Prepare worker account with some balance.""" return base_account() @pytest.fixture(scope='session') def ro_worker_name(): - """ Fixture to share ro Orders worker name - """ + """Fixture to share ro Orders worker name.""" return 'ro-worker' @pytest.fixture def config(bitshares, account, ro_worker_name): - """ Define worker's config with variable assets + """ + Define worker's config with variable assets. - This fixture should be function-scoped to use new fresh bitshares account for each test + This fixture should be function-scoped to use new fresh bitshares account for each test """ worker_name = ro_worker_name config = { @@ -88,8 +85,7 @@ def config(bitshares, account, ro_worker_name): @pytest.fixture def config_other_account(config, base_account, ro_worker_name): - """ Config for other account which simulates foreign trader - """ + """Config for other account which simulates foreign trader.""" config = copy.deepcopy(config) worker_name = ro_worker_name config['workers'][worker_name]['account'] = base_account() @@ -98,8 +94,7 @@ def config_other_account(config, base_account, ro_worker_name): @pytest.fixture def base_worker(bitshares, ro_worker_name): - """ Fixture to create a worker - """ + """Fixture to create a worker.""" workers = [] def _base_worker(config, worker_name=ro_worker_name): @@ -117,8 +112,7 @@ def _base_worker(config, worker_name=ro_worker_name): @pytest.fixture def ro_worker(base_worker, config): - """ Basic RO worker - """ + """Basic RO worker.""" worker = base_worker(config) return worker @@ -141,8 +135,7 @@ def empty_ticker_workaround(worker): @pytest.fixture def other_orders(other_worker): - """ Place some orders from second account to simulate foreign trader - """ + """Place some orders from second account to simulate foreign trader.""" worker = other_worker worker.place_market_buy_order(10, 0.5) worker.place_market_sell_order(10, 1.5) @@ -153,8 +146,7 @@ def other_orders(other_worker): @pytest.fixture def other_orders_random(other_worker): - """ Place some number of random orders within some range - """ + """Place some number of random orders within some range.""" worker = other_worker lower_bound = 0.3 upper_bound = 2 @@ -172,8 +164,7 @@ def other_orders_random(other_worker): @pytest.fixture def config_multiple_workers_1(bitshares, account): - """ Prepares config with multiple workers on same account - """ + """Prepares config with multiple workers on same account.""" config = { 'node': '{}'.format(bitshares.rpc.url), 'workers': { diff --git a/tests/strategies/relative_orders/test_relative_orders.py b/tests/strategies/relative_orders/test_relative_orders.py index ad63f21b0..680d6e539 100644 --- a/tests/strategies/relative_orders/test_relative_orders.py +++ b/tests/strategies/relative_orders/test_relative_orders.py @@ -17,8 +17,7 @@ def test_configure(ro_worker, config): def test_error(ro_worker): - """ Event method return None - """ + """Event method return None.""" worker = ro_worker worker.error() assert worker.disabled is True @@ -64,8 +63,7 @@ def test_calculate_order_prices(ro_worker): def test_calculate_order_prices_dynamic_spread(ro_worker, other_orders): - """ Check if dynamic spread is working overall - """ + """Check if dynamic spread is working overall.""" worker = ro_worker worker.calculate_order_prices() buy_price_before = worker.buy_price @@ -115,8 +113,7 @@ def test_update_orders(ro_worker): def test_calculate_center_price(ro_worker, other_orders): - """ Test dynamic center price calculation - """ + """Test dynamic center price calculation.""" worker = ro_worker highest_bid = float(worker.market.ticker().get('highestBid')) lowest_ask = float(worker.market.ticker().get('lowestAsk')) @@ -127,10 +124,10 @@ def test_calculate_center_price(ro_worker, other_orders): @pytest.mark.parametrize('variant', ['no_shift', 'base_shift', 'quote_shift']) def test_calculate_asset_offset(variant, ro_worker, other_orders, monkeypatch): - """ Check if automatic asset offset calculation works + """ + Check if automatic asset offset calculation works. - Instead of duplicating offset calculation code, test offset at different balance and see does it make sense or - not. + Instead of duplicating offset calculation code, test offset at different balance and see does it make sense or not. """ def mocked_balance_b(*args): @@ -166,16 +163,14 @@ def test_calculate_center_price_with_manual_offset(ro_worker): def test_check_orders(ro_worker): - """ check_orders() should result in 2 orders placed if no own orders - """ + """check_orders() should result in 2 orders placed if no own orders.""" worker = ro_worker worker.check_orders() assert len(worker.own_orders) == 2 def test_check_orders_fully_filled(ro_worker, other_worker): - """ When our order is fully filled, the strategy should place a new one - """ + """When our order is fully filled, the strategy should place a new one.""" worker = ro_worker worker2 = other_worker log.debug('worker1 account name: {}'.format(worker.account.name)) @@ -215,8 +210,7 @@ def test_check_orders_fully_filled(ro_worker, other_worker): def test_check_orders_partially_filled(ro_worker, other_worker): - """ When our order is partially filled more than threshold, order should be replaced - """ + """When our order is partially filled more than threshold, order should be replaced.""" worker2 = other_worker worker = ro_worker worker.update_orders() @@ -246,8 +240,7 @@ def test_check_orders_partially_filled(ro_worker, other_worker): def test_check_orders_reset_on_price_change(ro_worker, other_orders): - """ Check if orders resetted on center price change - """ + """Check if orders resetted on center price change.""" worker2 = other_orders worker = ro_worker @@ -313,8 +306,7 @@ def test_get_own_last_trade(base_account, base_worker, config_multiple_workers_1 def test_get_own_last_trade_taker_buy(base_account, ro_worker, other_worker): - """ Test for https://github.com/Codaone/DEXBot/issues/708 - """ + """Test for https://github.com/Codaone/DEXBot/issues/708.""" worker1 = ro_worker worker3 = base_account() market1 = Market(worker1.worker["market"]) @@ -341,8 +333,7 @@ def test_get_own_last_trade_taker_buy(base_account, ro_worker, other_worker): def test_get_own_last_trade_taker_sell(base_account, ro_worker, other_worker): - """ Test for https://github.com/Codaone/DEXBot/issues/708 - """ + """Test for https://github.com/Codaone/DEXBot/issues/708.""" worker1 = ro_worker worker3 = base_account() market1 = Market(worker1.worker["market"]) @@ -369,8 +360,7 @@ def test_get_own_last_trade_taker_sell(base_account, ro_worker, other_worker): def test_get_external_market_center_price(monkeypatch, ro_worker): - """ Simply test if get_external_market_center_price does correct proxying to PriceFeed class - """ + """Simply test if get_external_market_center_price does correct proxying to PriceFeed class.""" def mocked_cp(*args): return 1 @@ -385,8 +375,7 @@ def mocked_cp(*args): def test_mwsa_orders_cancel(base_worker, config_multiple_workers_1): - """ Test two RO workers using same account, They should not touch each other orders - """ + """Test two RO workers using same account, They should not touch each other orders.""" worker1 = base_worker(config_multiple_workers_1, worker_name='ro-worker-1') worker2 = base_worker(config_multiple_workers_1, worker_name='ro-worker-2') assert len(worker1.own_orders) == 2 diff --git a/tests/strategies/staggered_orders/conftest.py b/tests/strategies/staggered_orders/conftest.py index b998537f9..2d9684dc5 100644 --- a/tests/strategies/staggered_orders/conftest.py +++ b/tests/strategies/staggered_orders/conftest.py @@ -15,8 +15,7 @@ @pytest.fixture(scope='session') def assets(create_asset): - """ Create some assets with different precision - """ + """Create some assets with different precision.""" create_asset('BASEA', 3) create_asset('QUOTEA', 8) create_asset('BASEB', 8) @@ -25,8 +24,7 @@ def assets(create_asset): @pytest.fixture(scope='module') def base_account(assets, prepare_account): - """ Factory to generate random account with pre-defined balances - """ + """Factory to generate random account with pre-defined balances.""" def func(): account = prepare_account({'BASEA': 10000, 'QUOTEA': 100, 'BASEB': 10000, 'QUOTEB': 100, 'TEST': 1000}) @@ -37,39 +35,36 @@ def func(): @pytest.fixture def account(base_account): - """ Prepare worker account with some balance - """ + """Prepare worker account with some balance.""" return base_account() @pytest.fixture def account_only_base(assets, prepare_account): - """ Prepare worker account with only BASE assets balance - """ + """Prepare worker account with only BASE assets balance.""" account = prepare_account({'BASEA': 1000, 'BASEB': 1000, 'TEST': 1000}) return account @pytest.fixture def account_1_sat(assets, prepare_account): - """ Prepare worker account to simulate XXX/BTC trading near zero prices - """ + """Prepare worker account to simulate XXX/BTC trading near zero prices.""" account = prepare_account({'BASEB': 0.02, 'QUOTEB': 10000000, 'TEST': 1000}) return account @pytest.fixture(scope='session') def so_worker_name(): - """ Fixture to share Staggered Orders worker name - """ + """Fixture to share Staggered Orders worker name.""" return 'so-worker' @pytest.fixture(params=[('QUOTEA', 'BASEA'), ('QUOTEB', 'BASEB')]) def config(request, bitshares, account, so_worker_name): - """ Define worker's config with variable assets + """ + Define worker's config with variable assets. - This fixture should be function-scoped to use new fresh bitshares account for each test + This fixture should be function-scoped to use new fresh bitshares account for each test """ worker_name = so_worker_name config = { @@ -96,8 +91,7 @@ def config(request, bitshares, account, so_worker_name): @pytest.fixture(params=MODES) def config_variable_modes(request, config, so_worker_name): - """ Test config which tests all modes - """ + """Test config which tests all modes.""" worker_name = so_worker_name config = copy.deepcopy(config) config['workers'][worker_name]['mode'] = request.param @@ -106,8 +100,7 @@ def config_variable_modes(request, config, so_worker_name): @pytest.fixture def config_only_base(config, so_worker_name, account_only_base): - """ Config which uses an account with only BASE asset - """ + """Config which uses an account with only BASE asset.""" worker_name = so_worker_name config = copy.deepcopy(config) config['workers'][worker_name]['account'] = account_only_base @@ -116,8 +109,7 @@ def config_only_base(config, so_worker_name, account_only_base): @pytest.fixture def config_1_sat(so_worker_name, bitshares, account_1_sat): - """ Config to set up a worker on market with center price around 1 sats - """ + """Config to set up a worker on market with center price around 1 sats.""" worker_name = so_worker_name config = { 'node': '{}'.format(bitshares.rpc.url), @@ -143,9 +135,10 @@ def config_1_sat(so_worker_name, bitshares, account_1_sat): @pytest.fixture def config_multiple_workers_1(bitshares, account): - """ Prepares config with multiple SO workers on same account + """ + Prepares config with multiple SO workers on same account. - This fixture should be function-scoped to use new fresh bitshares account for each test + This fixture should be function-scoped to use new fresh bitshares account for each test """ config = { 'node': '{}'.format(bitshares.rpc.url), @@ -185,9 +178,10 @@ def config_multiple_workers_1(bitshares, account): @pytest.fixture def config_multiple_workers_2(config_multiple_workers_1): - """ Prepares config with multiple SO workers on same account + """ + Prepares config with multiple SO workers on same account. - This fixture should be function-scoped to use new fresh bitshares account for each test + This fixture should be function-scoped to use new fresh bitshares account for each test """ config = copy.deepcopy(config_multiple_workers_1) config['workers']['so-worker-1']['market'] = 'QUOTEA/BASEA' @@ -221,9 +215,10 @@ def _base_worker(config, worker_name=so_worker_name): @pytest.fixture(scope='session') def storage_db(): - """ Prepare custom sqlite database to not mess with main one + """ + Prepare custom sqlite database to not mess with main one. - TODO: this is doesn't work!!! + TODO: this is doesn't work!!! """ from dexbot.storage import sqlDataBaseFile @@ -234,16 +229,14 @@ def storage_db(): @pytest.fixture def worker(base_worker, config): - """ Worker to test in single mode (for methods which not required to be tested against all modes) - """ + """Worker to test in single mode (for methods which not required to be tested against all modes)""" worker = base_worker(config) return worker @pytest.fixture def worker2(base_worker, config_variable_modes): - """ Worker to test all modes - """ + """Worker to test all modes.""" worker = base_worker(config_variable_modes) return worker @@ -257,9 +250,10 @@ def init_empty_balances(worker, bitshares): @pytest.fixture def orders1(worker, bitshares, init_empty_balances): - """ Place 1 buy+sell real order, and 1 buy+sell virtual orders with prices outside of the range. + """ + Place 1 buy+sell real order, and 1 buy+sell virtual orders with prices outside of the range. - Note: this fixture don't calls refresh.xxx() intentionally! + Note: this fixture don't calls refresh.xxx() intentionally! """ # Make sure there are no orders worker.cancel_all_orders() @@ -283,8 +277,7 @@ def orders1(worker, bitshares, init_empty_balances): @pytest.fixture def orders2(worker): - """ Place buy+sell real orders near center price - """ + """Place buy+sell real orders near center price.""" worker.cancel_all_orders() buy_price = worker.market_center_price - 1 sell_price = worker.market_center_price + 1 @@ -301,8 +294,7 @@ def orders2(worker): @pytest.fixture def orders3(worker): - """ Place buy+sell virtual orders near center price - """ + """Place buy+sell virtual orders near center price.""" worker.cancel_all_orders() worker.refresh_balances() buy_price = worker.market_center_price - 1 @@ -317,17 +309,15 @@ def orders3(worker): @pytest.fixture def orders4(worker, orders1): - """ Just wrap orders1, but refresh balances in addition - """ + """Just wrap orders1, but refresh balances in addition.""" worker.refresh_balances() yield orders1 @pytest.fixture def orders5(worker2): - """ Place buy+sell virtual orders at some distance from center price, and - buy+sell real orders at 1 order distance from center - """ + """Place buy+sell virtual orders at some distance from center price, and buy+sell real orders at 1 order distance + from center.""" worker = worker2 worker.cancel_all_orders() @@ -366,8 +356,7 @@ def orders5(worker2): @pytest.fixture def partially_filled_order(worker): - """ Create partially filled order - """ + """Create partially filled order.""" worker.cancel_all_orders() order = worker.place_market_buy_order(100, 1, returnOrderId=True) worker.place_market_sell_order(20, 1) @@ -381,9 +370,10 @@ def partially_filled_order(worker): @pytest.fixture(scope='session') def increase_until_allocated(): - """ Run increase_order_sizes() until funds are allocated + """ + Run increase_order_sizes() until funds are allocated. - :param Strategy worker: worker instance + :param Strategy worker: worker instance """ def func(worker): @@ -403,9 +393,10 @@ def func(worker): @pytest.fixture(scope='session') def maintain_until_allocated(): - """ Run maintain_strategy() on a specific worker until funds are allocated + """ + Run maintain_strategy() on a specific worker until funds are allocated. - :param Strategy worker: worker instance + :param Strategy worker: worker instance """ def func(worker): @@ -426,10 +417,11 @@ def func(worker): @pytest.fixture def do_initial_allocation(maintain_until_allocated): - """ Run maintain_strategy() to make an initial allocation of funds + """ + Run maintain_strategy() to make an initial allocation of funds. - :param Strategy worker: initialized worker - :param str mode: SO mode (valley, mountain etc) + :param Strategy worker: initialized worker + :param str mode: SO mode (valley, mountain etc) """ def func(worker, mode): diff --git a/tests/strategies/staggered_orders/test_pybitshares.py b/tests/strategies/staggered_orders/test_pybitshares.py index 4f5a54eab..24d5cac06 100644 --- a/tests/strategies/staggered_orders/test_pybitshares.py +++ b/tests/strategies/staggered_orders/test_pybitshares.py @@ -1,6 +1,5 @@ def test_correct_asset_names(orders1): - """ Test for https://github.com/bitshares/python-bitshares/issues/239 - """ + """Test for https://github.com/bitshares/python-bitshares/issues/239.""" worker = orders1 worker.account.refresh() orders = worker.account.openorders diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index dadcbbb17..5afbaabe7 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -20,8 +20,7 @@ def test_maintain_strategy_manual_cp_empty_market(worker): - """ On empty market, center price should be set to manual CP - """ + """On empty market, center price should be set to manual CP.""" worker.cancel_all_orders() # Undefine market_center_price worker.market_center_price = None @@ -32,8 +31,7 @@ def test_maintain_strategy_manual_cp_empty_market(worker): def test_maintain_strategy_no_manual_cp_empty_market(worker): - """ Strategy should not work on empty market if no manual CP was set - """ + """Strategy should not work on empty market if no manual CP was set.""" worker.cancel_all_orders() # Undefine market_center_price worker.market_center_price = None @@ -46,8 +44,7 @@ def test_maintain_strategy_no_manual_cp_empty_market(worker): @pytest.mark.parametrize('mode', MODES) def test_maintain_strategy_basic(mode, worker, do_initial_allocation): - """ Check if intial orders placement is correct - """ + """Check if intial orders placement is correct.""" worker = do_initial_allocation(worker, mode) # Check target spread is reached @@ -75,8 +72,7 @@ def test_maintain_strategy_basic(mode, worker, do_initial_allocation): @pytest.mark.parametrize('mode', MODES) def test_maintain_strategy_one_sided(mode, base_worker, config_only_base, do_initial_allocation): - """ Test for one-sided start (buy only) - """ + """Test for one-sided start (buy only)""" worker = base_worker(config_only_base) do_initial_allocation(worker, mode) @@ -127,9 +123,8 @@ def test_maintain_strategy_1sat(base_worker, config_1_sat, do_initial_allocation # Combine each mode with base and quote @pytest.mark.parametrize('asset', ['base', 'quote']) def test_maintain_strategy_fallback_logic(asset, worker, do_initial_allocation): - """ Check fallback logic: when spread is not reached, furthest order should be cancelled to make free funds to - close spread - """ + """Check fallback logic: when spread is not reached, furthest order should be cancelled to make free funds to close + spread.""" do_initial_allocation(worker, worker.mode) # TODO: strategy must turn off bootstrapping once target spread is reached worker['bootstrapping'] = False @@ -157,9 +152,8 @@ def test_maintain_strategy_fallback_logic(asset, worker, do_initial_allocation): @pytest.mark.parametrize('asset', ['base', 'quote']) def test_maintain_strategy_fallback_logic_disabled(asset, worker, do_initial_allocation): - """ Check fallback logic: when spread is not reached, furthest order should be cancelled to make free funds to - close spread - """ + """Check fallback logic: when spread is not reached, furthest order should be cancelled to make free funds to close + spread.""" worker.enable_fallback_logic = False worker.operational_depth = 2 do_initial_allocation(worker, 'valley') @@ -192,8 +186,7 @@ def test_maintain_strategy_fallback_logic_disabled(asset, worker, do_initial_all def test_check_operational_depth(worker, do_initial_allocation): - """ Test for correct operational depth following - """ + """Test for correct operational depth following.""" worker.operational_depth = 10 do_initial_allocation(worker, worker.mode) worker['bootstrapping'] = False @@ -219,8 +212,7 @@ def test_check_operational_depth(worker, do_initial_allocation): def test_increase_order_sizes_valley_basic(worker, do_initial_allocation, issue_asset, increase_until_allocated): - """ Test increases in valley mode when all orders are equal (new allocation round). - """ + """Test increases in valley mode when all orders are equal (new allocation round).""" do_initial_allocation(worker, 'valley') # Double worker's balance issue_asset(worker.market['base']['symbol'], worker.base_total_balance, worker.account.name) @@ -236,14 +228,15 @@ def test_increase_order_sizes_valley_basic(worker, do_initial_allocation, issue_ def test_increase_order_sizes_valley_direction(worker, do_initial_allocation, issue_asset, increase_until_allocated): - """ Test increase direction in valley mode: new allocation round must be started from closest order. + """ + Test increase direction in valley mode: new allocation round must be started from closest order. - Buy side, amounts in BASE: + Buy side, amounts in BASE: - 100 100 100 100 100 - 100 100 100 100 115 - 100 100 100 115 115 - 100 100 115 115 115 + 100 100 100 100 100 + 100 100 100 100 115 + 100 100 100 115 115 + 100 100 115 115 115 """ do_initial_allocation(worker, 'valley') @@ -263,14 +256,15 @@ def test_increase_order_sizes_valley_direction(worker, do_initial_allocation, is def test_increase_order_sizes_valley_transit_from_mountain(worker, do_initial_allocation, issue_asset): - """ Transition from mountain to valley + """ + Transition from mountain to valley. - Buy side, amounts in BASE, increase should be like this: + Buy side, amounts in BASE, increase should be like this: - 70 80 90 100 - 80 80 90 100 - 80 90 90 100 - 90 90 90 100 + 70 80 90 100 + 80 80 90 100 + 80 90 90 100 + 90 90 90 100 """ # Set up mountain do_initial_allocation(worker, 'mountain') @@ -299,12 +293,13 @@ def test_increase_order_sizes_valley_transit_from_mountain(worker, do_initial_al def test_increase_order_sizes_valley_smaller_closest_orders(worker, do_initial_allocation, increase_until_allocated): - """ Test increase when closest-to-center orders are less than further orders. Normal situation when initial sides - are imbalanced and several orders were filled. + """ + Test increase when closest-to-center orders are less than further orders. Normal situation when initial sides are + imbalanced and several orders were filled. - Buy side, amounts in BASE: + Buy side, amounts in BASE: - 100 100 100 10 10 10
+ 100 100 100 10 10 10
""" worker = do_initial_allocation(worker, 'valley') increase_until_allocated(worker) @@ -341,16 +336,17 @@ def test_increase_order_sizes_valley_smaller_closest_orders(worker, do_initial_a def test_increase_order_sizes_valley_imbalaced_small_further(worker, do_initial_allocation, increase_until_allocated): - """ If furthest orders are smaller than closest, they should be increased first. - See https://github.com/Codaone/DEXBot/issues/444 for details + """ + If furthest orders are smaller than closest, they should be increased first. See + https://github.com/Codaone/DEXBot/issues/444 for details. - Buy side, amounts in BASE: + Buy side, amounts in BASE: - 5 5 5 100 100 10 10 10
+ 5 5 5 100 100 10 10 10
- Should be: + Should be: - 10 10 10 100 100 10 10 10
+ 10 10 10 100 100 10 10 10
""" worker = do_initial_allocation(worker, 'valley') @@ -392,8 +388,7 @@ def test_increase_order_sizes_valley_imbalaced_small_further(worker, do_initial_ def test_increase_order_sizes_valley_closest_order(worker, do_initial_allocation, issue_asset): - """ Should test proper calculation of closest order: order should not be less that min_increase_factor - """ + """Should test proper calculation of closest order: order should not be less that min_increase_factor.""" worker = do_initial_allocation(worker, 'valley') # Add balance to increase 2 orders @@ -412,8 +407,10 @@ def test_increase_order_sizes_valley_closest_order(worker, do_initial_allocation def test_increase_order_sizes_mountain_basic(worker, do_initial_allocation, issue_asset, increase_until_allocated): - """ Test increases in mountain mode when all orders are equal (new allocation round). New orders should be equal in - their "quote" + """ + Test increases in mountain mode when all orders are equal (new allocation round). + + New orders should be equal in their "quote" """ do_initial_allocation(worker, 'mountain') increase_until_allocated(worker) @@ -436,14 +433,15 @@ def test_increase_order_sizes_mountain_basic(worker, do_initial_allocation, issu def test_increase_order_sizes_mountain_direction(worker, do_initial_allocation, issue_asset, increase_until_allocated): - """ Test increase direction in mountain mode + """ + Test increase direction in mountain mode. - Buy side, amounts in QUOTE: + Buy side, amounts in QUOTE: - 10 10 10 10 10 - 15 10 10 10 10 - 15 15 10 10 10 - 15 15 15 10 10 + 10 10 10 10 10 + 15 10 10 10 10 + 15 15 10 10 10 + 15 15 15 10 10 """ do_initial_allocation(worker, 'mountain') increase_until_allocated(worker) @@ -474,8 +472,7 @@ def test_increase_order_sizes_mountain_direction(worker, do_initial_allocation, def test_increase_order_sizes_mountain_furthest_order( worker, do_initial_allocation, increase_until_allocated, issue_asset ): - """ Should test proper calculation of furthest order: try to maximize, don't allow too small increase - """ + """Should test proper calculation of furthest order: try to maximize, don't allow too small increase.""" do_initial_allocation(worker, 'mountain') previous_buy_orders = worker.buy_orders @@ -495,14 +492,15 @@ def test_increase_order_sizes_mountain_furthest_order( def test_increase_order_sizes_mountain_imbalanced(worker, do_initial_allocation): - """ Test situation when sides was imbalances, several orders filled on opposite side. - This also tests transition from vally to mountain. + """ + Test situation when sides was imbalances, several orders filled on opposite side. This also tests transition from + vally to mountain. - Buy side, amounts in QUOTE: + Buy side, amounts in QUOTE: - 100 100 100 10 10 10 - 100 100 100 20 10 10 - 100 100 100 20 20 10 + 100 100 100 10 10 10 + 100 100 100 20 10 10 + 100 100 100 20 20 10 """ do_initial_allocation(worker, 'mountain') worker.mode = 'mountain' @@ -545,8 +543,7 @@ def test_increase_order_sizes_mountain_imbalanced(worker, do_initial_allocation) def test_increase_order_sizes_neutral_basic(worker, do_initial_allocation, issue_asset, increase_until_allocated): - """ Test increases in neutral mode when all orders are equal (new allocation round) - """ + """Test increases in neutral mode when all orders are equal (new allocation round)""" do_initial_allocation(worker, 'neutral') increase_until_allocated(worker) @@ -594,14 +591,15 @@ def test_increase_order_sizes_neutral_basic(worker, do_initial_allocation, issue def test_increase_order_sizes_neutral_direction(worker, do_initial_allocation, issue_asset, increase_until_allocated): - """ Test increase direction in neutral mode: new allocation round must be started from closest order. + """ + Test increase direction in neutral mode: new allocation round must be started from closest order. - Buy side, amounts in BASE: + Buy side, amounts in BASE: - 100 100 100 100 100 - 100 100 100 100 115 - 100 100 100 114 115 - 100 100 113 114 115 + 100 100 100 100 100 + 100 100 100 100 115 + 100 100 100 114 115 + 100 100 113 114 115 """ do_initial_allocation(worker, 'neutral') @@ -621,14 +619,15 @@ def test_increase_order_sizes_neutral_direction(worker, do_initial_allocation, i def test_increase_order_sizes_neutral_transit_from_mountain(worker, do_initial_allocation, issue_asset): - """ Transition from mountain to neutral + """ + Transition from mountain to neutral. - Buy side, amounts in BASE, increase should be like this: + Buy side, amounts in BASE, increase should be like this: - 70 80 90 100 - 80 80 90 100 - 80 90 90 100 - 90 90 90 100 + 70 80 90 100 + 80 80 90 100 + 80 90 90 100 + 90 90 90 100 """ # Set up mountain do_initial_allocation(worker, 'mountain') @@ -657,12 +656,13 @@ def test_increase_order_sizes_neutral_transit_from_mountain(worker, do_initial_a def test_increase_order_sizes_neutral_smaller_closest_orders(worker, do_initial_allocation, increase_until_allocated): - """ Test increase when closest-to-center orders are less than further orders. Normal situation when initial sides - are imbalanced and several orders were filled. + """ + Test increase when closest-to-center orders are less than further orders. Normal situation when initial sides are + imbalanced and several orders were filled. - Buy side, amounts in BASE: + Buy side, amounts in BASE: - 100 100 100 10 10 10
+ 100 100 100 10 10 10
""" worker = do_initial_allocation(worker, 'neutral') increase_until_allocated(worker) @@ -701,16 +701,17 @@ def test_increase_order_sizes_neutral_smaller_closest_orders(worker, do_initial_ def test_increase_order_sizes_neutral_imbalaced_small_further(worker, do_initial_allocation, increase_until_allocated): - """ If furthest orders are smaller than closest, they should be increased first. - See https://github.com/Codaone/DEXBot/issues/444 for details + """ + If furthest orders are smaller than closest, they should be increased first. See + https://github.com/Codaone/DEXBot/issues/444 for details. - Buy side, amounts in BASE: + Buy side, amounts in BASE: - 5 5 5 100 100 10 10 10
+ 5 5 5 100 100 10 10 10
- Should be: + Should be: - 10 10 10 100 100 10 10 10
+ 10 10 10 100 100 10 10 10
""" worker = do_initial_allocation(worker, 'neutral') @@ -759,8 +760,7 @@ def test_increase_order_sizes_neutral_imbalaced_small_further(worker, do_initial def test_increase_order_sizes_neutral_closest_order( worker, do_initial_allocation, increase_until_allocated, issue_asset ): - """ Should test proper calculation of closest order: order should not be less that min_increase_factor - """ + """Should test proper calculation of closest order: order should not be less that min_increase_factor.""" worker = do_initial_allocation(worker, 'neutral') increase_until_allocated(worker) @@ -781,8 +781,7 @@ def test_increase_order_sizes_neutral_closest_order( def test_increase_order_sizes_buy_slope(worker, do_initial_allocation, issue_asset, increase_until_allocated): - """ Check correct orders sizes on both sides - """ + """Check correct orders sizes on both sides.""" do_initial_allocation(worker, 'buy_slope') # Double worker's balance @@ -814,8 +813,7 @@ def test_increase_order_sizes_buy_slope(worker, do_initial_allocation, issue_ass def test_increase_order_sizes_sell_slope(worker, do_initial_allocation, issue_asset, increase_until_allocated): - """ Check correct orders sizes on both sides - """ + """Check correct orders sizes on both sides.""" do_initial_allocation(worker, 'sell_slope') # Double worker's balance @@ -851,8 +849,7 @@ def test_increase_order_sizes_sell_slope(worker, do_initial_allocation, issue_as def test_allocate_asset_basic(worker): - """ Check that free balance is shrinking after each allocation and spread is decreasing - """ + """Check that free balance is shrinking after each allocation and spread is decreasing.""" worker.refresh_balances() spread_after = worker.get_actual_spread() @@ -884,8 +881,7 @@ def test_allocate_asset_basic(worker): def test_allocate_asset_replace_closest_partial_order(worker, do_initial_allocation, base_account, issue_asset): - """ Test that partially filled order is replaced when target spread is not reached, before placing closer order - """ + """Test that partially filled order is replaced when target spread is not reached, before placing closer order.""" do_initial_allocation(worker, worker.mode) additional_account = base_account() @@ -910,7 +906,10 @@ def test_allocate_asset_replace_closest_partial_order(worker, do_initial_allocat def test_allocate_asset_replace_partially_filled_orders( worker, do_initial_allocation, base_account, issue_asset, maintain_until_allocated ): - """ Check replacement of partially filled orders on both sides. Simple check. + """ + Check replacement of partially filled orders on both sides. + + Simple check. """ do_initial_allocation(worker, worker.mode) # TODO: automatically turn off bootstrapping after target spread is closed? @@ -940,8 +939,7 @@ def test_allocate_asset_replace_partially_filled_orders( def test_allocate_asset_increase_orders(worker, do_initial_allocation, maintain_until_allocated, issue_asset): - """ Add balance, expect increased orders - """ + """Add balance, expect increased orders.""" do_initial_allocation(worker, worker.mode) order_ids = [order['id'] for order in worker.own_orders] balance_in_orders_before = worker.get_allocated_assets(order_ids) @@ -958,8 +956,7 @@ def test_allocate_asset_increase_orders(worker, do_initial_allocation, maintain_ def test_allocate_asset_dust_order_simple(worker, do_initial_allocation, maintain_until_allocated, base_account): - """ Make dust order, check if it canceled and closer opposite order placed - """ + """Make dust order, check if it canceled and closer opposite order placed.""" do_initial_allocation(worker, worker.mode) num_sell_orders_before = len(worker.sell_orders) num_buy_orders_before = len(worker.buy_orders) @@ -984,9 +981,8 @@ def test_allocate_asset_dust_order_simple(worker, do_initial_allocation, maintai def test_allocate_asset_dust_order_excess_funds( worker, do_initial_allocation, maintain_until_allocated, base_account, issue_asset ): - """ Make dust order, add additional funds, these funds should be allocated - and then dust order should be canceled and closer opposite order placed - """ + """Make dust order, add additional funds, these funds should be allocated and then dust order should be canceled and + closer opposite order placed.""" do_initial_allocation(worker, worker.mode) num_sell_orders_before = len(worker.sell_orders) num_buy_orders_before = len(worker.buy_orders) @@ -1012,9 +1008,10 @@ def test_allocate_asset_dust_order_excess_funds( def test_allocate_asset_dust_order_increase_race(worker, do_initial_allocation, base_account, issue_asset): - """ Test for https://github.com/Codaone/DEXBot/issues/587 + """ + Test for https://github.com/Codaone/DEXBot/issues/587. - Check if cancelling dust orders on opposite side will not cause a race for allocate_asset() on opposite side + Check if cancelling dust orders on opposite side will not cause a race for allocate_asset() on opposite side """ do_initial_allocation(worker, worker.mode) additional_account = base_account() @@ -1043,8 +1040,7 @@ def test_allocate_asset_dust_order_increase_race(worker, do_initial_allocation, def test_allocate_asset_filled_orders(worker, do_initial_allocation, base_account): - """ Fill an order and check if opposite order placed - """ + """Fill an order and check if opposite order placed.""" do_initial_allocation(worker, worker.mode) # TODO: automatically turn off bootstrapping after target spread is closed? worker['bootstrapping'] = False @@ -1064,11 +1060,12 @@ def test_allocate_asset_filled_orders(worker, do_initial_allocation, base_accoun def test_allocate_asset_filled_order_on_massively_imbalanced_sides(worker, do_initial_allocation, base_account): - """ When sides are massively imbalanced, make sure that spread will be closed after filling one order on - smaller side. The goal is to test a situation when one side has a big-sized orders, and other side has much - smaller orders. Correct behavior: when order on smaller side filled, big side should place closer order. + """ + When sides are massively imbalanced, make sure that spread will be closed after filling one order on smaller side. + The goal is to test a situation when one side has a big-sized orders, and other side has much smaller orders. + Correct behavior: when order on smaller side filled, big side should place closer order. - Test for https://github.com/Codaone/DEXBot/issues/588 + Test for https://github.com/Codaone/DEXBot/issues/588 """ do_initial_allocation(worker, worker.mode) spread_before = worker.get_actual_spread() @@ -1129,14 +1126,15 @@ def test_allocate_asset_filled_order_on_massively_imbalanced_sides(worker, do_in def test_allocate_asset_partially_filled_order_on_massively_imbalanced_sides( worker, do_initial_allocation, base_account ): - """ When sides are massively imbalanced, make sure that spread will be closed after filling one order on - smaller side. The goal is to test a situation when one side has a big-sized orders, and other side has much - smaller orders. Correct behavior: when order on smaller side filled, big side should place closer order. + """ + When sides are massively imbalanced, make sure that spread will be closed after filling one order on smaller side. + The goal is to test a situation when one side has a big-sized orders, and other side has much smaller orders. + Correct behavior: when order on smaller side filled, big side should place closer order. - This test is similar to test_allocate_asset_filled_order_on_massively_imbalanced_sides, but tests partially - filled order where "calncel dust order" logic is in action. + This test is similar to test_allocate_asset_filled_order_on_massively_imbalanced_sides, but tests partially + filled order where "calncel dust order" logic is in action. - Test for https://github.com/Codaone/DEXBot/issues/588 + Test for https://github.com/Codaone/DEXBot/issues/588 """ do_initial_allocation(worker, worker.mode) spread_before = worker.get_actual_spread() @@ -1192,9 +1190,8 @@ def test_allocate_asset_partially_filled_order_on_massively_imbalanced_sides( @pytest.mark.parametrize('mode', MODES) def test_allocate_asset_limiting_on_sell_side(mode, worker, do_initial_allocation, base_account): - """ Check order size limiting when placing closer order on side which is bigger (using funds obtained from filled - orders on side which is smaller) - """ + """Check order size limiting when placing closer order on side which is bigger (using funds obtained from filled + orders on side which is smaller)""" do_initial_allocation(worker, mode) # TODO: automatically turn off bootstrapping after target spread is closed? worker['bootstrapping'] = False @@ -1244,9 +1241,8 @@ def test_allocate_asset_limiting_on_sell_side(mode, worker, do_initial_allocatio @pytest.mark.parametrize('mode', MODES) def test_allocate_asset_limiting_on_buy_side(mode, worker, do_initial_allocation, base_account, issue_asset): - """ Check order size limiting when placing closer order on side which is bigger (using funds obtained from filled - orders on side which is smaller) - """ + """Check order size limiting when placing closer order on side which is bigger (using funds obtained from filled + orders on side which is smaller)""" worker.center_price = 1 worker.lower_bound = 0.4 worker.upper_bound = 1.4 @@ -1338,8 +1334,7 @@ def test_stop_loss_check(worker, base_account, do_initial_allocation, issue_asse def test_tick(worker): - """ Check tick counter increment - """ + """Check tick counter increment.""" counter_before = worker.counter worker.tick('foo') counter_after = worker.counter diff --git a/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py b/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py index 970b9d12f..b40570df5 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py @@ -15,8 +15,7 @@ def test_refresh_balances(orders1): - """ Check if balance refresh works - """ + """Check if balance refresh works.""" worker = orders1 worker.refresh_balances() balance = worker.count_asset() @@ -28,9 +27,10 @@ def test_refresh_balances(orders1): def test_refresh_orders(orders1): - """ Make sure orders refresh is working + """ + Make sure orders refresh is working. - Note: this test doesn't checks orders sorting + Note: this test doesn't checks orders sorting """ worker = orders1 worker.refresh_orders() @@ -43,8 +43,7 @@ def test_refresh_orders(orders1): def test_check_min_order_size(worker): - """ Make sure our orders are always match minimal allowed size - """ + """Make sure our orders are always match minimal allowed size.""" worker.calculate_min_amounts() if worker.order_min_quote > worker.order_min_base: # Limiting asset is QUOTE @@ -62,8 +61,7 @@ def test_check_min_order_size(worker): def test_remove_outside_orders(orders1): - """ All orders in orders1 fixture are outside of the range, so remove_outside_orders() should cancel all - """ + """All orders in orders1 fixture are outside of the range, so remove_outside_orders() should cancel all.""" worker = orders1 worker.refresh_orders() assert worker.remove_outside_orders(worker.sell_orders, worker.buy_orders) @@ -72,8 +70,7 @@ def test_remove_outside_orders(orders1): def test_restore_virtual_orders(orders2): - """ Basic test to make sure virtual orders are placed on further ends - """ + """Basic test to make sure virtual orders are placed on further ends.""" worker = orders2 # Restore virtual orders from scratch (db is empty at this moment) worker.restore_virtual_orders() @@ -89,8 +86,7 @@ def test_restore_virtual_orders(orders2): def test_replace_real_order_with_virtual(orders2): - """ Try to replace 2 furthest orders with virtual, then compare difference - """ + """Try to replace 2 furthest orders with virtual, then compare difference.""" worker = orders2 worker.virtual_orders = [] num_orders_before = len(worker.real_buy_orders) + len(worker.real_sell_orders) @@ -103,8 +99,7 @@ def test_replace_real_order_with_virtual(orders2): def test_replace_virtual_order_with_real(orders3): - """ Try to replace 2 furthest virtual orders with real orders - """ + """Try to replace 2 furthest virtual orders with real orders.""" worker = orders3 num_orders_before = len(worker.virtual_orders) num_real_orders_before = len(worker.own_orders) @@ -117,8 +112,7 @@ def test_replace_virtual_order_with_real(orders3): def test_store_profit_estimation_data(worker, storage_db): - """ Check if storing of profit estimation data works - """ + """Check if storing of profit estimation data works.""" worker.refresh_balances() worker.store_profit_estimation_data(force=True) account = worker.worker.get('account') @@ -129,8 +123,7 @@ def test_store_profit_estimation_data(worker, storage_db): def test_check_partial_fill(worker, partially_filled_order): - """ Test that check_partial_fill() can detect partially filled order - """ + """Test that check_partial_fill() can detect partially filled order.""" is_not_partially_filled = worker.check_partial_fill(partially_filled_order, fill_threshold=0) assert not is_not_partially_filled is_not_partially_filled = worker.check_partial_fill(partially_filled_order, fill_threshold=90) @@ -138,16 +131,14 @@ def test_check_partial_fill(worker, partially_filled_order): def test_replace_partially_filled_order(worker, partially_filled_order): - """ Test if replace_partially_filled_order() do correct replacement - """ + """Test if replace_partially_filled_order() do correct replacement.""" worker.replace_partially_filled_order(partially_filled_order) new_order = worker.own_orders[0] assert new_order['base']['amount'] == new_order['for_sale']['amount'] def test_place_lowest_buy_order(worker2): - """ Check if placement of lowest buy order works in general - """ + """Check if placement of lowest buy order works in general.""" worker = worker2 worker.refresh_balances() worker.place_lowest_buy_order(worker.base_balance) @@ -158,8 +149,7 @@ def test_place_lowest_buy_order(worker2): def test_place_highest_sell_order(worker2): - """ Check if placement of highest sell order works in general - """ + """Check if placement of highest sell order works in general.""" worker = worker2 worker.refresh_balances() worker.place_highest_sell_order(worker.quote_balance) @@ -171,11 +161,12 @@ def test_place_highest_sell_order(worker2): @pytest.mark.parametrize('asset', ['base', 'quote']) def test_place_closer_order_real_or_virtual(orders5, asset): - """ Closer order may be real or virtual, depending on distance from the center and operational_depth + """ + Closer order may be real or virtual, depending on distance from the center and operational_depth. - 1. Closer order within operational depth must be real - 2. Closer order outside of operational depth must be virtual if previous order is virtual - 3. Closer order outside of operational depth must be real if previous order is real + 1. Closer order within operational depth must be real + 2. Closer order outside of operational depth must be virtual if previous order is virtual + 3. Closer order outside of operational depth must be real if previous order is real """ worker = orders5 if asset == 'base': @@ -208,8 +199,7 @@ def test_place_closer_order_real_or_virtual(orders5, asset): @pytest.mark.xfail(reason='https://github.com/bitshares/python-bitshares/issues/227') @pytest.mark.parametrize('asset', ['base', 'quote']) def test_place_closer_order_price_amount(orders5, asset): - """ Test that closer order price and amounts are correct - """ + """Test that closer order price and amounts are correct.""" worker = orders5 if asset == 'base': @@ -243,8 +233,7 @@ def test_place_closer_order_price_amount(orders5, asset): @pytest.mark.xfail(reason='https://github.com/bitshares/python-bitshares/issues/227') @pytest.mark.parametrize('asset', ['base', 'quote']) def test_place_closer_order_no_place_order(orders5, asset): - """ Test place_closer_order() with place_order=False kwarg - """ + """Test place_closer_order() with place_order=False kwarg.""" worker = orders5 if asset == 'base': @@ -271,8 +260,7 @@ def test_place_closer_order_no_place_order(orders5, asset): @pytest.mark.parametrize('asset', ['base', 'quote']) def test_place_closer_order_allow_partial_hard_limit(orders2, asset): - """ Test place_closer_order with allow_partial=True when avail balance is less than minimal allowed order size - """ + """Test place_closer_order with allow_partial=True when avail balance is less than minimal allowed order size.""" worker = orders2 if asset == 'base': @@ -294,9 +282,8 @@ def test_place_closer_order_allow_partial_hard_limit(orders2, asset): @pytest.mark.parametrize('asset', ['base', 'quote']) def test_place_closer_order_allow_partial(orders2, asset): - """ Test place_closer_order with allow_partial=True when avail balance is more than self.partial_fill_threshold - restriction (enough for partial order) - """ + """Test place_closer_order with allow_partial=True when avail balance is more than self.partial_fill_threshold + restriction (enough for partial order)""" worker = orders2 if asset == 'base': @@ -315,8 +302,7 @@ def test_place_closer_order_allow_partial(orders2, asset): @pytest.mark.parametrize('asset', ['base', 'quote']) def test_place_closer_order_not_allow_partial(orders2, asset): - """ Test place_closer_order with allow_partial=False - """ + """Test place_closer_order with allow_partial=False.""" worker = orders2 if asset == 'base': @@ -335,8 +321,7 @@ def test_place_closer_order_not_allow_partial(orders2, asset): @pytest.mark.parametrize('asset', ['base', 'quote']) def test_place_closer_order_own_asset_limit(orders5, asset): - """ Place closer order with own_asset_limit, test that amount of a new order is matching limit - """ + """Place closer order with own_asset_limit, test that amount of a new order is matching limit.""" worker = orders5 if asset == 'base': @@ -353,8 +338,7 @@ def test_place_closer_order_own_asset_limit(orders5, asset): @pytest.mark.parametrize('asset', ['base', 'quote']) def test_place_closer_order_opposite_asset_limit(orders5, asset): - """ Place closer order with opposite_asset_limit, test that amount of a new order is matching limit - """ + """Place closer order with opposite_asset_limit, test that amount of a new order is matching limit.""" worker = orders5 if asset == 'base': @@ -371,8 +355,7 @@ def test_place_closer_order_opposite_asset_limit(orders5, asset): @pytest.mark.parametrize('asset', ['base', 'quote']) def test_place_closer_order_instant_fill_disabled(orders5, asset): - """ When instant fill is disabled, new order should not cross lowest ask or highest bid - """ + """When instant fill is disabled, new order should not cross lowest ask or highest bid.""" worker = orders5 if asset == 'base': @@ -389,11 +372,12 @@ def test_place_closer_order_instant_fill_disabled(orders5, asset): @pytest.mark.parametrize('asset', ['base', 'quote']) def test_place_further_order_real_or_virtual(orders5, asset): - """ Further order may be real or virtual, depending on distance from the center and operational_depth + """ + Further order may be real or virtual, depending on distance from the center and operational_depth. - 1. Further order within operational depth must be real - 2. Further order within operational depth must be virtual if virtual=True was given - 2. Further order outside of operational depth must be virtual + 1. Further order within operational depth must be real + 2. Further order within operational depth must be virtual if virtual=True was given + 2. Further order outside of operational depth must be virtual """ worker = orders5 if asset == 'base': @@ -418,8 +402,7 @@ def test_place_further_order_real_or_virtual(orders5, asset): @pytest.mark.xfail(reason='https://github.com/bitshares/python-bitshares/issues/227') @pytest.mark.parametrize('asset', ['base', 'quote']) def test_place_further_order_price_amount(orders5, asset): - """ Test that further order price and amounts are correct - """ + """Test that further order price and amounts are correct.""" worker = orders5 if asset == 'base': @@ -453,8 +436,7 @@ def test_place_further_order_price_amount(orders5, asset): @pytest.mark.xfail(reason='https://github.com/bitshares/python-bitshares/issues/227') @pytest.mark.parametrize('asset', ['base', 'quote']) def test_place_further_order_no_place_order(orders5, asset): - """ Test place_further_order() with place_order=False kwarg - """ + """Test place_further_order() with place_order=False kwarg.""" worker = orders5 if asset == 'base': @@ -482,8 +464,7 @@ def test_place_further_order_no_place_order(orders5, asset): @pytest.mark.parametrize('asset', ['base', 'quote']) def test_place_further_order_not_allow_partial(orders2, asset): - """ Test place_further_order with allow_partial=False - """ + """Test place_further_order with allow_partial=False.""" worker = orders2 if asset == 'base': @@ -502,8 +483,7 @@ def test_place_further_order_not_allow_partial(orders2, asset): @pytest.mark.parametrize('asset', ['base', 'quote']) def test_place_further_order_allow_partial_hard_limit(orders2, asset): - """ Test place_further_order with allow_partial=True when avail balance is less than minimal allowed order size - """ + """Test place_further_order with allow_partial=True when avail balance is less than minimal allowed order size.""" worker = orders2 if asset == 'base': @@ -525,8 +505,7 @@ def test_place_further_order_allow_partial_hard_limit(orders2, asset): @pytest.mark.parametrize('asset', ['base', 'quote']) def test_place_further_order_allow_partial(orders2, asset): - """ Test place_further_order with allow_partial=True - """ + """Test place_further_order with allow_partial=True.""" worker = orders2 if asset == 'base': diff --git a/tests/strategies/staggered_orders/test_staggered_orders_init.py b/tests/strategies/staggered_orders/test_staggered_orders_init.py index 397211a03..5ff0181f1 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_init.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_init.py @@ -16,8 +16,7 @@ @pytest.mark.parametrize('spread, increment', [(1, 2), pytest.param(2, 2, marks=pytest.mark.xfail(reason="bug"))]) def test_spread_increment_check(bitshares, config, so_worker_name, spread, increment): - """ Spread must be greater than increment - """ + """Spread must be greater than increment.""" worker_name = so_worker_name incorrect_config = copy.deepcopy(config) incorrect_config['workers'][worker_name]['spread'] = spread @@ -27,8 +26,7 @@ def test_spread_increment_check(bitshares, config, so_worker_name, spread, incre def test_min_operational_depth(bitshares, config, so_worker_name): - """ Operational depth should not be too small - """ + """Operational depth should not be too small.""" worker_name = so_worker_name incorrect_config = copy.deepcopy(config) incorrect_config['workers'][worker_name]['operational_depth'] = 1 diff --git a/tests/strategies/staggered_orders/test_staggered_orders_lowlevel.py b/tests/strategies/staggered_orders/test_staggered_orders_lowlevel.py index 8b956376e..43d52a902 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_lowlevel.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_lowlevel.py @@ -45,8 +45,7 @@ def test_place_virtual_sell_order(worker, init_empty_balances): def test_sync_current_orders(orders1): - """ Sync current orders then fetch them back and compare to these orders - """ + """Sync current orders then fetch them back and compare to these orders.""" worker = orders1 worker.refresh_orders() worker.sync_current_orders() diff --git a/tests/strategies/staggered_orders/test_staggered_orders_mwsa.py b/tests/strategies/staggered_orders/test_staggered_orders_mwsa.py index 20f36f768..86f920fd3 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_mwsa.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_mwsa.py @@ -8,8 +8,7 @@ def test_mwsa_base_intersection(base_worker, config_multiple_workers_1, do_initial_allocation): - """ Check if workers usage of BASE asset is equal - """ + """Check if workers usage of BASE asset is equal.""" worker1 = base_worker(config_multiple_workers_1, worker_name="so-worker-1") worker2 = base_worker(config_multiple_workers_1, worker_name="so-worker-2") do_initial_allocation(worker1, worker1.mode) @@ -19,8 +18,7 @@ def test_mwsa_base_intersection(base_worker, config_multiple_workers_1, do_initi def test_mwsa_quote_intersection(base_worker, config_multiple_workers_2, do_initial_allocation): - """ Check if workers usage of QUOTE asset is equal - """ + """Check if workers usage of QUOTE asset is equal.""" worker1 = base_worker(config_multiple_workers_2, worker_name="so-worker-1") worker2 = base_worker(config_multiple_workers_2, worker_name="so-worker-2") do_initial_allocation(worker1, worker1.mode) @@ -30,8 +28,7 @@ def test_mwsa_quote_intersection(base_worker, config_multiple_workers_2, do_init def test_mwsa_manual_base_percent(base_worker, config_multiple_workers_1, do_initial_allocation): - """ Check if workers usage of BASE asset is in accordance with op_percent setting - """ + """Check if workers usage of BASE asset is in accordance with op_percent setting.""" worker1 = base_worker(config_multiple_workers_1, worker_name="so-worker-1") worker2 = base_worker(config_multiple_workers_1, worker_name="so-worker-2") worker1.operational_percent_base = 0.8 @@ -46,8 +43,7 @@ def test_mwsa_manual_base_percent(base_worker, config_multiple_workers_1, do_ini def test_mwsa_manual_quote_percent(base_worker, config_multiple_workers_2, do_initial_allocation): - """ Check if workers usage of QUOTE asset is in accordance with op_percent setting - """ + """Check if workers usage of QUOTE asset is in accordance with op_percent setting.""" worker1 = base_worker(config_multiple_workers_2, worker_name="so-worker-1") worker2 = base_worker(config_multiple_workers_2, worker_name="so-worker-2") worker1.operational_percent_quote = 0.8 diff --git a/tests/strategies/staggered_orders/test_staggered_orders_unittests.py b/tests/strategies/staggered_orders/test_staggered_orders_unittests.py index 18eafda0d..252e763a7 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_unittests.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_unittests.py @@ -11,8 +11,7 @@ def test_calculate_min_amounts(worker): - """ Min amounts should be greater than assets precision - """ + """Min amounts should be greater than assets precision.""" worker.calculate_min_amounts() assert worker.order_min_base > 10 ** -worker.market['base']['precision'] assert worker.order_min_quote > 10 ** -worker.market['quote']['precision'] diff --git a/tests/test_measure_latency.py b/tests/test_measure_latency.py index 3e86e74da..e912f479d 100644 --- a/tests/test_measure_latency.py +++ b/tests/test_measure_latency.py @@ -18,14 +18,12 @@ def many_failing_one_working(unused_port, bitshares_testnet): @pytest.mark.mandatory def test_measure_latency_all_failing(failing_nodes): - """ Expect an error if no nodes could be reached - """ + """Expect an error if no nodes could be reached.""" with pytest.raises(NumRetriesReached): MainController.measure_latency(failing_nodes) @pytest.mark.mandatory def test_measure_latency_one_working(many_failing_one_working): - """ Test connection to 3 nodes where only 3rd is working - """ + """Test connection to 3 nodes where only 3rd is working.""" MainController.measure_latency(many_failing_one_working) diff --git a/tests/test_worker_infrastructure.py b/tests/test_worker_infrastructure.py index 314d0a286..54e3e0e3e 100644 --- a/tests/test_worker_infrastructure.py +++ b/tests/test_worker_infrastructure.py @@ -27,8 +27,7 @@ def config(bitshares, account): @pytest.mark.mandatory def test_worker_infrastructure(bitshares, config): - """ Test whether dexbot core is able to work - """ + """Test whether dexbot core is able to work.""" worker_infrastructure = WorkerInfrastructure(config=config, bitshares_instance=bitshares) def wait_then_stop(): From 1e2042c62fefb4aaf206dd3023edef8a3a2f0894 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Mon, 6 Apr 2020 23:58:25 +0500 Subject: [PATCH 1794/1846] Run isort on all files --- dexbot/cli.py | 3 ++- dexbot/cli_conf.py | 3 ++- dexbot/config.py | 3 ++- dexbot/controllers/main_controller.py | 3 ++- dexbot/controllers/settings_controller.py | 3 ++- dexbot/controllers/worker_controller.py | 3 ++- dexbot/gui.py | 3 ++- dexbot/helper.py | 1 + dexbot/orderengines/bitshares_engine.py | 3 ++- dexbot/qt_queue/queue_dispatcher.py | 3 ++- dexbot/storage.py | 3 ++- dexbot/strategies/external_feeds/gecko_feed.py | 1 + dexbot/strategies/external_feeds/waves_feed.py | 3 ++- dexbot/strategies/king_of_the_hill.py | 1 + dexbot/strategies/staggered_orders.py | 1 + dexbot/ui.py | 3 ++- dexbot/views/create_wallet.py | 3 ++- dexbot/views/create_worker.py | 3 ++- dexbot/views/edit_worker.py | 3 ++- dexbot/views/errors.py | 3 ++- dexbot/views/settings.py | 3 ++- dexbot/views/strategy_form.py | 3 ++- dexbot/views/unlock_wallet.py | 3 ++- dexbot/views/worker_details.py | 5 +++-- dexbot/views/worker_item.py | 3 ++- dexbot/views/worker_list.py | 9 +++++---- dexbot/worker.py | 3 ++- tests/gecko_test.py | 1 + tests/migrations/conftest.py | 3 ++- tests/migrations/test_migrations.py | 1 + tests/storage/conftest.py | 1 + tests/strategies/king_of_the_hill/conftest.py | 1 + tests/strategies/relative_orders/conftest.py | 1 + tests/strategies/staggered_orders/conftest.py | 1 + .../staggered_orders/test_staggered_orders_highlevel.py | 1 + .../staggered_orders/test_staggered_orders_init.py | 1 + tests/test_measure_latency.py | 3 ++- tests/test_worker_infrastructure.py | 1 + 38 files changed, 67 insertions(+), 29 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index 1c9cb7c75..a41bb9005 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -11,12 +11,13 @@ import click # noqa: E402 import graphenecommon.exceptions from bitshares.market import Market +from uptick.decorators import online + from dexbot.cli_conf import SYSTEMD_SERVICE_NAME, get_whiptail, setup_systemd from dexbot.config import DEFAULT_CONFIG_FILE, Config from dexbot.helper import initialize_data_folders, initialize_orders_log from dexbot.storage import Storage from dexbot.ui import chain, configfile, reset_nodes, unlock, verbose -from uptick.decorators import online from . import errors, helper from .cli_conf import configure_dexbot, dexbot_service_running diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index 3394fb887..d14e13025 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -20,8 +20,9 @@ import subprocess import sys -import dexbot.helper from bitshares.account import Account + +import dexbot.helper from dexbot.config_validator import ConfigValidator from dexbot.node_manager import get_sorted_nodelist from dexbot.strategies.base import StrategyBase diff --git a/dexbot/config.py b/dexbot/config.py index e73b93d9b..e33fee783 100644 --- a/dexbot/config.py +++ b/dexbot/config.py @@ -3,9 +3,10 @@ from collections import OrderedDict, defaultdict import appdirs +from ruamel import yaml + from dexbot import APP_NAME, AUTHOR from dexbot.node_manager import get_sorted_nodelist -from ruamel import yaml DEFAULT_CONFIG_DIR = appdirs.user_config_dir(APP_NAME, appauthor=AUTHOR) DEFAULT_CONFIG_FILE = os.path.join(DEFAULT_CONFIG_DIR, 'config.yml') diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 78a5a834c..4a3160139 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -7,11 +7,12 @@ from bitshares.bitshares import BitShares from bitshares.instance import set_shared_bitshares_instance from bitsharesapi.bitsharesnoderpc import BitSharesNodeRPC +from grapheneapi.exceptions import NumRetriesReached + from dexbot import APP_NAME, AUTHOR, VERSION from dexbot.helper import initialize_data_folders, initialize_orders_log from dexbot.views.errors import PyQtHandler from dexbot.worker import WorkerInfrastructure -from grapheneapi.exceptions import NumRetriesReached class MainController: diff --git a/dexbot/controllers/settings_controller.py b/dexbot/controllers/settings_controller.py index e70e767dd..717772568 100644 --- a/dexbot/controllers/settings_controller.py +++ b/dexbot/controllers/settings_controller.py @@ -1,7 +1,8 @@ -from dexbot.config import Config from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QTreeWidgetItem +from dexbot.config import Config + class SettingsController: def __init__(self, view): diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index 71b9b2554..1f0099323 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -2,6 +2,8 @@ import re from bitshares.instance import shared_bitshares_instance +from PyQt5 import QtGui + from dexbot.config import Config from dexbot.config_validator import ConfigValidator from dexbot.helper import find_external_strategies @@ -9,7 +11,6 @@ from dexbot.views.errors import gui_error from dexbot.views.notice import NoticeDialog from dexbot.views.strategy_form import StrategyFormWidget -from PyQt5 import QtGui class WorkerController: diff --git a/dexbot/gui.py b/dexbot/gui.py index 7bc4fb1eb..47dd81f5c 100644 --- a/dexbot/gui.py +++ b/dexbot/gui.py @@ -1,9 +1,10 @@ import sys +from PyQt5.QtWidgets import QApplication + from dexbot.config import Config from dexbot.controllers.main_controller import MainController from dexbot.views.worker_list import MainView -from PyQt5.QtWidgets import QApplication class App(QApplication): diff --git a/dexbot/helper.py b/dexbot/helper.py index 1ffcc4b62..11a24f117 100644 --- a/dexbot/helper.py +++ b/dexbot/helper.py @@ -5,6 +5,7 @@ import shutil from appdirs import user_data_dir + from dexbot import APP_NAME, AUTHOR diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index c6972ac2a..0a6621bf4 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -12,10 +12,11 @@ from bitshares.market import Market from bitshares.price import FilledOrder, Order, UpdateCallOrder from bitshares.utils import formatTime +from events import Events + from dexbot.config import Config from dexbot.helper import truncate from dexbot.storage import Storage -from events import Events # Number of maximum retries used to retry action before failing MAX_TRIES = 3 diff --git a/dexbot/qt_queue/queue_dispatcher.py b/dexbot/qt_queue/queue_dispatcher.py index 843b701b1..410bbd39c 100644 --- a/dexbot/qt_queue/queue_dispatcher.py +++ b/dexbot/qt_queue/queue_dispatcher.py @@ -1,7 +1,8 @@ -from dexbot.qt_queue.idle_queue import idle_loop from PyQt5.QtCore import QEvent, QThread from PyQt5.QtWidgets import QApplication +from dexbot.qt_queue.idle_queue import idle_loop + class ThreadDispatcher(QThread): def __init__(self, parent): diff --git a/dexbot/storage.py b/dexbot/storage.py index 579512272..9d25f8fcd 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -9,11 +9,12 @@ import alembic import alembic.config from appdirs import user_data_dir -from dexbot import APP_NAME, AUTHOR from sqlalchemy import Boolean, Column, Float, Integer, String, create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import load_only, sessionmaker +from dexbot import APP_NAME, AUTHOR + from . import helper Base = declarative_base() diff --git a/dexbot/strategies/external_feeds/gecko_feed.py b/dexbot/strategies/external_feeds/gecko_feed.py index 030950fff..f5f14eb8f 100644 --- a/dexbot/strategies/external_feeds/gecko_feed.py +++ b/dexbot/strategies/external_feeds/gecko_feed.py @@ -1,6 +1,7 @@ import asyncio import requests + from dexbot.strategies.external_feeds.process_pair import debug, split_pair """ To use Gecko API, note that gecko does not provide pairs by default. diff --git a/dexbot/strategies/external_feeds/waves_feed.py b/dexbot/strategies/external_feeds/waves_feed.py index 91749d959..13c4e5432 100644 --- a/dexbot/strategies/external_feeds/waves_feed.py +++ b/dexbot/strategies/external_feeds/waves_feed.py @@ -1,8 +1,9 @@ import asyncio -import dexbot.strategies.external_feeds.process_pair import requests +import dexbot.strategies.external_feeds.process_pair + WAVES_URL = 'https://marketdata.wavesplatform.com/api/' SYMBOLS_URL = "/symbols" MARKET_URL = "/ticker/" diff --git a/dexbot/strategies/king_of_the_hill.py b/dexbot/strategies/king_of_the_hill.py index 44b09340a..ecde1ce5b 100644 --- a/dexbot/strategies/king_of_the_hill.py +++ b/dexbot/strategies/king_of_the_hill.py @@ -2,6 +2,7 @@ from decimal import Decimal from bitshares.price import Price + from dexbot.decorators import check_last_run from dexbot.strategies.base import StrategyBase from dexbot.strategies.config_parts.koth_config import KothConfig diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 734ca9f9d..152926861 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -6,6 +6,7 @@ import bitsharesapi.exceptions from bitshares.amount import Amount from bitshares.dex import Dex + from dexbot.decorators import check_last_run from dexbot.strategies.base import StrategyBase from dexbot.strategies.config_parts.staggered_config import StaggeredConfig diff --git a/dexbot/ui.py b/dexbot/ui.py index 244bdbb48..612538edd 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -10,10 +10,11 @@ from bitshares import BitShares from bitshares.exceptions import WrongMasterPasswordException from bitshares.instance import set_shared_bitshares_instance +from ruamel import yaml + from dexbot import APP_NAME, AUTHOR, VERSION from dexbot.config import Config from dexbot.node_manager import get_sorted_nodelist, ping -from ruamel import yaml log = logging.getLogger(__name__) diff --git a/dexbot/views/create_wallet.py b/dexbot/views/create_wallet.py index 5b341f49a..7e7538bc2 100644 --- a/dexbot/views/create_wallet.py +++ b/dexbot/views/create_wallet.py @@ -1,7 +1,8 @@ +from PyQt5.QtWidgets import QDialog + from dexbot.views.errors import gui_error from dexbot.views.notice import NoticeDialog from dexbot.views.ui.create_wallet_window_ui import Ui_Dialog -from PyQt5.QtWidgets import QDialog class CreateWalletView(QDialog, Ui_Dialog): diff --git a/dexbot/views/create_worker.py b/dexbot/views/create_worker.py index 010246ded..36d27ee0f 100644 --- a/dexbot/views/create_worker.py +++ b/dexbot/views/create_worker.py @@ -1,6 +1,7 @@ -from dexbot.controllers.worker_controller import UppercaseValidator, WorkerController from PyQt5 import QtWidgets +from dexbot.controllers.worker_controller import UppercaseValidator, WorkerController + from .ui.create_worker_window_ui import Ui_Dialog diff --git a/dexbot/views/edit_worker.py b/dexbot/views/edit_worker.py index a1d3a6edf..1f5cd0748 100644 --- a/dexbot/views/edit_worker.py +++ b/dexbot/views/edit_worker.py @@ -1,6 +1,7 @@ -from dexbot.controllers.worker_controller import UppercaseValidator, WorkerController from PyQt5 import QtWidgets +from dexbot.controllers.worker_controller import UppercaseValidator, WorkerController + from .ui.edit_worker_window_ui import Ui_Dialog diff --git a/dexbot/views/errors.py b/dexbot/views/errors.py index 497e329e2..4d51c7db5 100644 --- a/dexbot/views/errors.py +++ b/dexbot/views/errors.py @@ -1,9 +1,10 @@ import logging import traceback +from PyQt5 import QtCore, QtWidgets + from dexbot.qt_queue.idle_queue import idle_add from dexbot.ui import translate_error -from PyQt5 import QtCore, QtWidgets from .ui.error_dialog_ui import Ui_Dialog diff --git a/dexbot/views/settings.py b/dexbot/views/settings.py index d9847f87a..c2f90b170 100644 --- a/dexbot/views/settings.py +++ b/dexbot/views/settings.py @@ -1,6 +1,7 @@ +from PyQt5.QtWidgets import QDialog, QDialogButtonBox + from dexbot.controllers.settings_controller import SettingsController from dexbot.views.ui.settings_window_ui import Ui_settings_dialog -from PyQt5.QtWidgets import QDialog, QDialogButtonBox class SettingsView(QDialog, Ui_settings_dialog): diff --git a/dexbot/views/strategy_form.py b/dexbot/views/strategy_form.py index 3aeaa7e88..74f900633 100644 --- a/dexbot/views/strategy_form.py +++ b/dexbot/views/strategy_form.py @@ -1,8 +1,9 @@ import importlib -import dexbot.controllers.strategy_controller from PyQt5 import QtCore, QtGui, QtWidgets +import dexbot.controllers.strategy_controller + class StrategyFormWidget(QtWidgets.QWidget): def __init__(self, controller, strategy_module, worker_config=None): diff --git a/dexbot/views/unlock_wallet.py b/dexbot/views/unlock_wallet.py index 90cfef318..5b55f8344 100644 --- a/dexbot/views/unlock_wallet.py +++ b/dexbot/views/unlock_wallet.py @@ -1,7 +1,8 @@ +from PyQt5.QtWidgets import QDialog + from dexbot.views.errors import gui_error from dexbot.views.notice import NoticeDialog from dexbot.views.ui.unlock_wallet_window_ui import Ui_Dialog -from PyQt5.QtWidgets import QDialog class UnlockWalletView(QDialog, Ui_Dialog): diff --git a/dexbot/views/worker_details.py b/dexbot/views/worker_details.py index 5b56a39b2..ea98a8c1b 100644 --- a/dexbot/views/worker_details.py +++ b/dexbot/views/worker_details.py @@ -1,14 +1,15 @@ import importlib import os +from PyQt5 import QtWidgets +from PyQt5.QtWidgets import QWidget + from dexbot.controllers.worker_details_controller import WorkerDetailsController from dexbot.helper import get_user_data_directory from dexbot.views.ui.tabs.graph_tab_ui import Ui_Graph_Tab from dexbot.views.ui.tabs.table_tab_ui import Ui_Table_Tab from dexbot.views.ui.tabs.text_tab_ui import Ui_Text_Tab from dexbot.views.ui.worker_details_window_ui import Ui_details_dialog -from PyQt5 import QtWidgets -from PyQt5.QtWidgets import QWidget class WorkerDetailsView(QtWidgets.QDialog, Ui_details_dialog, Ui_Graph_Tab, Ui_Table_Tab, Ui_Text_Tab): diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index aa9673a34..0a3348810 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -1,9 +1,10 @@ import re +from PyQt5 import QtCore, QtWidgets + from dexbot.controllers.worker_controller import WorkerController from dexbot.storage import db_worker from dexbot.views.errors import gui_error -from PyQt5 import QtCore, QtWidgets from .confirmation import ConfirmationDialog from .edit_worker import EditWorkerView diff --git a/dexbot/views/worker_list.py b/dexbot/views/worker_list.py index 7890709a2..4ec6e5ab4 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -2,6 +2,11 @@ import webbrowser from threading import Thread +from grapheneapi.exceptions import NumRetriesReached +from PyQt5.QtCore import pyqtSlot +from PyQt5.QtGui import QFontDatabase +from PyQt5.QtWidgets import QMainWindow + from dexbot import __version__ from dexbot.config import Config from dexbot.controllers.wallet_controller import WalletController @@ -15,10 +20,6 @@ from dexbot.views.ui.worker_list_window_ui import Ui_MainWindow from dexbot.views.unlock_wallet import UnlockWalletView from dexbot.views.worker_item import WorkerItemWidget -from grapheneapi.exceptions import NumRetriesReached -from PyQt5.QtCore import pyqtSlot -from PyQt5.QtGui import QFontDatabase -from PyQt5.QtWidgets import QMainWindow class MainView(QMainWindow, Ui_MainWindow): diff --git a/dexbot/worker.py b/dexbot/worker.py index 8c19ee51d..5099f7fc7 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -5,10 +5,11 @@ import sys import threading -import dexbot.errors as errors from bitshares.instance import shared_bitshares_instance from bitshares.notify import Notify from bitshares.utils import parse_time + +import dexbot.errors as errors from dexbot.strategies.base import StrategyBase log = logging.getLogger(__name__) diff --git a/tests/gecko_test.py b/tests/gecko_test.py index 9c0b2f277..ce332e9af 100644 --- a/tests/gecko_test.py +++ b/tests/gecko_test.py @@ -1,4 +1,5 @@ import click + from dexbot.strategies.external_feeds.gecko_feed import get_gecko_price from dexbot.strategies.external_feeds.process_pair import split_pair from dexbot.styles import yellow diff --git a/tests/migrations/conftest.py b/tests/migrations/conftest.py index 2bc5108e1..dc871646c 100644 --- a/tests/migrations/conftest.py +++ b/tests/migrations/conftest.py @@ -3,11 +3,12 @@ import tempfile import pytest -from dexbot.storage import DatabaseWorker from sqlalchemy import Column, Float, Integer, String, create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker +from dexbot.storage import DatabaseWorker + log = logging.getLogger("dexbot") log.setLevel(logging.DEBUG) diff --git a/tests/migrations/test_migrations.py b/tests/migrations/test_migrations.py index 91caf2800..cbbb475ef 100644 --- a/tests/migrations/test_migrations.py +++ b/tests/migrations/test_migrations.py @@ -1,4 +1,5 @@ import pytest + from dexbot.storage import DatabaseWorker diff --git a/tests/storage/conftest.py b/tests/storage/conftest.py index 458643da7..40927213d 100644 --- a/tests/storage/conftest.py +++ b/tests/storage/conftest.py @@ -1,6 +1,7 @@ import logging import pytest + from dexbot.storage import Storage log = logging.getLogger("dexbot") diff --git a/tests/strategies/king_of_the_hill/conftest.py b/tests/strategies/king_of_the_hill/conftest.py index c64a9a5ae..e85e159be 100644 --- a/tests/strategies/king_of_the_hill/conftest.py +++ b/tests/strategies/king_of_the_hill/conftest.py @@ -7,6 +7,7 @@ from bitshares.asset import Asset from bitshares.dex import Dex from bitshares.price import Price + from dexbot.strategies.base import StrategyBase from dexbot.strategies.king_of_the_hill import Strategy diff --git a/tests/strategies/relative_orders/conftest.py b/tests/strategies/relative_orders/conftest.py index 853a33ed6..cacd785a0 100644 --- a/tests/strategies/relative_orders/conftest.py +++ b/tests/strategies/relative_orders/conftest.py @@ -4,6 +4,7 @@ import time import pytest + from dexbot.strategies.base import StrategyBase from dexbot.strategies.relative_orders import Strategy diff --git a/tests/strategies/staggered_orders/conftest.py b/tests/strategies/staggered_orders/conftest.py index 2d9684dc5..992baeb3c 100644 --- a/tests/strategies/staggered_orders/conftest.py +++ b/tests/strategies/staggered_orders/conftest.py @@ -6,6 +6,7 @@ import pytest from bitshares.amount import Amount + from dexbot.strategies.staggered_orders import Strategy log = logging.getLogger("dexbot") diff --git a/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py b/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py index b40570df5..84fc10932 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py @@ -2,6 +2,7 @@ import math import pytest + from dexbot.strategies.staggered_orders import VirtualOrder # Turn on debug for dexbot logger diff --git a/tests/strategies/staggered_orders/test_staggered_orders_init.py b/tests/strategies/staggered_orders/test_staggered_orders_init.py index 5ff0181f1..130f8fd10 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_init.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_init.py @@ -2,6 +2,7 @@ import logging import pytest + from dexbot.strategies.staggered_orders import Strategy # Turn on debug for dexbot logger diff --git a/tests/test_measure_latency.py b/tests/test_measure_latency.py index e912f479d..7c9021d7d 100644 --- a/tests/test_measure_latency.py +++ b/tests/test_measure_latency.py @@ -1,7 +1,8 @@ import pytest -from dexbot.controllers.main_controller import MainController from grapheneapi.exceptions import NumRetriesReached +from dexbot.controllers.main_controller import MainController + @pytest.fixture def failing_nodes(unused_port): diff --git a/tests/test_worker_infrastructure.py b/tests/test_worker_infrastructure.py index 54e3e0e3e..5cf38da11 100644 --- a/tests/test_worker_infrastructure.py +++ b/tests/test_worker_infrastructure.py @@ -3,6 +3,7 @@ import time import pytest + from dexbot.worker import WorkerInfrastructure logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s') From 9648b7e79862b6d2fa8b2eb363965ce6ef2c4f8b Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 7 Apr 2020 00:05:57 +0500 Subject: [PATCH 1795/1846] Fix some mypy errors --- dexbot/cli_conf.py | 2 +- dexbot/storage.py | 3 ++- docs/conf.py | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index d14e13025..58f9af666 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -43,7 +43,7 @@ while tag in tags_so_far: tag = tag + str(i) i += 1 - tags_so_far.add(tag) + tags_so_far.append(tag) STRATEGIES.append({'tag': tag, 'class': module, 'name': desc}) SYSTEMD_SERVICE_NAME = os.path.expanduser("~/.local/share/systemd/user/dexbot.service") diff --git a/dexbot/storage.py b/dexbot/storage.py index 9d25f8fcd..07bb181f1 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -5,6 +5,7 @@ import sys import threading import uuid +from typing import Any import alembic import alembic.config @@ -17,7 +18,7 @@ from . import helper -Base = declarative_base() +Base: Any = declarative_base() # For dexbot.sqlite file storageDatabase = "dexbot.sqlite" diff --git a/docs/conf.py b/docs/conf.py index 7b5585876..6f7e9a8b2 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,6 +15,7 @@ import os import sys +from typing import Dict # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -216,7 +217,7 @@ # -- Options for LaTeX output --------------------------------------------- -latex_elements = { +latex_elements: Dict[str, str] = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). From 82a7c8523d990fdea4222aba2f261b2900a27994 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 7 Apr 2020 23:33:08 +0500 Subject: [PATCH 1796/1846] Mark all storage tests as mandatory --- tests/storage/test_storage.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/storage/test_storage.py b/tests/storage/test_storage.py index 3eacb3777..6eec25bb4 100644 --- a/tests/storage/test_storage.py +++ b/tests/storage/test_storage.py @@ -5,8 +5,9 @@ log = logging.getLogger("dexbot") log.setLevel(logging.DEBUG) +pytestmark = pytest.mark.mandatory + -@pytest.mark.mandatory def test_fetch_orders(storage): order = {'id': '111', 'base': '10 CNY', 'quote': '1 BTS'} storage.save_order(order) @@ -15,7 +16,6 @@ def test_fetch_orders(storage): assert fetched[order['id']] == order -@pytest.mark.mandatory def test_fetch_orders_extended(storage): order = {'id': '111', 'base': '10 CNY', 'quote': '1 BTS'} text = 'foo bar' @@ -38,7 +38,6 @@ def test_fetch_orders_extended(storage): assert result['order'] == order -@pytest.mark.mandatory def test_clear_orders(storage): order = {'id': '111', 'base': '10 CNY', 'quote': '1 BTS'} storage.save_order(order) @@ -47,7 +46,6 @@ def test_clear_orders(storage): assert fetched is None -@pytest.mark.mandatory def test_clear_orders_extended(storage): order = {'id': '111', 'base': '10 CNY', 'quote': '1 BTS'} storage.save_order_extended(order, virtual=True) @@ -61,7 +59,6 @@ def test_clear_orders_extended(storage): assert fetched == [] -@pytest.mark.mandatory def test_remove_order(storage): order = {'id': '111', 'base': '10 CNY', 'quote': '1 BTS'} storage.save_order(order) @@ -69,7 +66,6 @@ def test_remove_order(storage): assert storage.fetch_orders() is None -@pytest.mark.mandatory def test_remove_order_by_id(storage): order = {'id': '111', 'base': '10 CNY', 'quote': '1 BTS'} storage.save_order(order) From 8a7af19f054ea5a973b0ce2247b47372d3651905 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 7 Apr 2020 23:51:56 +0500 Subject: [PATCH 1797/1846] Refactor DatabaseWorker singleton Instead of having single DatabaseWorker instance across all application, keep one instance per each sqlite database file. Now we can init Storage with custom sqlite database if needed. Closes: #667 --- dexbot/cli.py | 3 +- dexbot/storage.py | 100 ++++++++++-------- dexbot/strategies/base.py | 3 +- dexbot/views/worker_item.py | 7 +- tests/migrations/conftest.py | 2 +- tests/storage/conftest.py | 8 +- tests/storage/test_storage.py | 20 ++++ tests/strategies/staggered_orders/conftest.py | 14 ++- 8 files changed, 96 insertions(+), 61 deletions(-) diff --git a/dexbot/cli.py b/dexbot/cli.py index e70b43c69..a7c15f3aa 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -193,7 +193,8 @@ def drop_state(worker_name): """ Drop state of the worker (sqlite data) """ click.echo('Dropping state for {}'.format(worker_name)) - Storage.clear_worker_data(worker_name) + storage = Storage(worker_name) + storage.clear_worker_data() time.sleep(1) diff --git a/dexbot/storage.py b/dexbot/storage.py index 4b76152c0..91aa3442e 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -18,9 +18,6 @@ Base = declarative_base() -# For dexbot.sqlite file -storageDatabase = "dexbot.sqlite" - class Config(Base): __tablename__ = 'config' @@ -81,36 +78,62 @@ def __init__(self, account, worker, base_total, base_symbol, quote_total, quote_ class Storage(dict): """ Storage class - :param string category: The category to distinguish - different storage namespaces + Storage can be instantiated with custom database path. For each db file single DatabaseWorker instance is used. + This allows threadsafe db access from multiple threads. """ - def __init__(self, category): + # For each database path as key, we're keeping DatabaseWorker instance as value + __db_workers = {} + + def __init__(self, category, db_file=None): + """ + :param string category: The category to distinguish + different storage namespaces + :param str db_file: path to sqlite database file + """ self.category = category + db_file = db_file or self.get_default_db_file() + path = os.path.abspath(db_file) + # Get or create DatabaseWorker instance + self.db_worker = self.__db_workers.setdefault(path, DatabaseWorker(path)) + + @staticmethod + def get_default_db_file(): + filename = "dexbot.sqlite" + + # Derive sqlite file directory + data_dir = user_data_dir(APP_NAME, AUTHOR) + db_file = os.path.join(data_dir, filename) + + # Create directory for sqlite file + helper.mkdir(data_dir) + + return db_file + def __setitem__(self, key, value): - db_worker.set_item(self.category, key, value) + self.db_worker.set_item(self.category, key, value) def __getitem__(self, key): - return db_worker.get_item(self.category, key) + return self.db_worker.get_item(self.category, key) def __delitem__(self, key): - db_worker.del_item(self.category, key) + self.db_worker.del_item(self.category, key) def __contains__(self, key): - return db_worker.contains(self.category, key) + return self.db_worker.contains(self.category, key) def items(self): - return db_worker.get_items(self.category) + return self.db_worker.get_items(self.category) def clear(self): - db_worker.clear(self.category) + 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) + self.db_worker.save_order(self.category, order_id, order) def save_order_extended(self, order, virtual=None, custom=None): """ Save the order to the database providing additional data @@ -120,7 +143,7 @@ def save_order_extended(self, order, virtual=None, custom=None): :param str custom: any additional data """ order_id = order['id'] - db_worker.save_order_extended(self.category, order_id, order, virtual, custom) + self.db_worker.save_order_extended(self.category, order_id, order, virtual, custom) def remove_order(self, order): """ Removes an order from the database @@ -131,12 +154,12 @@ def remove_order(self, order): order_id = order['id'] else: order_id = order - db_worker.remove_order(self.category, order_id) + self.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) + self.db_worker.clear_orders(self.category) def clear_orders_extended(self, worker=None, only_virtual=False, only_real=False, custom=None): """ Removes worker's orders matching a criteria from the database @@ -150,7 +173,7 @@ def clear_orders_extended(self, worker=None, only_virtual=False, only_real=False raise ValueError('only_virtual and only_real are mutually exclusive') if not worker: worker = self.category - return db_worker.clear_orders_extended(worker, only_virtual, only_real, custom) + return self.db_worker.clear_orders_extended(worker, only_virtual, only_real, custom) def fetch_orders(self, worker=None): """ Get all the orders (or just specific worker's orders) from the database @@ -159,7 +182,7 @@ def fetch_orders(self, worker=None): """ if not worker: worker = self.category - return db_worker.fetch_orders(worker) + return self.db_worker.fetch_orders(worker) def fetch_orders_extended( self, worker=None, only_virtual=False, only_real=False, custom=None, return_ids_only=False @@ -179,39 +202,36 @@ def fetch_orders_extended( raise ValueError('only_virtual and only_real are mutually exclusive') if not worker: worker = self.category - return db_worker.fetch_orders_extended(worker, only_virtual, only_real, custom, return_ids_only) + return self.db_worker.fetch_orders_extended(worker, only_virtual, only_real, custom, return_ids_only) - @staticmethod - def clear_worker_data(worker): - db_worker.clear_orders(worker) - db_worker.clear(worker) + def clear_worker_data(self): + self.db_worker.clear_orders(self.category) + self.db_worker.clear(self.category) - @staticmethod def store_balance_entry( - account, worker, base_total, base_symbol, quote_total, quote_symbol, center_price, timestamp + self, account, worker, base_total, base_symbol, quote_total, quote_symbol, center_price, timestamp ): balance = Balances(account, worker, base_total, base_symbol, quote_total, quote_symbol, center_price, timestamp) # Save balance to db - db_worker.save_balance(balance) + self.db_worker.save_balance(balance) - @staticmethod - def get_balance_history(account, worker, timestamp, base_asset, quote_asset): - return db_worker.get_balance(account, worker, timestamp, base_asset, quote_asset) + def get_balance_history(self, account, worker, timestamp, base_asset, quote_asset): + return self.db_worker.get_balance(account, worker, timestamp, base_asset, quote_asset) - @staticmethod - def get_recent_balance_entry(account, worker, base_asset, quote_asset): - return db_worker.get_recent_balance_entry(account, worker, base_asset, quote_asset) + def get_recent_balance_entry(self, account, worker, base_asset, quote_asset): + return self.db_worker.get_recent_balance_entry(account, worker, base_asset, quote_asset) class DatabaseWorker(threading.Thread): """ Thread safe database worker """ - def __init__(self, **kwargs): + def __init__(self, sqlite_file, **kwargs): + """ + :param str sqlite_file: path to sqlite database file + """ super().__init__() - sqlite_file = kwargs.get('sqlite_file', sqlDataBaseFile) - # Obtain engine and session dsn = 'sqlite:///{}'.format(sqlite_file) engine = create_engine(dsn, echo=False) @@ -498,13 +518,3 @@ def _get_recent_balance_entry(self, account, worker, base_asset, quote_asset, to ) self._set_result(token, result) - - -# Derive sqlite file directory -data_dir = user_data_dir(APP_NAME, AUTHOR) -sqlDataBaseFile = os.path.join(data_dir, storageDatabase) - -# Create directory for sqlite file -helper.mkdir(data_dir) - -db_worker = DatabaseWorker() diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 1b1b8b5f4..44ff7b5a3 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -333,7 +333,8 @@ def purge_all_local_worker_data(worker_name): :param worker_name: Name of the worker to be removed """ - Storage.clear_worker_data(worker_name) + storage = Storage(worker_name) + storage.clear_worker_data() # GUI updaters def update_gui_slider(self): diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index fcaaf7b37..added0cdb 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -1,7 +1,7 @@ import re from dexbot.controllers.worker_controller import WorkerController -from dexbot.storage import db_worker +from dexbot.storage import Storage from dexbot.views.errors import gui_error from PyQt5 import QtCore, QtWidgets @@ -20,6 +20,7 @@ def __init__(self, worker_name, config, main_ctrl, view): self.worker_name = worker_name self.worker_config = self.main_ctrl.config.get_worker_config(worker_name) self.view = view + self.storage = Storage(self.worker_name) self.setupUi(self) @@ -41,13 +42,13 @@ def setup_ui_data(self, config): strategies = WorkerController.get_strategies() self.set_worker_strategy(strategies[module]['name']) - profit = db_worker.get_item(worker_name, 'profit') + profit = self.storage.db_worker.get_item(worker_name, 'profit') if profit: self.set_worker_profit(profit) else: self.set_worker_profit(0) - percentage = db_worker.get_item(worker_name, 'slider') + percentage = self.storage.db_worker.get_item(worker_name, 'slider') if percentage: self.set_worker_slider(percentage) else: diff --git a/tests/migrations/conftest.py b/tests/migrations/conftest.py index 2bc5108e1..f173880b7 100644 --- a/tests/migrations/conftest.py +++ b/tests/migrations/conftest.py @@ -52,7 +52,7 @@ class Balances(Base): def fresh_db(): _, db_file = tempfile.mkstemp() # noqa: F811 - _ = DatabaseWorker(sqlite_file=db_file) + _ = DatabaseWorker(db_file) yield db_file os.unlink(db_file) diff --git a/tests/storage/conftest.py b/tests/storage/conftest.py index 458643da7..f3970c1e4 100644 --- a/tests/storage/conftest.py +++ b/tests/storage/conftest.py @@ -1,4 +1,6 @@ import logging +import os +import tempfile import pytest from dexbot.storage import Storage @@ -10,5 +12,7 @@ @pytest.fixture def storage(): worker_name = 'test_worker' - yield Storage(worker_name) - Storage.clear_worker_data(worker_name) + _, db_file = tempfile.mkstemp() # noqa: F811 + storage = Storage(worker_name, db_file=db_file) + yield storage + os.unlink(db_file) diff --git a/tests/storage/test_storage.py b/tests/storage/test_storage.py index 6eec25bb4..151b2caa7 100644 --- a/tests/storage/test_storage.py +++ b/tests/storage/test_storage.py @@ -1,6 +1,8 @@ import logging +import tempfile import pytest +from dexbot.storage import Storage log = logging.getLogger("dexbot") log.setLevel(logging.DEBUG) @@ -8,6 +10,24 @@ pytestmark = pytest.mark.mandatory +def test_init(storage): + + # Storage instances with same db_file using single DatabaseWorker() + _, db_file = tempfile.mkstemp() # noqa: F811 + storage1 = Storage('test', db_file=db_file) + storage2 = Storage('test2', db_file=db_file) + assert storage1.db_worker is storage2.db_worker + + # Different db files - different DatabaseWorker() + storage3 = Storage('test') + assert storage3.db_worker is not storage1.db_worker + + +def test_get_default_db_file(storage): + file_ = storage.get_default_db_file() + assert isinstance(file_, str) + + def test_fetch_orders(storage): order = {'id': '111', 'base': '10 CNY', 'quote': '1 BTS'} storage.save_order(order) diff --git a/tests/strategies/staggered_orders/conftest.py b/tests/strategies/staggered_orders/conftest.py index b998537f9..0123724aa 100644 --- a/tests/strategies/staggered_orders/conftest.py +++ b/tests/strategies/staggered_orders/conftest.py @@ -6,6 +6,7 @@ import pytest from bitshares.amount import Amount +from dexbot.storage import Storage from dexbot.strategies.staggered_orders import Strategy log = logging.getLogger("dexbot") @@ -220,16 +221,13 @@ def _base_worker(config, worker_name=so_worker_name): @pytest.fixture(scope='session') -def storage_db(): +def storage_db(so_worker_name): """ Prepare custom sqlite database to not mess with main one - - TODO: this is doesn't work!!! """ - from dexbot.storage import sqlDataBaseFile - - _, sqlDataBaseFile = tempfile.mkstemp() # noqa: F811 - yield - os.unlink(sqlDataBaseFile) + _, db_file = tempfile.mkstemp() # noqa: F811 + storage = Storage(so_worker_name, db_file=db_file) + yield storage + os.unlink(db_file) @pytest.fixture From aeea44dc880a55feeb4a1ca004d194e147b52cf8 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 9 Apr 2020 20:33:59 +0500 Subject: [PATCH 1798/1846] Fix SO tests failed due to min_check_interval Regression introduced in ed2ef04cb4969d0db0e7edda7bd4e457184ccd73 --- tests/strategies/staggered_orders/conftest.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/strategies/staggered_orders/conftest.py b/tests/strategies/staggered_orders/conftest.py index b998537f9..8c8db2c88 100644 --- a/tests/strategies/staggered_orders/conftest.py +++ b/tests/strategies/staggered_orders/conftest.py @@ -204,6 +204,10 @@ def _base_worker(config, worker_name=so_worker_name): worker = Strategy(config=config, name=worker_name, bitshares_instance=bitshares) # Set market center price to avoid calling of maintain_strategy() worker.market_center_price = worker.worker['center_price'] + # Prevent maintenance bypass during tests + worker.min_check_interval = 0 + worker.max_check_interval = 0.00000000001 # should differ from min_check_interval! + worker.check_interval = worker.min_check_interval log.info('Initialized {} on account {}'.format(worker_name, worker.account.name)) workers.append(worker) return worker @@ -409,9 +413,6 @@ def maintain_until_allocated(): """ def func(worker): - # Speed up a little - worker.min_check_interval = 0.01 - worker.check_interval = worker.min_check_interval while True: worker.maintain_strategy() if not worker.check_interval == worker.min_check_interval: @@ -438,7 +439,7 @@ def func(worker, mode): maintain_until_allocated(worker) worker.refresh_orders() worker.refresh_balances(use_cached_orders=True) - worker.current_check_interval = 0 + worker.check_interval = 0 log.info('Initial allocation done') return worker From 9a06852845e97a008ceccd32567985e9de743544 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 9 Apr 2020 21:35:55 +0500 Subject: [PATCH 1799/1846] Add debug on asset issue --- tests/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 2ae5b7cf6..fed3e0b97 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +import logging import os.path import random import socket @@ -16,6 +17,8 @@ from bitsharesbase.account import PublicKey from bitsharesbase.chains import known_chains +log = logging.getLogger("dexbot") + # Note: chain_id is generated from genesis.json, every time it's changes you need to get new chain_id from # `bitshares.rpc.get_chain_properties()` known_chains["TEST"]["chain_id"] = "c74ddb39b3a233445dd95d7b6fc2d0fa4ba666698db26b53855d94fffcc460af" @@ -143,6 +146,7 @@ def issue_asset(bitshares): def _issue_asset(asset, amount, to): asset = Asset(asset, bitshares_instance=bitshares) + log.debug(f'Issuing {amount} of {asset.symbol} to {to}') asset.issue(amount, to) return _issue_asset From 404d2a5f19e1a9610854b33028725922a9316fb8 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 9 Apr 2020 20:25:21 +0500 Subject: [PATCH 1800/1846] Remove spread check from test_maintain_strategy_one_sided Not applicable with get_actual_spread() --- .../staggered_orders/test_staggered_orders_complex.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index dadcbbb17..513dea70f 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -80,9 +80,6 @@ def test_maintain_strategy_one_sided(mode, base_worker, config_only_base, do_ini worker = base_worker(config_only_base) do_initial_allocation(worker, mode) - # Check target spread is reached - assert worker.actual_spread == pytest.approx(worker.target_spread + worker.increment, abs=(worker.increment / 2)) - # Check number of orders price = worker.center_price / math.sqrt(1 + worker.target_spread) buy_orders_count = worker.calc_buy_orders_count(price, worker.lower_bound) From 97fc987daf9f5c8a8a317f89199f2233c10a6764 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 9 Apr 2020 16:15:52 +0500 Subject: [PATCH 1801/1846] Add test to repruduce #601 --- .../test_staggered_orders_complex.py | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index 513dea70f..f2d08b0db 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -1187,6 +1187,70 @@ def test_allocate_asset_partially_filled_order_on_massively_imbalanced_sides( assert spread_after < worker.target_spread + worker.increment +def test_allocate_asset_several_filled_orders_on_massively_imbalanced_sides( + worker, do_initial_allocation, base_account +): + """ When sides are massively imbalanced, make sure that spread will be closed after filling several orders on + smaller side. The goal is to test a situation when one side has a big-sized orders, and other side has much + smaller orders. Correct behavior: when multiple orders on smaller side filled at once, big side should place + appropriate number of closer orders to close the spread. + + Test for https://github.com/Codaone/DEXBot/issues/601 + """ + do_initial_allocation(worker, worker.mode) + spread_before = worker.get_actual_spread() + log.info('Worker spread after bootstrap: {}'.format(spread_before)) + # TODO: automatically turn off bootstrapping after target spread is closed? + worker['bootstrapping'] = False + + # Cancel several closest orders + num_orders_to_cancel = 3 + worker.cancel_orders_wrapper(worker.sell_orders[:num_orders_to_cancel]) + worker.refresh_orders() + worker.refresh_balances() + + # Place limited orders; the goal is to limit order amount to be much smaller than opposite + quote_limit = worker.buy_orders[0]['quote']['amount'] * worker.partial_fill_threshold / 2 + spread_after = worker.get_actual_spread() + while spread_after >= worker.target_spread + worker.increment: + # We're using spread check because we cannot just place same number of orders as num_orders_to_cancel because + # it may result in too close spread because of price shifts + worker.place_closer_order('quote', worker.sell_orders[0], own_asset_limit=quote_limit) + worker.refresh_orders() + spread_after = worker.get_actual_spread() + + log.info('Worker spread: {}'.format(worker.get_actual_spread())) + + # Fill only one newly placed order from another account + additional_account = base_account() + num_orders_to_fill = num_orders_to_cancel + for i in range(0, num_orders_to_fill): + price = worker.sell_orders[i]['price'] ** -1 * 1.01 + amount = worker.sell_orders[i]['base']['amount'] * 1.01 + log.debug('Filling {} @ {}'.format(amount, price)) + worker.market.buy(price, amount, account=additional_account) + + # Cancel unmatched dust + account = Account(additional_account, bitshares_instance=worker.bitshares) + ids = [order['id'] for order in account.openorders if 'id' in order] + worker.bitshares.cancel(ids, account=additional_account) + worker.refresh_orders() + worker.refresh_balances(use_cached_orders=True) + + # Allocate obtained BASE + counter = 0 + spread_after = worker.get_actual_spread() + while spread_after >= worker.target_spread + worker.increment: + worker.allocate_asset('base', worker.base_balance) + worker.refresh_orders() + worker.refresh_balances(use_cached_orders=True) + spread_after = worker.get_actual_spread() + counter += 1 + # Counter is for preventing infinity loop + # Success execution means target spread is reached + assert counter < 20 + + @pytest.mark.parametrize('mode', MODES) def test_allocate_asset_limiting_on_sell_side(mode, worker, do_initial_allocation, base_account): """ Check order size limiting when placing closer order on side which is bigger (using funds obtained from filled From 659106056b63ab5e592fbd948186bf722bf99033 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Thu, 9 Apr 2020 18:35:39 +0500 Subject: [PATCH 1802/1846] Update limiting logic in SO New logic correctly handles #601 corner case, which happens when spread is not reached, sides imbalanced, several sell orders filled at once and need to place closer buy order. To correctly determine limiter, just use previously stored orders (fetch from db). Updated logic is more correctly determines limits for mountain and neutral modes also. Closes: #601 --- dexbot/strategies/staggered_orders.py | 45 ++++++++++--------- .../test_staggered_orders_complex.py | 43 ++++++++++++------ 2 files changed, 54 insertions(+), 34 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index c9860c6d9..b7560be66 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -819,6 +819,7 @@ def allocate_asset(self, asset, asset_balance): order_type, actual_spread, self.target_spread + self.increment ) ) + if self['bootstrapping']: self.place_closer_order(asset, closest_own_order) elif opposite_orders and actual_spread - self.increment < self.target_spread + self.increment: @@ -826,40 +827,42 @@ def allocate_asset(self, asset, asset_balance): increases) """ self.place_closer_order(asset, closest_own_order, allow_partial=True) + # Place order limited by size of the opposite-side order elif opposite_orders: - # Place order limited by size of the opposite-side order - if self.mode == 'mountain': - opposite_asset_limit = closest_opposite_order['base']['amount'] * (1 + self.increment) - own_asset_limit = None - self.log.debug( - 'Limiting {} order by opposite order: {:.{prec}f} {}'.format( - order_type, opposite_asset_limit, opposite_symbol, prec=opposite_precision - ) - ) - elif (self.mode == 'buy_slope' and asset == 'base') or ( - self.mode == 'sell_slope' and asset == 'quote' + # Load previously stored opposite orders + result = self.fetch_orders_extended(custom='current') + stored_orders = [entry['order'] for entry in result] + if asset == 'base': + opposite_orders = self.filter_sell_orders(stored_orders, sort='DESC', invert=False) + elif asset == 'quote': + opposite_orders = self.filter_buy_orders(stored_orders, sort='DESC') + + try: + opposite_order = opposite_orders[0] + self.log.debug('Using stored opposite order') + except IndexError: + self.log.debug('Using real opposite order') + opposite_order = closest_opposite_order + + if ( + self.mode == 'mountain' + or (self.mode == 'buy_slope' and asset == 'base') + or (self.mode == 'sell_slope' and asset == 'quote') ): opposite_asset_limit = None - own_asset_limit = closest_opposite_order['quote']['amount'] + own_asset_limit = opposite_order['quote']['amount'] self.log.debug( 'Limiting {} order by opposite order: {:.{prec}f} {}'.format( order_type, own_asset_limit, own_symbol, prec=own_precision ) ) - elif self.mode == 'neutral': - opposite_asset_limit = closest_opposite_order['base']['amount'] * math.sqrt(1 + self.increment) - own_asset_limit = None - self.log.debug( - 'Limiting {} order by opposite order: {:.{prec}f} {}'.format( - order_type, opposite_asset_limit, opposite_symbol, prec=opposite_precision - ) - ) elif ( self.mode == 'valley' + or self.mode == 'neutral' or (self.mode == 'buy_slope' and asset == 'quote') or (self.mode == 'sell_slope' and asset == 'base') ): - opposite_asset_limit = closest_opposite_order['base']['amount'] + opposite_asset_limit = opposite_order['base']['amount'] own_asset_limit = None self.log.debug( 'Limiting {} order by opposite order: {:.{prec}f} {}'.format( diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index f2d08b0db..8a4e8798e 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -1187,8 +1187,9 @@ def test_allocate_asset_partially_filled_order_on_massively_imbalanced_sides( assert spread_after < worker.target_spread + worker.increment +@pytest.mark.parametrize('mode', MODES) def test_allocate_asset_several_filled_orders_on_massively_imbalanced_sides( - worker, do_initial_allocation, base_account + mode, worker, do_initial_allocation, base_account ): """ When sides are massively imbalanced, make sure that spread will be closed after filling several orders on smaller side. The goal is to test a situation when one side has a big-sized orders, and other side has much @@ -1197,6 +1198,7 @@ def test_allocate_asset_several_filled_orders_on_massively_imbalanced_sides( Test for https://github.com/Codaone/DEXBot/issues/601 """ + worker.mode = mode do_initial_allocation(worker, worker.mode) spread_before = worker.get_actual_spread() log.info('Worker spread after bootstrap: {}'.format(spread_before)) @@ -1211,13 +1213,17 @@ def test_allocate_asset_several_filled_orders_on_massively_imbalanced_sides( # Place limited orders; the goal is to limit order amount to be much smaller than opposite quote_limit = worker.buy_orders[0]['quote']['amount'] * worker.partial_fill_threshold / 2 + place_limited_order = True spread_after = worker.get_actual_spread() while spread_after >= worker.target_spread + worker.increment: # We're using spread check because we cannot just place same number of orders as num_orders_to_cancel because # it may result in too close spread because of price shifts - worker.place_closer_order('quote', worker.sell_orders[0], own_asset_limit=quote_limit) + limit = quote_limit if place_limited_order else None + worker.place_closer_order('quote', worker.sell_orders[0], own_asset_limit=limit) worker.refresh_orders() + worker.sync_current_orders() spread_after = worker.get_actual_spread() + place_limited_order = False # only first order should be limited log.info('Worker spread: {}'.format(worker.get_actual_spread())) @@ -1264,9 +1270,9 @@ def test_allocate_asset_limiting_on_sell_side(mode, worker, do_initial_allocatio # Fill several orders num_orders_to_fill = 4 for i in range(0, num_orders_to_fill): - price = worker.buy_orders[i]['price'] - amount = worker.buy_orders[i]['quote']['amount'] * 1.01 - log.debug('Filling {} @ {}'.format(amount, price)) + price = worker.buy_orders[i]['price'] / 1.01 + amount = worker.buy_orders[i]['quote']['amount'] + log.debug('Filling buy order buys {} QUOTE @ {}'.format(amount, price)) worker.market.sell(price, amount, account=additional_account) # Cancel unmatched dust @@ -1280,7 +1286,6 @@ def test_allocate_asset_limiting_on_sell_side(mode, worker, do_initial_allocatio spread_after = worker.get_actual_spread() counter = 0 while spread_after >= worker.target_spread + worker.increment: - worker.allocate_asset('base', worker.base_balance) worker.allocate_asset('quote', worker.quote_balance) worker.refresh_orders() worker.refresh_balances(use_cached_orders=True) @@ -1291,8 +1296,14 @@ def test_allocate_asset_limiting_on_sell_side(mode, worker, do_initial_allocatio # Check 2 closest orders to match mode if worker.mode == 'valley' or worker.mode == 'sell_slope': - assert worker.sell_orders[0]['base']['amount'] == worker.sell_orders[1]['base']['amount'] - elif worker.mode == 'mountain' or worker.mode == 'buy_slope': + assert worker.sell_orders[0]['base']['amount'] == pytest.approx(worker.sell_orders[1]['base']['amount']) + elif worker.mode == 'mountain': + assert ( + worker.sell_orders[0]['base']['amount'] + == pytest.approx(worker.sell_orders[1]['base']['amount'], abs=(10 ** -worker.market['quote']['precision'])) + or worker.sell_orders[0]['base']['amount'] >= worker.sell_orders[1]['base']['amount'] + ) + elif worker.mode == 'buy_slope': assert worker.sell_orders[0]['quote']['amount'] == pytest.approx( worker.sell_orders[1]['quote']['amount'], rel=(10 ** -worker.market['base']['precision']) ) @@ -1319,9 +1330,9 @@ def test_allocate_asset_limiting_on_buy_side(mode, worker, do_initial_allocation # Fill several orders num_orders_to_fill = 5 for i in range(0, num_orders_to_fill): - price = worker.sell_orders[i]['price'] ** -1 - amount = worker.sell_orders[i]['base']['amount'] * 1.01 - log.debug('Filling {} @ {}'.format(amount, price)) + price = worker.sell_orders[i]['price'] ** -1 * 1.01 + amount = worker.sell_orders[i]['base']['amount'] + log.debug('Filling {} QUOTE @ {}'.format(amount, price)) worker.market.buy(price, amount, account=additional_account) # Cancel unmatched dust @@ -1336,7 +1347,6 @@ def test_allocate_asset_limiting_on_buy_side(mode, worker, do_initial_allocation counter = 0 while spread_after >= worker.target_spread + worker.increment: worker.allocate_asset('base', worker.base_balance) - worker.allocate_asset('quote', worker.quote_balance) worker.refresh_orders() worker.refresh_balances(use_cached_orders=True) spread_after = worker.get_actual_spread() @@ -1347,7 +1357,14 @@ def test_allocate_asset_limiting_on_buy_side(mode, worker, do_initial_allocation # Check 2 closest orders to match mode if worker.mode == 'valley' or worker.mode == 'buy_slope': assert worker.buy_orders[0]['base']['amount'] == worker.buy_orders[1]['base']['amount'] - elif worker.mode == 'mountain' or worker.mode == 'sell_slope': + elif worker.mode == 'mountain': + # In mountain mode allow both equal orders and increased closest order - it may be placed without limiting + assert ( + worker.buy_orders[0]['base']['amount'] + == pytest.approx(worker.buy_orders[1]['base']['amount'], abs=(10 ** -worker.market['base']['precision'])) + or worker.buy_orders[0]['base']['amount'] >= worker.buy_orders[1]['base']['amount'] + ) + elif worker.mode == 'sell_slope': assert worker.buy_orders[0]['quote']['amount'] == pytest.approx( worker.buy_orders[1]['quote']['amount'], rel=(10 ** -worker.market['base']['precision']) ) From 88d79b7b14e5d9d6ee3d3c3bbd45b8d1e4ad7f38 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 10 Apr 2020 16:56:09 +0500 Subject: [PATCH 1803/1846] Disallow 0-profit spread/increment Profit from one filled order in SO should be > 0, previously >= 0 was allowed. --- dexbot/strategies/staggered_orders.py | 2 +- tests/strategies/staggered_orders/test_staggered_orders_init.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index b7560be66..b74e870d7 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -60,7 +60,7 @@ def __init__(self, *args, **kwargs): self.center_price = self.worker['center_price'] fee_sum = self.market['base'].market_fee_percent + self.market['quote'].market_fee_percent - if self.target_spread - self.increment < fee_sum: + if self.target_spread - self.increment <= fee_sum: self.log.error( 'Spread must be greater than increment by at least {}, refusing to work because worker' ' will make losses'.format(fee_sum) diff --git a/tests/strategies/staggered_orders/test_staggered_orders_init.py b/tests/strategies/staggered_orders/test_staggered_orders_init.py index 397211a03..10ee7d932 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_init.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_init.py @@ -14,7 +14,7 @@ ################### -@pytest.mark.parametrize('spread, increment', [(1, 2), pytest.param(2, 2, marks=pytest.mark.xfail(reason="bug"))]) +@pytest.mark.parametrize('spread, increment', [(1, 2), (2, 2)]) def test_spread_increment_check(bitshares, config, so_worker_name, spread, increment): """ Spread must be greater than increment """ From 50d5ce1eda87190a1aabc7dca65748330dc65b36 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 10 Apr 2020 17:58:50 +0500 Subject: [PATCH 1804/1846] Use only one market to run SO tests Looks like two different markets with flipped asset precisions is redundant and doubles tests run time. Leave only one market for speed up. --- tests/strategies/staggered_orders/conftest.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/strategies/staggered_orders/conftest.py b/tests/strategies/staggered_orders/conftest.py index 8c8db2c88..31e493442 100644 --- a/tests/strategies/staggered_orders/conftest.py +++ b/tests/strategies/staggered_orders/conftest.py @@ -17,10 +17,10 @@ def assets(create_asset): """ Create some assets with different precision """ - create_asset('BASEA', 3) - create_asset('QUOTEA', 8) - create_asset('BASEB', 8) - create_asset('QUOTEB', 3) + create_asset('BASEA', 8) + create_asset('QUOTEA', 3) + create_asset('BASEB', 3) + create_asset('QUOTEB', 8) @pytest.fixture(scope='module') @@ -65,7 +65,7 @@ def so_worker_name(): return 'so-worker' -@pytest.fixture(params=[('QUOTEA', 'BASEA'), ('QUOTEB', 'BASEB')]) +@pytest.fixture() def config(request, bitshares, account, so_worker_name): """ Define worker's config with variable assets @@ -77,7 +77,7 @@ def config(request, bitshares, account, so_worker_name): 'workers': { worker_name: { 'account': '{}'.format(account), - 'market': '{}/{}'.format(request.param[0], request.param[1]), + 'market': 'QUOTEA/BASEA', 'module': 'dexbot.strategies.staggered_orders', 'mode': 'valley', 'center_price': 100.0, From e5b6cf496268b986f3ffdb77f7e856c1f2285440 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 10 Apr 2020 16:50:06 +0500 Subject: [PATCH 1805/1846] Add workarounds for some SO tests Sevaral tests were market as xfailed due to imprecise price caclulation, this commit adds pytest.approx() to allow slight error, see #575 --- .../test_staggered_orders_highlevel.py | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py b/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py index 970b9d12f..d92e40a74 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py @@ -205,12 +205,12 @@ def test_place_closer_order_real_or_virtual(orders5, asset): assert closer_order, "Closer order within operational depth must be real" -@pytest.mark.xfail(reason='https://github.com/bitshares/python-bitshares/issues/227') @pytest.mark.parametrize('asset', ['base', 'quote']) def test_place_closer_order_price_amount(orders5, asset): """ Test that closer order price and amounts are correct """ worker = orders5 + precision = min(worker.market['base']['precision'], worker.market['quote']['precision']) if asset == 'base': order = worker.buy_orders[0] @@ -221,7 +221,7 @@ def test_place_closer_order_price_amount(orders5, asset): closer_order = worker.place_closer_order(asset, order, place_order=True) # Test for correct price - assert closer_order['price'] == order['price'] * (1 + worker.increment) + assert closer_order['price'] == pytest.approx(order['price'] * (1 + worker.increment), abs=(11 * 10 ** -precision)) # Test for correct amount if ( @@ -237,15 +237,17 @@ def test_place_closer_order_price_amount(orders5, asset): ): assert closer_order['base']['amount'] == order['base']['amount'] elif worker.mode == 'neutral': - assert closer_order['base']['amount'] == order['base']['amount'] * math.sqrt(1 + worker.increment) + assert closer_order['base']['amount'] == pytest.approx( + order['base']['amount'] * math.sqrt(1 + worker.increment), abs=(10 ** -order['base']['asset']['precision']) + ) -@pytest.mark.xfail(reason='https://github.com/bitshares/python-bitshares/issues/227') @pytest.mark.parametrize('asset', ['base', 'quote']) def test_place_closer_order_no_place_order(orders5, asset): """ Test place_closer_order() with place_order=False kwarg """ worker = orders5 + precision = min(worker.market['base']['precision'], worker.market['quote']['precision']) if asset == 'base': order = worker.buy_orders[0] @@ -265,8 +267,8 @@ def test_place_closer_order_no_place_order(orders5, asset): price = real_order['price'] ** -1 amount = real_order['base']['amount'] - assert closer_order['price'] == price - assert closer_order['amount'] == amount + assert closer_order['price'] == pytest.approx(price, abs=(11 * 10 ** -precision)) + assert closer_order['amount'] == pytest.approx(amount, abs=(10 ** -precision)) @pytest.mark.parametrize('asset', ['base', 'quote']) @@ -415,12 +417,12 @@ def test_place_further_order_real_or_virtual(orders5, asset): assert isinstance(further_order, VirtualOrder), "Further order outside of operational depth must be virtual" -@pytest.mark.xfail(reason='https://github.com/bitshares/python-bitshares/issues/227') @pytest.mark.parametrize('asset', ['base', 'quote']) def test_place_further_order_price_amount(orders5, asset): """ Test that further order price and amounts are correct """ worker = orders5 + precision = min(worker.market['base']['precision'], worker.market['quote']['precision']) if asset == 'base': order = worker.buy_orders[0] @@ -431,7 +433,7 @@ def test_place_further_order_price_amount(orders5, asset): further_order = worker.place_further_order(asset, order, place_order=True) # Test for correct price - assert further_order['price'] == order['price'] / (1 + worker.increment) + assert further_order['price'] == pytest.approx(order['price'] / (1 + worker.increment), abs=(2 * 10 ** -precision)) # Test for correct amount if ( @@ -447,15 +449,17 @@ def test_place_further_order_price_amount(orders5, asset): ): assert further_order['base']['amount'] == order['base']['amount'] elif worker.mode == 'neutral': - assert further_order['base']['amount'] == order['base']['amount'] / math.sqrt(1 + worker.increment) + assert further_order['base']['amount'] == pytest.approx( + order['base']['amount'] / math.sqrt(1 + worker.increment), abs=(10 ** -order['base']['asset']['precision']) + ) -@pytest.mark.xfail(reason='https://github.com/bitshares/python-bitshares/issues/227') @pytest.mark.parametrize('asset', ['base', 'quote']) def test_place_further_order_no_place_order(orders5, asset): """ Test place_further_order() with place_order=False kwarg """ worker = orders5 + precision = min(worker.market['base']['precision'], worker.market['quote']['precision']) if asset == 'base': order = worker.buy_orders[0] @@ -476,8 +480,8 @@ def test_place_further_order_no_place_order(orders5, asset): price = real_order['price'] ** -1 amount = real_order['base']['amount'] - assert further_order['price'] == price - assert further_order['amount'] == amount + assert further_order['price'] == pytest.approx(price, abs=(11 * 10 ** -precision)) + assert further_order['amount'] == pytest.approx(amount, abs=(10 ** -precision)) @pytest.mark.parametrize('asset', ['base', 'quote']) From f391226eb9171603c4c93173daf56927f4a06fdd Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Fri, 10 Apr 2020 22:13:21 +0500 Subject: [PATCH 1806/1846] Use separate assets for account_1_sat When all tests run at once, amount to issue may excess max supply --- tests/strategies/staggered_orders/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/strategies/staggered_orders/conftest.py b/tests/strategies/staggered_orders/conftest.py index 31e493442..e4293a9c2 100644 --- a/tests/strategies/staggered_orders/conftest.py +++ b/tests/strategies/staggered_orders/conftest.py @@ -54,7 +54,7 @@ def account_only_base(assets, prepare_account): def account_1_sat(assets, prepare_account): """ Prepare worker account to simulate XXX/BTC trading near zero prices """ - account = prepare_account({'BASEB': 0.02, 'QUOTEB': 10000000, 'TEST': 1000}) + account = prepare_account({'BASEC': 0.02, 'QUOTEC': 10000000, 'TEST': 1000}) return account @@ -124,7 +124,7 @@ def config_1_sat(so_worker_name, bitshares, account_1_sat): 'workers': { worker_name: { 'account': '{}'.format(account_1_sat), - 'market': 'QUOTEB/BASEB', + 'market': 'QUOTEC/BASEC', 'module': 'dexbot.strategies.staggered_orders', 'mode': 'valley', 'center_price': 0.00000001, From 596ed39733be0fd325a6233f8fdbc94bc44168b5 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 11 Apr 2020 00:51:23 +0500 Subject: [PATCH 1807/1846] Remove unused logger --- .../staggered_orders/test_staggered_orders_highlevel.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py b/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py index d92e40a74..ad8a3da00 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py @@ -1,14 +1,8 @@ -import logging import math import pytest from dexbot.strategies.staggered_orders import VirtualOrder -# Turn on debug for dexbot logger -logger = logging.getLogger("dexbot") -logger.setLevel(logging.DEBUG) - - ################### # Higher-level methods which depends on lower-level methods ################### From 4f99f1d664fb8a8ecda1a22b31d4c068968df102 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 11 Apr 2020 01:02:10 +0500 Subject: [PATCH 1808/1846] Set formatter for dexbot.per_worker in SO tests --- tests/strategies/staggered_orders/conftest.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/strategies/staggered_orders/conftest.py b/tests/strategies/staggered_orders/conftest.py index e4293a9c2..083a1f499 100644 --- a/tests/strategies/staggered_orders/conftest.py +++ b/tests/strategies/staggered_orders/conftest.py @@ -8,7 +8,14 @@ from bitshares.amount import Amount from dexbot.strategies.staggered_orders import Strategy +log = logging.getLogger("dexbot.per_worker") +handler = logging.StreamHandler() +formatter2 = logging.Formatter('%(asctime)s (%(module)s:%(lineno)d) - %(worker_name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter2) +log.addHandler(handler) + log = logging.getLogger("dexbot") +log.setLevel(logging.DEBUG) MODES = ['mountain', 'valley', 'neutral', 'buy_slope', 'sell_slope'] From 55e8e01efa67e4e25240daebefbddd48df897688 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sat, 11 Apr 2020 00:57:50 +0500 Subject: [PATCH 1809/1846] Use separate worker to simulate foreign trader Fixture-based approach (other_worker) is needed to correctly teardown tests not leaving open orders. git bisect note: all SO tests are passing --- tests/strategies/staggered_orders/conftest.py | 26 +++-- .../test_staggered_orders_complex.py | 94 ++++++++----------- 2 files changed, 58 insertions(+), 62 deletions(-) diff --git a/tests/strategies/staggered_orders/conftest.py b/tests/strategies/staggered_orders/conftest.py index 083a1f499..6be6918f0 100644 --- a/tests/strategies/staggered_orders/conftest.py +++ b/tests/strategies/staggered_orders/conftest.py @@ -6,6 +6,7 @@ import pytest from bitshares.amount import Amount +from dexbot.strategies.base import StrategyBase from dexbot.strategies.staggered_orders import Strategy log = logging.getLogger("dexbot.per_worker") @@ -203,6 +204,16 @@ def config_multiple_workers_2(config_multiple_workers_1): return config +@pytest.fixture +def config_other_account(config, base_account, so_worker_name): + """ Config for other account which simulates foreign trader + """ + config = copy.deepcopy(config) + worker_name = so_worker_name + config['workers'][worker_name]['account'] = base_account() + return config + + @pytest.fixture def base_worker(bitshares, so_worker_name, storage_db): workers = [] @@ -259,6 +270,15 @@ def worker2(base_worker, config_variable_modes): return worker +@pytest.fixture +def other_worker(so_worker_name, config_other_account): + """ Foreign trader + """ + worker = StrategyBase(name=so_worker_name, config=config_other_account) + yield worker + worker.cancel_all_orders() + + @pytest.fixture def init_empty_balances(worker, bitshares): # Defaults are None, which breaks place_virtual_xxx_order() @@ -272,8 +292,6 @@ def orders1(worker, bitshares, init_empty_balances): Note: this fixture don't calls refresh.xxx() intentionally! """ - # Make sure there are no orders - worker.cancel_all_orders() # Prices outside of the range buy_price = 1 # price for test_refresh_balances() sell_price = worker.upper_bound + 1 @@ -296,7 +314,6 @@ def orders1(worker, bitshares, init_empty_balances): def orders2(worker): """ Place buy+sell real orders near center price """ - worker.cancel_all_orders() buy_price = worker.market_center_price - 1 sell_price = worker.market_center_price + 1 # Place real orders @@ -314,7 +331,6 @@ def orders2(worker): def orders3(worker): """ Place buy+sell virtual orders near center price """ - worker.cancel_all_orders() worker.refresh_balances() buy_price = worker.market_center_price - 1 sell_price = worker.market_center_price + 1 @@ -341,7 +357,6 @@ def orders5(worker2): """ worker = worker2 - worker.cancel_all_orders() worker.refresh_balances() # Virtual orders outside of operational depth @@ -379,7 +394,6 @@ def orders5(worker2): def partially_filled_order(worker): """ Create partially filled order """ - worker.cancel_all_orders() order = worker.place_market_buy_order(100, 1, returnOrderId=True) worker.place_market_sell_order(20, 1) worker.refresh_balances() diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index 8a4e8798e..f5112ff51 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -4,7 +4,6 @@ from datetime import datetime import pytest -from bitshares.account import Account from bitshares.amount import Amount # Turn on debug for dexbot logger @@ -880,21 +879,20 @@ def test_allocate_asset_basic(worker): assert spread_after < spread_before -def test_allocate_asset_replace_closest_partial_order(worker, do_initial_allocation, base_account, issue_asset): +def test_allocate_asset_replace_closest_partial_order(worker, other_worker, do_initial_allocation, issue_asset): """ Test that partially filled order is replaced when target spread is not reached, before placing closer order """ do_initial_allocation(worker, worker.mode) - additional_account = base_account() # Sell some quote from another account to make PF order on buy side price = worker.buy_orders[0]['price'] / 1.01 amount = worker.buy_orders[0]['quote']['amount'] * (1 - worker.partial_fill_threshold * 1.1) - worker.market.sell(price, amount, account=additional_account) + other_worker.place_market_sell_order(amount, price) # Fill sell order price = worker.sell_orders[0]['price'] ** -1 * 1.01 amount = worker.sell_orders[0]['base']['amount'] - worker.market.buy(price, amount, account=additional_account) + other_worker.place_market_buy_order(amount, price) # Expect replaced closest buy order worker.refresh_orders() @@ -905,24 +903,24 @@ def test_allocate_asset_replace_closest_partial_order(worker, do_initial_allocat def test_allocate_asset_replace_partially_filled_orders( - worker, do_initial_allocation, base_account, issue_asset, maintain_until_allocated + worker, other_worker, do_initial_allocation, issue_asset, maintain_until_allocated ): """ Check replacement of partially filled orders on both sides. Simple check. """ do_initial_allocation(worker, worker.mode) # TODO: automatically turn off bootstrapping after target spread is closed? worker['bootstrapping'] = False - additional_account = base_account() # Partially fill closest orders price = worker.buy_orders[0]['price'] amount = worker.buy_orders[0]['quote']['amount'] / 2 log.debug('Filling {} @ {}'.format(amount, price)) - worker.market.sell(price, amount, account=additional_account) + other_worker.place_market_sell_order(amount, price) + price = worker.sell_orders[0]['price'] ** -1 amount = worker.sell_orders[0]['base']['amount'] / 2 log.debug('Filling {} @ {}'.format(amount, price)) - worker.market.buy(price, amount, account=additional_account) + other_worker.place_market_buy_order(amount, price) # Add some balance to worker to_issue = worker.buy_orders[0]['base']['amount'] @@ -954,18 +952,19 @@ def test_allocate_asset_increase_orders(worker, do_initial_allocation, maintain_ assert balance_in_orders_after['quote'] > balance_in_orders_before['quote'] -def test_allocate_asset_dust_order_simple(worker, do_initial_allocation, maintain_until_allocated, base_account): +def test_allocate_asset_dust_order_simple( + worker, other_worker, do_initial_allocation, maintain_until_allocated, base_account +): """ Make dust order, check if it canceled and closer opposite order placed """ do_initial_allocation(worker, worker.mode) num_sell_orders_before = len(worker.sell_orders) num_buy_orders_before = len(worker.buy_orders) - additional_account = base_account() # Partially fill order from another account sell_price = worker.buy_orders[0]['price'] / 1.01 sell_amount = worker.buy_orders[0]['quote']['amount'] * (1 - worker.partial_fill_threshold) * 1.1 - worker.market.sell(sell_price, sell_amount, account=additional_account) + other_worker.place_market_sell_order(sell_amount, sell_price) worker.refresh_balances() worker.refresh_orders() @@ -979,7 +978,7 @@ def test_allocate_asset_dust_order_simple(worker, do_initial_allocation, maintai def test_allocate_asset_dust_order_excess_funds( - worker, do_initial_allocation, maintain_until_allocated, base_account, issue_asset + worker, other_worker, do_initial_allocation, maintain_until_allocated, issue_asset ): """ Make dust order, add additional funds, these funds should be allocated and then dust order should be canceled and closer opposite order placed @@ -987,12 +986,11 @@ def test_allocate_asset_dust_order_excess_funds( do_initial_allocation(worker, worker.mode) num_sell_orders_before = len(worker.sell_orders) num_buy_orders_before = len(worker.buy_orders) - additional_account = base_account() # Partially fill order from another account sell_price = worker.buy_orders[0]['price'] / 1.01 sell_amount = worker.buy_orders[0]['quote']['amount'] * (1 - worker.partial_fill_threshold) * 1.1 - worker.market.sell(sell_price, sell_amount, account=additional_account) + other_worker.place_market_sell_order(sell_amount, sell_price) # Add some balance to the worker issue_asset(worker.market['quote']['symbol'], worker.sell_orders[0]['base']['amount'], worker.account.name) @@ -1008,13 +1006,12 @@ def test_allocate_asset_dust_order_excess_funds( assert num_sell_orders_after - num_sell_orders_before == 1 -def test_allocate_asset_dust_order_increase_race(worker, do_initial_allocation, base_account, issue_asset): +def test_allocate_asset_dust_order_increase_race(worker, other_worker, do_initial_allocation, issue_asset): """ Test for https://github.com/Codaone/DEXBot/issues/587 Check if cancelling dust orders on opposite side will not cause a race for allocate_asset() on opposite side """ do_initial_allocation(worker, worker.mode) - additional_account = base_account() num_buy_orders_before = len(worker.buy_orders) # Make closest sell order small enough to be a most likely candidate for increase @@ -1030,7 +1027,7 @@ def test_allocate_asset_dust_order_increase_race(worker, do_initial_allocation, buy_price = worker.sell_orders[0]['price'] ** -1 * 1.01 buy_amount = worker.sell_orders[0]['base']['amount'] * (1 - worker.partial_fill_threshold) * 1.1 log.debug('{}, {}'.format(buy_price, buy_amount)) - worker.market.buy(buy_price, buy_amount, account=additional_account) + other_worker.place_market_buy_order(buy_amount, buy_price) # PF fill sell order should be cancelled and closer buy placed worker.maintain_strategy() @@ -1039,19 +1036,18 @@ def test_allocate_asset_dust_order_increase_race(worker, do_initial_allocation, assert num_buy_orders_after - num_buy_orders_before == 1 -def test_allocate_asset_filled_orders(worker, do_initial_allocation, base_account): +def test_allocate_asset_filled_orders(worker, other_worker, do_initial_allocation, base_account): """ Fill an order and check if opposite order placed """ do_initial_allocation(worker, worker.mode) # TODO: automatically turn off bootstrapping after target spread is closed? worker['bootstrapping'] = False - additional_account = base_account() num_sell_orders_before = len(worker.sell_orders) # Fill sell order price = worker.buy_orders[0]['price'] amount = worker.buy_orders[0]['quote']['amount'] - worker.market.sell(price, amount, account=additional_account) + other_worker.place_market_sell_order(amount, price) worker.refresh_balances() worker.refresh_orders() worker.allocate_asset('quote', worker.quote_balance) @@ -1060,7 +1056,9 @@ def test_allocate_asset_filled_orders(worker, do_initial_allocation, base_accoun assert num_sell_orders_after - num_sell_orders_before == 1 -def test_allocate_asset_filled_order_on_massively_imbalanced_sides(worker, do_initial_allocation, base_account): +def test_allocate_asset_filled_order_on_massively_imbalanced_sides( + worker, other_worker, do_initial_allocation, base_account +): """ When sides are massively imbalanced, make sure that spread will be closed after filling one order on smaller side. The goal is to test a situation when one side has a big-sized orders, and other side has much smaller orders. Correct behavior: when order on smaller side filled, big side should place closer order. @@ -1092,18 +1090,15 @@ def test_allocate_asset_filled_order_on_massively_imbalanced_sides(worker, do_in log.info('Worker spread: {}'.format(worker.get_actual_spread())) # Fill only one newly placed order from another account - additional_account = base_account() num_orders_to_fill = 1 for i in range(0, num_orders_to_fill): price = worker.sell_orders[i]['price'] ** -1 * 1.01 amount = worker.sell_orders[i]['base']['amount'] * 1.01 log.debug('Filling {} @ {}'.format(amount, price)) - worker.market.buy(price, amount, account=additional_account) + other_worker.place_market_buy_order(amount, price) # Cancel unmatched dust - account = Account(additional_account, bitshares_instance=worker.bitshares) - ids = [order['id'] for order in account.openorders if 'id' in order] - worker.bitshares.cancel(ids, account=additional_account) + other_worker.cancel_all_orders() worker.refresh_orders() worker.refresh_balances(use_cached_orders=True) @@ -1124,7 +1119,7 @@ def test_allocate_asset_filled_order_on_massively_imbalanced_sides(worker, do_in def test_allocate_asset_partially_filled_order_on_massively_imbalanced_sides( - worker, do_initial_allocation, base_account + worker, other_worker, do_initial_allocation, base_account ): """ When sides are massively imbalanced, make sure that spread will be closed after filling one order on smaller side. The goal is to test a situation when one side has a big-sized orders, and other side has much @@ -1160,19 +1155,16 @@ def test_allocate_asset_partially_filled_order_on_massively_imbalanced_sides( log.info('Worker spread: {}'.format(worker.get_actual_spread())) # Fill only one newly placed order from another account - additional_account = base_account() num_orders_to_fill = 1 for i in range(0, num_orders_to_fill): price = worker.sell_orders[i]['price'] ** -1 * 1.01 # Make partially filled order (dust order) amount = worker.sell_orders[i]['base']['amount'] * (1 - worker.partial_fill_threshold) * 1.01 log.debug('Filling {} @ {}'.format(amount, price)) - worker.market.buy(price, amount, account=additional_account) + other_worker.place_market_buy_order(amount, price) # Cancel unmatched dust - account = Account(additional_account, bitshares_instance=worker.bitshares) - ids = [order['id'] for order in account.openorders if 'id' in order] - worker.bitshares.cancel(ids, account=additional_account) + other_worker.cancel_all_orders() worker.refresh_orders() worker.refresh_balances(use_cached_orders=True) @@ -1189,7 +1181,7 @@ def test_allocate_asset_partially_filled_order_on_massively_imbalanced_sides( @pytest.mark.parametrize('mode', MODES) def test_allocate_asset_several_filled_orders_on_massively_imbalanced_sides( - mode, worker, do_initial_allocation, base_account + mode, worker, other_worker, do_initial_allocation, base_account ): """ When sides are massively imbalanced, make sure that spread will be closed after filling several orders on smaller side. The goal is to test a situation when one side has a big-sized orders, and other side has much @@ -1228,18 +1220,15 @@ def test_allocate_asset_several_filled_orders_on_massively_imbalanced_sides( log.info('Worker spread: {}'.format(worker.get_actual_spread())) # Fill only one newly placed order from another account - additional_account = base_account() num_orders_to_fill = num_orders_to_cancel for i in range(0, num_orders_to_fill): price = worker.sell_orders[i]['price'] ** -1 * 1.01 amount = worker.sell_orders[i]['base']['amount'] * 1.01 log.debug('Filling {} @ {}'.format(amount, price)) - worker.market.buy(price, amount, account=additional_account) + other_worker.place_market_buy_order(amount, price) # Cancel unmatched dust - account = Account(additional_account, bitshares_instance=worker.bitshares) - ids = [order['id'] for order in account.openorders if 'id' in order] - worker.bitshares.cancel(ids, account=additional_account) + other_worker.cancel_all_orders() worker.refresh_orders() worker.refresh_balances(use_cached_orders=True) @@ -1258,14 +1247,13 @@ def test_allocate_asset_several_filled_orders_on_massively_imbalanced_sides( @pytest.mark.parametrize('mode', MODES) -def test_allocate_asset_limiting_on_sell_side(mode, worker, do_initial_allocation, base_account): +def test_allocate_asset_limiting_on_sell_side(mode, worker, other_worker, do_initial_allocation, base_account): """ Check order size limiting when placing closer order on side which is bigger (using funds obtained from filled orders on side which is smaller) """ do_initial_allocation(worker, mode) # TODO: automatically turn off bootstrapping after target spread is closed? worker['bootstrapping'] = False - additional_account = base_account() # Fill several orders num_orders_to_fill = 4 @@ -1273,12 +1261,10 @@ def test_allocate_asset_limiting_on_sell_side(mode, worker, do_initial_allocatio price = worker.buy_orders[i]['price'] / 1.01 amount = worker.buy_orders[i]['quote']['amount'] log.debug('Filling buy order buys {} QUOTE @ {}'.format(amount, price)) - worker.market.sell(price, amount, account=additional_account) + other_worker.place_market_sell_order(amount, price) # Cancel unmatched dust - account = Account(additional_account, bitshares_instance=worker.bitshares) - ids = [order['id'] for order in account.openorders if 'id' in order] - worker.bitshares.cancel(ids, account=additional_account) + other_worker.cancel_all_orders() # Allocate asset until target spread will be reached worker.refresh_orders() @@ -1315,7 +1301,7 @@ def test_allocate_asset_limiting_on_sell_side(mode, worker, do_initial_allocatio @pytest.mark.parametrize('mode', MODES) -def test_allocate_asset_limiting_on_buy_side(mode, worker, do_initial_allocation, base_account, issue_asset): +def test_allocate_asset_limiting_on_buy_side(mode, worker, other_worker, do_initial_allocation, issue_asset): """ Check order size limiting when placing closer order on side which is bigger (using funds obtained from filled orders on side which is smaller) """ @@ -1325,7 +1311,6 @@ def test_allocate_asset_limiting_on_buy_side(mode, worker, do_initial_allocation do_initial_allocation(worker, mode) # TODO: automatically turn off bootstrapping after target spread is closed? worker['bootstrapping'] = False - additional_account = base_account() # Fill several orders num_orders_to_fill = 5 @@ -1333,12 +1318,10 @@ def test_allocate_asset_limiting_on_buy_side(mode, worker, do_initial_allocation price = worker.sell_orders[i]['price'] ** -1 * 1.01 amount = worker.sell_orders[i]['base']['amount'] log.debug('Filling {} QUOTE @ {}'.format(amount, price)) - worker.market.buy(price, amount, account=additional_account) + other_worker.place_market_buy_order(amount, price) # Cancel unmatched dust - account = Account(additional_account, bitshares_instance=worker.bitshares) - ids = [order['id'] for order in account.openorders if 'id' in order] - worker.bitshares.cancel(ids, account=additional_account) + other_worker.cancel_all_orders() # Allocate asset until target spread will be reached worker.refresh_orders() @@ -1384,13 +1367,12 @@ def test_get_actual_spread(worker): assert float('Inf') > spread > 0 -def test_stop_loss_check(worker, base_account, do_initial_allocation, issue_asset): +def test_stop_loss_check(worker, other_worker, do_initial_allocation, issue_asset): worker.operational_depth = 100 worker.target_spread = 0.1 # speed up allocation do_initial_allocation(worker, worker.mode) - additional_account = base_account() # Issue additional QUOTE to 2nd account - issue_asset(worker.market['quote']['symbol'], 500, additional_account) + issue_asset(worker.market['quote']['symbol'], 500, other_worker.account.name) # Sleep is needed to allow node to update ticker time.sleep(2) @@ -1400,10 +1382,10 @@ def test_stop_loss_check(worker, base_account, do_initial_allocation, issue_asse assert worker.disabled is False # Place bid below lower bound - worker.market.buy(worker.lower_bound / 1.01, 1, account=additional_account) + other_worker.place_market_buy_order(1, worker.lower_bound / 1.01) # Fill all orders pushing price below lower bound - worker.market.sell(worker.lower_bound, 500, account=additional_account) + other_worker.place_market_sell_order(500, worker.lower_bound) time.sleep(2) worker.refresh_orders() From 197a4c2c291cf221201dfa70147b9a3d30b2a477 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 14 Apr 2020 00:01:35 +0500 Subject: [PATCH 1810/1846] Fix clean exit from cli See https://github.com/websocket-client/websocket-client/issues/449 Closes: #594 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8e2b6521d..2deee0354 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ uptick==0.2.4 ruamel.yaml>=0.15.37 appdirs>=1.4.3 pycryptodomex==3.6.4 -websocket-client==0.56.0 +websocket-client==0.57.0 sdnotify==0.3.2 sqlalchemy==1.3.0 click==7.0 From f030bf7eb51a69b715fdef167cdc4472557de99b Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Thu, 7 May 2020 09:33:54 +0300 Subject: [PATCH 1811/1846] Change dexbot version number to 0.25.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 77344bce9..0bbf948a1 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.24.2' +VERSION = '0.25.0' AUTHOR = 'Codaone Oy' __version__ = VERSION From 10f2e45adc6cccb685c479851926ba10d552d112 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Thu, 7 May 2020 09:36:46 +0300 Subject: [PATCH 1812/1846] Change dexbot version number to 0.26.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index 0bbf948a1..b583e4514 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.25.0' +VERSION = '0.26.0' AUTHOR = 'Codaone Oy' __version__ = VERSION From 492a7be11cd772bcf3d1c054edff8b11fed8fc3d Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Thu, 7 May 2020 09:45:26 +0300 Subject: [PATCH 1813/1846] Change dexbot version number to 0.27.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index b583e4514..eedbb1327 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.26.0' +VERSION = '0.27.0' AUTHOR = 'Codaone Oy' __version__ = VERSION From 5f18185132cba128b04336790c731afe6bbd1d54 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Thu, 7 May 2020 10:08:21 +0300 Subject: [PATCH 1814/1846] Change dexbot version number to 0.27.1 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index eedbb1327..ff1f655d4 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.27.0' +VERSION = '0.27.1' AUTHOR = 'Codaone Oy' __version__ = VERSION From f8684d3571fc30a41d3a258b0aae7f1030d730c4 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Thu, 7 May 2020 10:10:12 +0300 Subject: [PATCH 1815/1846] Change dexbot version number to 0.28.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index ff1f655d4..ca2aa86df 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.27.1' +VERSION = '0.28.0' AUTHOR = 'Codaone Oy' __version__ = VERSION From 79b96b9ec5461ef15e8573e38f7ca86d038de238 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 7 Apr 2020 18:04:07 +0500 Subject: [PATCH 1816/1846] Remove balances() from StrategyBase It's defined in BitsharesOrderEngine --- dexbot/strategies/base.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 2aff50dbb..3f956dcc6 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -264,15 +264,6 @@ def calc_profit(self): return profit - @property - def balances(self): - """ - Returns all the balances of the account assigned for the worker. - - :return: Balances in list where each asset is in their own Amount object - """ - return self._account.balances - @staticmethod def purge_all_local_worker_data(worker_name): """ From 6079ed9b30c3a9fb07971e34fc802a62501fe5c9 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 7 Apr 2020 18:11:31 +0500 Subject: [PATCH 1817/1846] Fix methods order --- dexbot/strategies/base.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 3f956dcc6..0d9ef7aec 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -64,14 +64,6 @@ class StrategyBase(BitsharesOrderEngine, BitsharesPriceFeed): throw an exception. The framework catches all exceptions thrown from event handlers and logs appropriately. """ - @classmethod - def configure(cls, return_base_config=True): - return BaseConfig.configure(return_base_config) - - @classmethod - def configure_details(cls, include_default_tabs=True): - return BaseConfig.configure_details(include_default_tabs) - __events__ = [ 'onAccount', 'onMarketUpdate', @@ -144,6 +136,23 @@ def __init__( self.orders_log = logging.LoggerAdapter(logging.getLogger('dexbot.orders_log'), {}) + @staticmethod + def purge_all_local_worker_data(worker_name): + """ + Removes worker's data and orders from local sqlite database. + + :param worker_name: Name of the worker to be removed + """ + Storage.clear_worker_data(worker_name) + + @classmethod + def configure(cls, return_base_config=True): + return BaseConfig.configure(return_base_config) + + @classmethod + def configure_details(cls, include_default_tabs=True): + return BaseConfig.configure_details(include_default_tabs) + def pause(self): """ Pause the worker. @@ -264,16 +273,6 @@ def calc_profit(self): return profit - @staticmethod - def purge_all_local_worker_data(worker_name): - """ - Removes worker's data and orders from local sqlite database. - - :param worker_name: Name of the worker to be removed - """ - storage = Storage(worker_name) - storage.clear_worker_data() - # GUI updaters def update_gui_slider(self): ticker = self.market.ticker() From 4f0184b16a19fcfc3d41587ca62bcd0d194db4c8 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 7 Apr 2020 18:52:37 +0500 Subject: [PATCH 1818/1846] Move call outside for cycle --- dexbot/orderengines/bitshares_engine.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index f0280a4cc..b85d937bc 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -765,8 +765,9 @@ def get_own_orders(self, refresh=True): if refresh: self._account.refresh() + worker_market = self._market.get_string('/') + for order in self._account.openorders: - worker_market = self._market.get_string('/') if worker_market == order.market and self._account.openorders: orders.append(order) From df5830d61aad143b793b440b237d7f165bab51db Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 7 Apr 2020 21:35:43 +0500 Subject: [PATCH 1819/1846] Add generic method to get operation balance Closes: #747 --- dexbot/strategies/base.py | 17 +++++++++++++++++ tests/primitives/test_strategybase.py | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 0d9ef7aec..299036a03 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -2,6 +2,7 @@ import math import time from datetime import datetime +from typing import Dict from dexbot.config import Config from dexbot.orderengines.bitshares_engine import BitsharesOrderEngine @@ -199,6 +200,22 @@ def get_worker_share_for_asset(self, asset): else: self.log.error('Got asset which is not used by this worker') + def get_operational_balance(self) -> Dict[str, float]: + """ + Get operational balance available to a worker. + + Operational balance is a part of the whole account balance which should be designated to this worker + + :return: dict with base and quote balance + """ + balance = self.count_asset() + op_percent_quote = self.get_worker_share_for_asset(self.market['quote']['symbol']) + op_percent_base = self.get_worker_share_for_asset(self.market['base']['symbol']) + balance['base'] *= op_percent_base + balance['quote'] *= op_percent_quote + + return balance + def store_profit_estimation_data(self): """Save total quote, total base, center_price, and datetime in to the database.""" assets = self.count_asset() diff --git a/tests/primitives/test_strategybase.py b/tests/primitives/test_strategybase.py index dc5227478..1a34da99d 100644 --- a/tests/primitives/test_strategybase.py +++ b/tests/primitives/test_strategybase.py @@ -14,3 +14,20 @@ def worker(strategybase): @pytest.mark.mandatory def test_init(worker): pass + + +@pytest.mark.parametrize('asset', ['base', 'quote']) +def test_get_operational_balance(asset, worker, monkeypatch): + share = 0.1 + + def get_share(*args): + return share + + symbol = worker.market[asset]['symbol'] + balance = worker.balance(symbol) + op_balance = worker.get_operational_balance() + assert op_balance[asset] == balance['amount'] + + monkeypatch.setattr(worker, 'get_worker_share_for_asset', get_share) + op_balance = worker.get_operational_balance() + assert op_balance[asset] == balance['amount'] * share From 43f7885badc8b65350fed9c500e4355348bf335a Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 7 Apr 2020 21:42:02 +0500 Subject: [PATCH 1820/1846] Fix flake8 errors --- dexbot/strategies/relative_orders.py | 116 +++++++++++++-------------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index f55be01dc..cece64c80 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -9,14 +9,6 @@ class Strategy(StrategyBase): """Relative Orders strategy.""" - @classmethod - def configure(cls, return_base_config=True): - return RelativeConfig.configure(return_base_config) - - @classmethod - def configure_details(cls, include_default_tabs=True): - return RelativeConfig.configure_details(include_default_tabs) - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.log.info("Initializing Relative Orders") @@ -118,20 +110,6 @@ def __init__(self, *args, **kwargs): else: self.check_orders() - def error(self, *args, **kwargs): - self.disabled = True - - def tick(self, d): - """ - Ticks come in on every block. - - We need to periodically check orders because cancelled orders do not triggers a market_update event - """ - if self.is_reset_on_price_change and not self.counter % 8: - self.log.debug('Checking orders by tick threshold') - self.check_orders() - self.counter += 1 - @property def amount_to_sell(self): """Get quote amount, calculate if order size is relative.""" @@ -165,6 +143,45 @@ def amount_to_buy(self): amount = 0 return amount + @staticmethod + def calculate_manual_offset(center_price, manual_offset): + """ + Adds manual offset to given center price. + + :param float | center_price: + :param float | manual_offset: + :return: Center price with manual offset + + Adjust center price by given percent in symmetrical way. Thus, -1% adjustement on BTS:USD market will be + same as adjusting +1% on USD:BTS market. + """ + if manual_offset < 0: + return center_price / (1 + abs(manual_offset)) + else: + return center_price * (1 + manual_offset) + + @classmethod + def configure(cls, return_base_config=True): + return RelativeConfig.configure(return_base_config) + + @classmethod + def configure_details(cls, include_default_tabs=True): + return RelativeConfig.configure_details(include_default_tabs) + + def error(self, *args, **kwargs): + self.disabled = True + + def tick(self, block_hash): + """ + Ticks come in on every block. + + We need to periodically check orders because cancelled orders do not triggers a market_update event + """ + if self.is_reset_on_price_change and not self.counter % 8: + self.log.debug('Checking orders by tick threshold') + self.check_orders() + self.counter += 1 + def get_external_market_center_price(self, external_price_source): """ Get center price from an external market for current market pair. @@ -453,24 +470,6 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, **kwargs): return base_amount / quote_amount - def _calculate_center_price(self, suppress_errors=False): - highest_bid = float(self.ticker().get('highestBid')) - lowest_ask = float(self.ticker().get('lowestAsk')) - - if highest_bid is None or highest_bid == 0.0: - 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: - if not suppress_errors: - self.log.critical("Cannot estimate center price, there is no lowest ask.") - self.disabled = True - return None - - # Calculate center price between two closest orders on the market - return highest_bid * math.sqrt(lowest_ask / highest_bid) - def calculate_center_price( self, center_price=None, asset_offset=False, spread=None, order_ids=None, manual_offset=0, suppress_errors=False ): @@ -528,23 +527,6 @@ def calculate_asset_offset(self, center_price, order_ids, spread): return math.pow(highest_price, base_percent) * math.pow(lowest_price, quote_percent) - @staticmethod - def calculate_manual_offset(center_price, manual_offset): - """ - Adds manual offset to given center price. - - :param float | center_price: - :param float | manual_offset: - :return: Center price with manual offset - - Adjust center price by given percent in symmetrical way. Thus, -1% adjustement on BTS:USD market will be - same as adjusting +1% on USD:BTS market. - """ - if manual_offset < 0: - return center_price / (1 + abs(manual_offset)) - else: - return center_price * (1 + manual_offset) - @check_last_run def check_orders(self, *args, **kwargs): """Tests if the orders need updating.""" @@ -559,7 +541,7 @@ def check_orders(self, *args, **kwargs): need_update = True else: # Loop trough the orders and look for changes - for order_id, order in orders.items(): + for order_id, _order in orders.items(): if not order_id.startswith('1.7.'): need_update = True break @@ -637,3 +619,21 @@ def get_own_last_trade(self): except UnboundLocalError: # base or quote wasn't obtained return None + + def _calculate_center_price(self, suppress_errors=False): + highest_bid = float(self.ticker().get('highestBid')) + lowest_ask = float(self.ticker().get('lowestAsk')) + + if highest_bid is None or highest_bid == 0.0: + 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: + if not suppress_errors: + self.log.critical("Cannot estimate center price, there is no lowest ask.") + self.disabled = True + return None + + # Calculate center price between two closest orders on the market + return highest_bid * math.sqrt(lowest_ask / highest_bid) From dca68e88a607795e898af9d545e429cedc20b4e5 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 7 Apr 2020 21:58:18 +0500 Subject: [PATCH 1821/1846] Respect operational percent settings in RO Closes: #712 --- dexbot/strategies/relative_orders.py | 8 ++++---- tests/strategies/relative_orders/test_relative_orders.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index cece64c80..048e388f1 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -115,8 +115,8 @@ def amount_to_sell(self): """Get quote amount, calculate if order size is relative.""" amount = self.order_size if self.is_relative_order_size: - quote_balance = float(self.balance(self.market["quote"])) - amount = quote_balance * (self.order_size / 100) + balance = self.get_operational_balance() + amount = balance['quote'] * (self.order_size / 100) # Sell / receive amount should match x2 of minimal possible fraction of asset if ( @@ -131,9 +131,9 @@ def amount_to_buy(self): """Get base amount, calculate if order size is relative.""" amount = self.order_size if self.is_relative_order_size: - base_balance = float(self.balance(self.market["base"])) + balance = self.get_operational_balance() # amount = % of balance / buy_price = amount combined with calculated price to give % of balance - amount = base_balance * (self.order_size / 100) / self.buy_price + amount = balance['base'] * (self.order_size / 100) / self.buy_price # Sell / receive amount should match x2 of minimal possible fraction of asset if ( diff --git a/tests/strategies/relative_orders/test_relative_orders.py b/tests/strategies/relative_orders/test_relative_orders.py index 680d6e539..7f823ccad 100644 --- a/tests/strategies/relative_orders/test_relative_orders.py +++ b/tests/strategies/relative_orders/test_relative_orders.py @@ -32,7 +32,7 @@ def test_amount_to_sell(ro_worker): assert amount_to_sell == expected_amount worker.is_relative_order_size = True - quote_balance = float(worker.balance(worker.market['quote'])) + quote_balance = worker.count_asset()['quote'] amount_to_sell = worker.amount_to_sell expected_amount = quote_balance * (expected_amount / 100) assert amount_to_sell == expected_amount @@ -46,7 +46,7 @@ def test_amount_to_buy(ro_worker): assert worker.amount_to_buy == expected_amount worker.is_relative_order_size = True - base_balance = float(worker.balance(worker.market['base'])) + base_balance = worker.count_asset()['base'] expected_amount = base_balance * (expected_amount / 100) / worker.buy_price assert worker.amount_to_buy == expected_amount From aeba2591d9852b46089d478bb68beb7beeb4c9c6 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 7 Apr 2020 22:02:27 +0500 Subject: [PATCH 1822/1846] Fix flake8 errors --- dexbot/strategies/king_of_the_hill.py | 58 +++++++++++++-------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/dexbot/strategies/king_of_the_hill.py b/dexbot/strategies/king_of_the_hill.py index ecde1ce5b..035e34ed6 100644 --- a/dexbot/strategies/king_of_the_hill.py +++ b/dexbot/strategies/king_of_the_hill.py @@ -26,14 +26,6 @@ class Strategy(StrategyBase): closer to the opposing order book than any other order. """ - @classmethod - def configure(cls, return_base_config=True): - return KothConfig.configure(return_base_config) - - @classmethod - def configure_details(cls, include_default_tabs=True): - return KothConfig.configure_details(include_default_tabs) - def __init__(self, *args, **kwargs): # Initializes StrategyBase class super().__init__(*args, **kwargs) @@ -89,6 +81,34 @@ def __init__(self, *args, **kwargs): self.log.info("{} initialized.".format(STRATEGY_NAME)) + @property + def amount_quote(self): + """Get quote amount, calculate if order size is relative.""" + amount = self.sell_order_amount + if self.is_relative_order_size: + quote_balance = float(self.balance(self.market['quote'])) + amount = quote_balance * (amount / 100) + + return amount + + @property + def amount_base(self): + """Get base amount, calculate if order size is relative.""" + amount = self.buy_order_amount + if self.is_relative_order_size: + base_balance = float(self.balance(self.market['base'])) + amount = base_balance * (amount / 100) + + return amount + + @classmethod + def configure(cls, return_base_config=True): + return KothConfig.configure(return_base_config) + + @classmethod + def configure_details(cls, include_default_tabs=True): + return KothConfig.configure_details(include_default_tabs) + def check_bitasset_market(self): """Check if worker market is MPA:COLLATERAL market.""" if not (self.market['base'].is_bitasset or self.market['quote'].is_bitasset): @@ -395,31 +415,11 @@ def place_orders(self): if place_sell: self.place_order('sell') - @property - def amount_quote(self): - """Get quote amount, calculate if order size is relative.""" - amount = self.sell_order_amount - if self.is_relative_order_size: - quote_balance = float(self.balance(self.market['quote'])) - amount = quote_balance * (amount / 100) - - return amount - - @property - def amount_base(self): - """Get base amount, calculate if order size is relative.""" - amount = self.buy_order_amount - if self.is_relative_order_size: - base_balance = float(self.balance(self.market['base'])) - amount = base_balance * (amount / 100) - - return amount - def error(self, *args, **kwargs): """Defines what happens when error occurs.""" self.disabled = True - def tick(self, d): + def tick(self, block_hash): """Ticks come in on every block.""" if not (self.counter or 0) % 4: self.maintain_strategy() From 2c748d085fdac9eedb231971be6ad58b3f5baa9c Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Tue, 7 Apr 2020 22:05:12 +0500 Subject: [PATCH 1823/1846] Respect operational percent settings in KOTH --- dexbot/strategies/king_of_the_hill.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/king_of_the_hill.py b/dexbot/strategies/king_of_the_hill.py index 035e34ed6..0eefb6773 100644 --- a/dexbot/strategies/king_of_the_hill.py +++ b/dexbot/strategies/king_of_the_hill.py @@ -86,8 +86,8 @@ def amount_quote(self): """Get quote amount, calculate if order size is relative.""" amount = self.sell_order_amount if self.is_relative_order_size: - quote_balance = float(self.balance(self.market['quote'])) - amount = quote_balance * (amount / 100) + balance = self.get_operational_balance() + amount = balance['quote'] * (amount / 100) return amount @@ -96,8 +96,8 @@ def amount_base(self): """Get base amount, calculate if order size is relative.""" amount = self.buy_order_amount if self.is_relative_order_size: - base_balance = float(self.balance(self.market['base'])) - amount = base_balance * (amount / 100) + balance = self.get_operational_balance() + amount = balance['base'] * (amount / 100) return amount From 0d9761e9cb9c5973d313516ef3babc8cd2aa23ea Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 10 May 2020 14:06:27 +0500 Subject: [PATCH 1824/1846] Set semantic-versioned deps in requirements-dev.txt --- requirements-dev.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 3d59ffefa..027b1ea92 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ # Packages needed for running testsuite -docker==3.7.2 -pytest==4.4.0 +docker>=3.7.2,<4 +pytest>=4.4.0,<5 # Needed for development -pre-commit==1.20.0 +pre-commit>=1.20.0,<2 From e15e5e90e82c0ca5cdc845c617634160e5a84f72 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 10 May 2020 14:10:31 +0500 Subject: [PATCH 1825/1846] Remove all indirect requirements --- requirements.txt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2deee0354..0ea17a510 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,7 @@ pyqt5==5.10 pyqt-distutils==0.7.3 pyinstaller==3.4 -click-datetime==0.2 -cryptography==2.3 -aiohttp==3.0.1 requests==2.21.0 -yarl==1.1.0 ccxt==1.17.434 pywaves==0.8.20 graphenelib==1.2.0 @@ -13,8 +9,6 @@ bitshares==0.4.0 uptick==0.2.4 ruamel.yaml>=0.15.37 appdirs>=1.4.3 -pycryptodomex==3.6.4 -websocket-client==0.57.0 sdnotify==0.3.2 sqlalchemy==1.3.0 click==7.0 From 070069a1bb7b76d286315d26f6b0c489b317cca7 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 10 May 2020 14:10:56 +0500 Subject: [PATCH 1826/1846] Move pyinstaller dep to requirements-dev.txt --- requirements-dev.txt | 4 ++++ requirements.txt | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 027b1ea92..0dfb81d07 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,9 @@ # Packages needed for running testsuite docker>=3.7.2,<4 pytest>=4.4.0,<5 + # Needed for development pre-commit>=1.20.0,<2 + +# Make bundled executables +pyinstaller>=3.4,<4 diff --git a/requirements.txt b/requirements.txt index 0ea17a510..2492538f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ pyqt5==5.10 pyqt-distutils==0.7.3 -pyinstaller==3.4 requests==2.21.0 ccxt==1.17.434 pywaves==0.8.20 From beb2b9e64416e07e9fc6dcfccf92b181e2e02fca Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 10 May 2020 14:14:21 +0500 Subject: [PATCH 1827/1846] Set main requirements to semantically-versioned Closes: #767 --- requirements.txt | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2492538f3..e738b6706 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,14 @@ -pyqt5==5.10 -pyqt-distutils==0.7.3 -requests==2.21.0 -ccxt==1.17.434 -pywaves==0.8.20 -graphenelib==1.2.0 -bitshares==0.4.0 -uptick==0.2.4 -ruamel.yaml>=0.15.37 -appdirs>=1.4.3 -sdnotify==0.3.2 -sqlalchemy==1.3.0 -click==7.0 -alembic==1.0.11 +pyqt5>=5.10,<6 +pyqt-distutils>=0.7.3,<1 +requests>=2.21.0,<3 +ccxt>=1.17.434,<2 +pywaves>=0.8.20,<1 +graphenelib>=1.2.0,<2 +bitshares>=0.4.0,<0.5 +uptick>=0.2.4,<1 +ruamel.yaml>=0.15.37,<1 +appdirs>=1.4.3,<2 +sdnotify>=0.3.2,<1 +sqlalchemy>=1.3.0,<2 +click>=7.0,<8 +alembic>=1.0.11,<2 From fdf09599d1ed5da6fac01c0c73ecaeb5d3f31468 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 10 May 2020 14:33:19 +0500 Subject: [PATCH 1828/1846] Read requirements.txt inside setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9aab3a9c2..f6db338f2 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ cmd_class = {} console_scripts = ['dexbot-cli = dexbot.cli:main'] -install_requires = [] +install_requires = open("requirements.txt").readlines() class BuildCommand(build_module.build): From 9e45b4f51eb119e280ca1d99d4b9c1823c37ad52 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 10 May 2020 14:45:30 +0500 Subject: [PATCH 1829/1846] Bump bitshares dependency Also remove direct graphenelib dependency, we don't need it. --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index e738b6706..d3293596d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,8 +3,7 @@ pyqt-distutils>=0.7.3,<1 requests>=2.21.0,<3 ccxt>=1.17.434,<2 pywaves>=0.8.20,<1 -graphenelib>=1.2.0,<2 -bitshares>=0.4.0,<0.5 +bitshares>=0.5.0,<0.6 uptick>=0.2.4,<1 ruamel.yaml>=0.15.37,<1 appdirs>=1.4.3,<2 From d1744d9f86688ad6e44f20cb880f90ad72952d8a Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 10 May 2020 15:06:46 +0500 Subject: [PATCH 1830/1846] Restore websocket-client dependency --- requirements.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d3293596d..8f85e3d27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ pyqt5>=5.10,<6 -pyqt-distutils>=0.7.3,<1 requests>=2.21.0,<3 ccxt>=1.17.434,<2 pywaves>=0.8.20,<1 @@ -11,3 +10,9 @@ sdnotify>=0.3.2,<1 sqlalchemy>=1.3.0,<2 click>=7.0,<8 alembic>=1.0.11,<2 + +# GUI building +pyqt-distutils>=0.7.3,<1 + +# Non-direct but needed to avoid an issue +websocket-client>=0.57.0,<1 From 5ffbc2c3f98361c453637873ead8c55609b33868 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 10 May 2020 15:19:41 +0500 Subject: [PATCH 1831/1846] Install devel requirements before running pyinstaller --- Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index cd95a9fc0..5b7ccdbed 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,9 @@ clean-pyc: pip: python3 -m pip install -r requirements.txt +pip-dev: + python3 -m pip install -r requirements-dev.txt + pip-user: python3 -m pip install --user -r requirements.txt @@ -48,7 +51,7 @@ git: check: pip python3 setup.py check -package: build +package: build pip-dev pyinstaller gui.spec pyinstaller cli.spec From 80d56f7940c3500829a29687cd918552ad998af8 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 10 May 2020 15:32:18 +0500 Subject: [PATCH 1832/1846] Remove websocket-client dependency Required version is conflicting with python-graphenelib dependency. Reopen #594 --- requirements.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8f85e3d27..22247f001 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,3 @@ alembic>=1.0.11,<2 # GUI building pyqt-distutils>=0.7.3,<1 - -# Non-direct but needed to avoid an issue -websocket-client>=0.57.0,<1 From 6778c36881ceacd81a686f06b4c2f35a092b846a Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 10 May 2020 15:50:01 +0500 Subject: [PATCH 1833/1846] Stick to pyqt<5.14 Travis cannot build 5.14.x due to old pip version, the error: AttributeError: module 'sipbuild.api' has no attribute 'prepare_metadata_for_build_wheel' See https://www.riverbankcomputing.com/pipermail/pyqt/2020-January/042430.html --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 22247f001..a4b154fc2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -pyqt5>=5.10,<6 +pyqt5>=5.10,<5.14 requests>=2.21.0,<3 ccxt>=1.17.434,<2 pywaves>=0.8.20,<1 From 4fa5f44a5a5c269d3d7cf8fafea070231b4e8bd0 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 10 May 2020 16:01:57 +0500 Subject: [PATCH 1834/1846] Bump pytest dependency --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 0dfb81d07..7bb13cdaf 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,6 @@ # Packages needed for running testsuite docker>=3.7.2,<4 -pytest>=4.4.0,<5 +pytest>=5,<6 # Needed for development pre-commit>=1.20.0,<2 From 487223bb409e40531df1e3fd414e95ad30c5d40e Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 10 May 2020 16:30:15 +0500 Subject: [PATCH 1835/1846] Fix compatibility with pyinstaller 3.6 Old hook name Crypto was in conflict with built-in pyinstaller hook, so rename local hook. --- hooks/{hook-Crypto.py => hook-Crypto-local.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename hooks/{hook-Crypto.py => hook-Crypto-local.py} (100%) diff --git a/hooks/hook-Crypto.py b/hooks/hook-Crypto-local.py similarity index 100% rename from hooks/hook-Crypto.py rename to hooks/hook-Crypto-local.py From 6a6f23f96cacffffdc0b751cba6c048b3673fca9 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 10 May 2020 16:31:10 +0500 Subject: [PATCH 1836/1846] Fix travis build --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4f355de64..5c445cd0b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,6 @@ before_install: - docker build -t dexbot/dexbot . - docker pull bitshares/bitshares-core:testnet install: - - pip install pyinstaller - pip install --upgrade setuptools - make install - if [ "$TRAVIS_OS_NAME" = "linux" ]; then python3 -m pip install -r requirements-dev.txt ; fi @@ -30,6 +29,7 @@ before_script: - if [ "$TRAVIS_OS_NAME" = "linux" ]; then pytest -m mandatory tests/ && python3 -m pip uninstall -y -r requirements-dev.txt; fi script: + - pip install pyinstaller - pyinstaller --distpath dist/$TRAVIS_OS_NAME gui.spec before_deploy: - git config --local user.name "Travis" From 9b52cfb013ccc774e21288adb4c85dd86f0a5796 Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 10 May 2020 17:30:52 +0500 Subject: [PATCH 1837/1846] Fix insufficient balance error Case happens when base or quote balance is not enough to place minimum allowed order when placing lowest buy or highest sell order. Closes: #765 --- dexbot/strategies/staggered_orders.py | 16 ++++++-- .../test_staggered_orders_highlevel.py | 38 +++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index bf12dca6e..472a10543 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1932,8 +1932,12 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente # Make sure new order is bigger than allowed minimum corrected_amount = self.check_min_order_size(amount_quote, price) if corrected_amount > amount_quote: - self.log.warning('Placing increased order because calculated size is less than allowed minimum') - amount_quote = corrected_amount + if quote_balance >= corrected_amount: + self.log.warning('Placing increased order because calculated size is less than allowed minimum') + amount_quote = corrected_amount + else: + self.log.debug('Insufficient balance to place sell order') + return if sell_orders_count > self.operational_depth: order = self.place_virtual_sell_order(amount_quote, price) @@ -2062,8 +2066,12 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p # Make sure new order is bigger than allowed minimum corrected_amount = self.check_min_order_size(amount_quote, price) if corrected_amount > amount_quote: - self.log.warning('Placing increased order because calculated size is less than allowed minimum') - amount_quote = corrected_amount + if base_balance >= corrected_amount: + self.log.warning('Placing increased order because calculated size is less than allowed minimum') + amount_quote = corrected_amount + else: + self.log.debug('Insufficient balance to place buy order') + return if buy_orders_count > self.operational_depth: order = self.place_virtual_buy_order(amount_quote, price) diff --git a/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py b/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py index 00e6373ff..2786ac4e9 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_highlevel.py @@ -143,6 +143,25 @@ def test_place_lowest_buy_order(worker2): assert worker.buy_orders[-1]['price'] < worker.lower_bound * (1 + worker.increment * 2) +def test_place_lowest_buy_order_corrected_amount(worker, monkeypatch): + """Test if worker handles situation when avail balance is not enough to place minimal allowed order + https://github.com/Codaone/DEXBot/issues/765.""" + + def mock(amount, price): + return max(amount * 1.01, 1) + + worker.refresh_balances() + monkeypatch.setattr(worker, 'check_min_order_size', mock) + order = worker.place_lowest_buy_order(worker.base_balance) + assert order + + worker.refresh_balances() + worker.bitshares.reserve(worker.base_balance, account=worker.account) + worker.base_balance['amount'] = 0 + worker.place_lowest_buy_order(worker.base_balance) + assert worker.disabled is False + + def test_place_highest_sell_order(worker2): """Check if placement of highest sell order works in general.""" worker = worker2 @@ -154,6 +173,25 @@ def test_place_highest_sell_order(worker2): assert worker.sell_orders[-1]['price'] ** -1 > worker.upper_bound / (1 + worker.increment * 2) +def test_place_highest_sell_order_corrected_amount(worker, monkeypatch): + """Test if worker handles situation when avail balance is not enough to place minimal allowed order + https://github.com/Codaone/DEXBot/issues/765.""" + + def mock(amount, price): + return max(amount * 1.01, 1) + + worker.refresh_balances() + monkeypatch.setattr(worker, 'check_min_order_size', mock) + order = worker.place_highest_sell_order(worker.quote_balance) + assert order + + worker.refresh_balances() + worker.bitshares.reserve(worker.quote_balance, account=worker.account) + worker.quote_balance['amount'] = 0 + worker.place_highest_sell_order(worker.quote_balance) + assert worker.disabled is False + + @pytest.mark.parametrize('asset', ['base', 'quote']) def test_place_closer_order_real_or_virtual(orders5, asset): """ From d13a917ac5077ce7e56d5771e4e6b99d43b1587e Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 10 May 2020 22:29:54 +0500 Subject: [PATCH 1838/1846] Handle null trx['operation_results'] in SO Closes: #764 --- dexbot/strategies/staggered_orders.py | 6 +++++- .../test_staggered_orders_complex.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index bf12dca6e..a5da5454e 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -208,7 +208,11 @@ def maintain_strategy(self, *args, **kwargs): return else: raise - order_ids = [result[1] for result in trx['operation_results']] + try: + order_ids = [result[1] for result in trx['operation_results']] + except TypeError: + # For some reason 'operation_results' may be None, this should not fail us + order_ids = [] self.log.debug('Placed orders: %s', order_ids) self.refresh_orders() self.sync_current_orders() diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index 34a82dea8..522387ae5 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -41,6 +41,18 @@ def test_maintain_strategy_no_manual_cp_empty_market(worker): assert worker.market_center_price is None +def test_maintain_strategy_no_operation_results(worker, monkeypatch): + """https://github.com/Codaone/DEXBot/issues/764.""" + + def mock(*args, **kwargs): + return {'operation_results': None} + + monkeypatch.setattr(worker, 'retry_action', mock) + worker.maintain_strategy() + # Run twice! + worker.maintain_strategy() + + @pytest.mark.parametrize('mode', MODES) def test_maintain_strategy_basic(mode, worker, do_initial_allocation): """Check if intial orders placement is correct.""" From c240fde762e3b190cf90335deaa49b34bc8f438b Mon Sep 17 00:00:00 2001 From: Vladimir Kamarzin Date: Sun, 10 May 2020 22:30:47 +0500 Subject: [PATCH 1839/1846] Apply docformatter fix --- .../staggered_orders/test_staggered_orders_complex.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/strategies/staggered_orders/test_staggered_orders_complex.py b/tests/strategies/staggered_orders/test_staggered_orders_complex.py index 522387ae5..16631af14 100644 --- a/tests/strategies/staggered_orders/test_staggered_orders_complex.py +++ b/tests/strategies/staggered_orders/test_staggered_orders_complex.py @@ -1193,12 +1193,13 @@ def test_allocate_asset_partially_filled_order_on_massively_imbalanced_sides( def test_allocate_asset_several_filled_orders_on_massively_imbalanced_sides( mode, worker, other_worker, do_initial_allocation, base_account ): - """ When sides are massively imbalanced, make sure that spread will be closed after filling several orders on - smaller side. The goal is to test a situation when one side has a big-sized orders, and other side has much - smaller orders. Correct behavior: when multiple orders on smaller side filled at once, big side should place - appropriate number of closer orders to close the spread. + """ + When sides are massively imbalanced, make sure that spread will be closed after filling several orders on smaller + side. The goal is to test a situation when one side has a big-sized orders, and other side has much smaller orders. + Correct behavior: when multiple orders on smaller side filled at once, big side should place appropriate number of + closer orders to close the spread. - Test for https://github.com/Codaone/DEXBot/issues/601 + Test for https://github.com/Codaone/DEXBot/issues/601 """ worker.mode = mode do_initial_allocation(worker, worker.mode) From b6ebec0a216c40c92dbb8d55dd0df14c1dd30f06 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 19 May 2020 13:55:22 +0300 Subject: [PATCH 1840/1846] Remove deprecated configurations from .travis.yml sudo is deprecated and simply has no effect anymore. skip_cleanup is deprecated and replaced with cleanup, which is false by default. --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5c445cd0b..9012a9c8e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,3 @@ -sudo: true services: - docker matrix: @@ -38,7 +37,6 @@ before_deploy: - tar -czvf dist/DEXBot-gui-$TRAVIS_OS_NAME-$TRAVIS_TAG.tar.gz dist/$TRAVIS_OS_NAME/DEXBot-gui 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 From a5e4be6cab2a68c320ed7b8926bf972d0c40e75b Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 19 May 2020 16:06:19 +0300 Subject: [PATCH 1841/1846] Change dexbot version number to 0.28.1 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index ca2aa86df..afb279ff9 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.28.0' +VERSION = '0.28.1' AUTHOR = 'Codaone Oy' __version__ = VERSION From 4307f979c0f987e6f389ff23ba7be25f2f9dddb9 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 19 May 2020 16:09:51 +0300 Subject: [PATCH 1842/1846] Change dexbot version number to 0.29.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index afb279ff9..ef0865c49 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.28.1' +VERSION = '0.29.0' AUTHOR = 'Codaone Oy' __version__ = VERSION From e88844b83a4191a635b1c2c33c350d5a93ed1144 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 19 May 2020 16:15:57 +0300 Subject: [PATCH 1843/1846] Change dexbot version number to 0.29.1 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index ef0865c49..b5356b9d6 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.29.0' +VERSION = '0.29.1' AUTHOR = 'Codaone Oy' __version__ = VERSION From 0fd298da69da41306d14245c993b2ea3f39a0681 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 19 May 2020 16:22:19 +0300 Subject: [PATCH 1844/1846] Change dexbot version number to 0.29.2 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index b5356b9d6..b9658868b 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.29.1' +VERSION = '0.29.2' AUTHOR = 'Codaone Oy' __version__ = VERSION From e58fa3fb4634fa8ce84a663cbdd7884f2846a02a Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Tue, 19 May 2020 16:23:20 +0300 Subject: [PATCH 1845/1846] Change dexbot version number to 1.0.0 --- dexbot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/__init__.py b/dexbot/__init__.py index b9658868b..74858de1c 100644 --- a/dexbot/__init__.py +++ b/dexbot/__init__.py @@ -1,4 +1,4 @@ APP_NAME = 'dexbot' -VERSION = '0.29.2' +VERSION = '1.0.0' AUTHOR = 'Codaone Oy' __version__ = VERSION From 44588f0cd7beb5f50508d966054d5f929818bf76 Mon Sep 17 00:00:00 2001 From: Joel Vainikka Date: Sat, 23 May 2020 18:48:29 +0300 Subject: [PATCH 1846/1846] Change .travis.yml Reverted back some settings --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 9012a9c8e..5c445cd0b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +sudo: true services: - docker matrix: @@ -37,6 +38,7 @@ before_deploy: - tar -czvf dist/DEXBot-gui-$TRAVIS_OS_NAME-$TRAVIS_TAG.tar.gz dist/$TRAVIS_OS_NAME/DEXBot-gui 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