diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 7ebfb7420..725f7ddaa 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -3,5 +3,3 @@ name: Feature request about: Suggest an idea for this project --- - - diff --git a/.gitignore b/.gitignore index 28f506e8a..828c12a91 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,6 @@ archive # Bandit config !bandit.yml + +# pre-commit hooks +!.pre-commit-config.yaml diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 000000000..a7fcfaf1e --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,4 @@ +[settings] +line_length = 120 +multi_line_output = 3 +include_trailing_comma = True diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..16b527296 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +# Read up on pre-commit +# https://ljvmiranda921.github.io/notebook/2018/06/21/precommits-using-black-and-flake8/ + +repos: + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.4.0 + hooks: + - id: trailing-whitespace + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-json + - id: check-yaml + - id: end-of-file-fixer + - id: no-commit-to-branch + - id: flake8 + +- repo: https://github.com/timothycrosley/isort + rev: 4.3.21 + hooks: + - id: isort + +- repo: https://github.com/ambv/black + rev: 19.10b0 + hooks: + - id: black + language_version: python3 diff --git a/MANIFEST.in b/MANIFEST.in index d0d04a220..8ff719a92 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ include *.md -include dexbot/resources/img/* \ No newline at end of file +include dexbot/resources/img/* diff --git a/README.md b/README.md index e58429a34..b7b4087d5 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,13 @@ Join the [Telegram Chat for DEXBot](https://t.me/DEXBOTbts). Install the software, use it and report any problems by creating a ticket. +Before commiting any changes first time, make sure to install pre-commit hooks! + +``` +pip install -r requirements-dev.txt +pre-commit install +``` + * [New Contributors Guide](https://github.com/Codaone/DEXBot/wiki/New-Contributors-Guide) * [Git Workflow](https://github.com/Codaone/DEXBot/wiki/Git-Workflow) diff --git a/dexbot/cli.py b/dexbot/cli.py index d17d59f0a..2b3130d27 100755 --- a/dexbot/cli.py +++ b/dexbot/cli.py @@ -4,42 +4,31 @@ import os.path import signal import sys +from multiprocessing import freeze_support -from uptick.decorators import online import bitshares.exceptions +import click # noqa: E402 import graphenecommon.exceptions from bitshares.market import Market - -from dexbot.config import Config, DEFAULT_CONFIG_FILE 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, - chain, - unlock, - configfile, - reset_nodes -) +from dexbot.config import DEFAULT_CONFIG_FILE, Config +from dexbot.helper import initialize_data_folders, initialize_orders_log +from dexbot.ui import chain, configfile, reset_nodes, unlock, verbose +from uptick.decorators import online -from .worker import WorkerInfrastructure +from . import errors, helper from .cli_conf import configure_dexbot, dexbot_service_running -from . import errors -from . import helper -from multiprocessing import freeze_support +from .worker import WorkerInfrastructure # We need to do this before importing click if "LANG" not in os.environ: os.environ['LANG'] = 'C.UTF-8' -import click # noqa: E402 log = logging.getLogger(__name__) # Initial logging before proper setup. -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s %(levelname)s %(message)s' -) +logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s') # Configure orders logging initialize_orders_log() @@ -50,39 +39,18 @@ @click.group() @click.option( - "--configfile", - default=DEFAULT_CONFIG_FILE, + "--configfile", default=DEFAULT_CONFIG_FILE, ) @click.option( '--logfile', default=None, type=click.Path(dir_okay=False, writable=True), - help='Override logfile location (example: ~/dexbot.log)' -) -@click.option( - '--verbose', - '-v', - type=int, - default=3, - help='Verbosity (0-15)') -@click.option( - '--systemd/--no-systemd', - '-d', - default=False, - help='Run as a daemon from systemd') -@click.option( - '--pidfile', - '-p', - type=click.Path(dir_okay=False, writable=True), - default='', - help='File to write PID') -@click.option( - '--sortnodes', - '-s', - type=int, - default=-1, - help='Sort nodes, w/max timeout in sec. [sec > 0]' + help='Override logfile location (example: ~/dexbot.log)', ) +@click.option('--verbose', '-v', type=int, default=3, help='Verbosity (0-15)') +@click.option('--systemd/--no-systemd', '-d', default=False, help='Run as a daemon from systemd') +@click.option('--pidfile', '-p', type=click.Path(dir_okay=False, writable=True), default='', help='File to write PID') +@click.option('--sortnodes', '-s', type=int, default=-1, help='Sort nodes, w/max timeout in sec. [sec > 0]') @click.pass_context def main(ctx, **kwargs): ctx.obj = {} @@ -131,6 +99,7 @@ def run(ctx): 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: @@ -203,9 +172,7 @@ def cancel(ctx, market, account): try: my_market = Market(market) ctx.bitshares.bundle = True - my_market.cancel([ - x["id"] for x in my_market.accountopenorders(account) - ], account=account) + my_market.cancel([x["id"] for x in my_market.accountopenorders(account)], account=account) response = ctx.bitshares.txbuffer.broadcast() log.info(response) if response is not None: diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index b6421cb3d..23fa8818d 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -14,33 +14,24 @@ """ import importlib -import pathlib import os import os.path -import sys +import pathlib import re import subprocess +import sys +import dexbot.helper from bitshares.account import Account - -from dexbot.whiptail import get_whiptail -from dexbot.strategies.base import StrategyBase from dexbot.config_validator import ConfigValidator from dexbot.node_manager import get_sorted_nodelist - -import dexbot.helper - +from dexbot.strategies.base import StrategyBase +from dexbot.whiptail import get_whiptail STRATEGIES = [ - {'tag': 'relative', - 'class': 'dexbot.strategies.relative_orders', - 'name': 'Relative Orders'}, - {'tag': 'stagger', - 'class': 'dexbot.strategies.staggered_orders', - 'name': 'Staggered Orders'}, - {'tag': 'koth', - 'class': 'dexbot.strategies.king_of_the_hill', - 'name': 'King of the Hill'}, + {'tag': 'relative', 'class': 'dexbot.strategies.relative_orders', 'name': 'Relative Orders'}, + {'tag': 'stagger', 'class': 'dexbot.strategies.staggered_orders', 'name': 'Staggered Orders'}, + {'tag': 'koth', 'class': 'dexbot.strategies.king_of_the_hill', 'name': 'King of the Hill'}, ] # Todo: tags must be unique. Are they really a tags? @@ -50,13 +41,12 @@ # make sure tag is unique i = 1 while tag in tags_so_far: - tag = tag+str(i) + 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") +SYSTEMD_SERVICE_NAME = os.path.expanduser("~/.local/share/systemd/user/dexbot.service") SYSTEMD_SERVICE_FILE = """ [Unit] @@ -78,8 +68,7 @@ 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(element, whiptail, worker_config): @@ -99,9 +88,7 @@ def process_config_element(element, whiptail, worker_config): if element.extra: while not re.match(element.extra, txt): whiptail.alert("The value is not valid") - txt = whiptail.prompt( - title, worker_config.get( - element.key, element.default)) + txt = whiptail.prompt(title, worker_config.get(element.key, element.default)) worker_config[element.key] = txt if element.type == "bool": @@ -132,8 +119,9 @@ def process_config_element(element, whiptail, worker_config): worker_config[element.key] = val if element.type == "choice": - worker_config[element.key] = whiptail.radiolist(title, select_choice( - worker_config.get(element.key, element.default), element.extra)) + worker_config[element.key] = whiptail.radiolist( + title, select_choice(worker_config.get(element.key, element.default), element.extra) + ) def dexbot_service_running(): @@ -156,8 +144,7 @@ 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?", default="no"): + if not whiptail.confirm("Do you want to run dexbot as a background (daemon) process?", default="no"): config['systemd_status'] = 'disabled' return @@ -173,16 +160,13 @@ def setup_systemd(whiptail, config): "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) + 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("~"))) + 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') @@ -228,8 +212,7 @@ def configure_worker(whiptail, worker_config, bitshares_instance): # Strategy selection worker_config['module'] = whiptail.radiolist( - "Choose a worker strategy", - select_choice(default_strategy, strategy_list) + "Choose a worker strategy", select_choice(default_strategy, strategy_list) ) for strategy in STRATEGIES: @@ -237,10 +220,7 @@ def configure_worker(whiptail, worker_config, bitshares_instance): worker_config['module'] = strategy['class'] # Import the strategy class but we don't __init__ it here - strategy_class = getattr( - importlib.import_module(worker_config["module"]), - 'Strategy' - ) + strategy_class = getattr(importlib.import_module(worker_config["module"]), 'Strategy') # Check if strategy has changed and editing existing worker if editing and default_strategy != get_strategy_tag(worker_config['module']): @@ -276,7 +256,8 @@ def configure_worker(whiptail, worker_config, bitshares_instance): 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") + "You will have to check the worker code and add configuration values to config.yml if required" + ) return worker_config @@ -305,19 +286,22 @@ def configure_dexbot(config, ctx): while True: action = whiptail.menu( "You have an existing configuration.\nSelect an action:", - [('LIST', 'List your workers'), - ('NEW', 'Create a new worker'), - ('EDIT', 'Edit 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'), - ('SORT_NODES', 'By latency (uses default list)'), - ('DEL_NODE', 'Delete A Node'), - ('HELP', 'Where to get help'), - ('EXIT', 'Quit this application')]) + [ + ('LIST', 'List your workers'), + ('NEW', 'Create a new worker'), + ('EDIT', 'Edit 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'), + ('SORT_NODES', 'By latency (uses default list)'), + ('DEL_NODE', 'Delete A Node'), + ('HELP', 'Where to get help'), + ('EXIT', 'Quit this application'), + ], + ) my_workers = [(index, index) for index in workers] @@ -339,8 +323,9 @@ def configure_dexbot(config, ctx): elif action == 'EDIT': 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) + config['workers'][worker_name] = configure_worker( + whiptail, config['workers'][worker_name], bitshares_instance + ) else: whiptail.alert('No workers to edit.') elif action == 'DEL_WORKER': @@ -380,8 +365,8 @@ def configure_dexbot(config, ctx): elif action == 'NODES': choice = whiptail.node_radiolist( msg="Choose your preferred node", - items=select_choice(config['node'][0], - [(index, index) for index 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) config['node'].insert(0, choice) @@ -393,8 +378,8 @@ def configure_dexbot(config, ctx): elif action == 'DEL_NODE': choice = whiptail.node_radiolist( msg="Choose node to delete", - items=select_choice(config['node'][0], - [(index, index) for index in config['node']])) + items=select_choice(config['node'][0], [(index, index) for index in config['node']]), + ) config['node'].remove(choice) # delete node permanently from config setup_systemd(whiptail, config) diff --git a/dexbot/config.py b/dexbot/config.py index 338e66b42..fa54b8920 100644 --- a/dexbot/config.py +++ b/dexbot/config.py @@ -1,20 +1,17 @@ import os import pathlib +from collections import OrderedDict, defaultdict +import appdirs from dexbot import APP_NAME, AUTHOR from dexbot.node_manager import get_sorted_nodelist - - -import appdirs from ruamel import yaml -from collections import OrderedDict, defaultdict DEFAULT_CONFIG_DIR = appdirs.user_config_dir(APP_NAME, appauthor=AUTHOR) DEFAULT_CONFIG_FILE = os.path.join(DEFAULT_CONFIG_DIR, 'config.yml') 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 @@ -165,9 +162,7 @@ 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) + OrderedLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, construct_mapping) return yaml.load(stream, OrderedLoader) @staticmethod @@ -194,6 +189,7 @@ def assets_intersections(config): } } """ + def update_data(asset, operational_percent): if isinstance(data[account][asset]['sum_pct'], float): # Existing dict key @@ -210,8 +206,7 @@ def update_data(asset, operational_percent): 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)) + raise ValueError('Operational percent for asset {} is more than 100%'.format(asset)) def tree(): return defaultdict(tree) diff --git a/dexbot/config_validator.py b/dexbot/config_validator.py index 1d5fd1af7..c2dab4bf0 100644 --- a/dexbot/config_validator.py +++ b/dexbot/config_validator.py @@ -1,10 +1,9 @@ -from dexbot.config import Config - -from bitshares.instance import shared_bitshares_instance from bitshares.account import Account from bitshares.asset import Asset -from bitshares.exceptions import KeyAlreadyInStoreException, AccountDoesNotExistsException, AssetDoesNotExistsException +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: diff --git a/dexbot/controllers/main_controller.py b/dexbot/controllers/main_controller.py index 86f07ea2b..1eeb962b4 100644 --- a/dexbot/controllers/main_controller.py +++ b/dexbot/controllers/main_controller.py @@ -1,22 +1,20 @@ -import os import logging +import os import sys import time -from dexbot import VERSION, APP_NAME, AUTHOR -from dexbot.helper import initialize_orders_log, initialize_data_folders -from dexbot.worker import WorkerInfrastructure -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 from bitsharesapi.bitsharesnoderpc import BitSharesNodeRPC +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: - def __init__(self, config): self.bitshares_instance = None self.config = config @@ -26,7 +24,8 @@ def __init__(self, config): 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') + '%(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(filename) fh.setFormatter(formatter) @@ -35,8 +34,10 @@ def __init__(self, config): 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'}) + logger.info( + "DEXBot {} on python {} {}".format(VERSION, sys.version[:6], sys.platform), + extra={'worker_name': 'NONE', 'account': 'NONE', 'market': 'NONE'}, + ) # Configure orders logging initialize_orders_log() diff --git a/dexbot/controllers/settings_controller.py b/dexbot/controllers/settings_controller.py index 8ccc7baf3..3ca924109 100644 --- a/dexbot/controllers/settings_controller.py +++ b/dexbot/controllers/settings_controller.py @@ -1,11 +1,9 @@ from dexbot.config import Config - -from PyQt5.QtWidgets import QTreeWidgetItem from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QTreeWidgetItem class SettingsController: - def __init__(self, view): self.config = Config() self.view = view diff --git a/dexbot/controllers/strategy_controller.py b/dexbot/controllers/strategy_controller.py index f11513870..2bca7c685 100644 --- a/dexbot/controllers/strategy_controller.py +++ b/dexbot/controllers/strategy_controller.py @@ -66,7 +66,7 @@ def elements(self): QtWidgets.QLineEdit, QtWidgets.QCheckBox, QtWidgets.QComboBox, - QtWidgets.QSlider + QtWidgets.QSlider, ) for option in self.configure: @@ -78,7 +78,6 @@ def elements(self): class RelativeOrdersController(StrategyController): - def __init__(self, view, configure, worker_controller, worker_data): # Check if there is worker data. This prevents error when multiplying None type when creating worker. if worker_data: @@ -254,7 +253,6 @@ def validation_errors(self): class StaggeredOrdersController(StrategyController): - def __init__(self, view, configure, worker_controller, worker_data): self.view = view self.configure = configure @@ -298,7 +296,6 @@ def validation_errors(self): class KingOfTheHillController(StrategyController): - def __init__(self, view, configure, worker_controller, worker_data): self.view = view self.configure = configure diff --git a/dexbot/controllers/wallet_controller.py b/dexbot/controllers/wallet_controller.py index 3dec4ae7f..420274eec 100644 --- a/dexbot/controllers/wallet_controller.py +++ b/dexbot/controllers/wallet_controller.py @@ -2,7 +2,6 @@ class WalletController: - def __init__(self, bitshares_instance): self.bitshares = bitshares_instance diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index 8e33d3d2d..aff6fb13b 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -1,20 +1,18 @@ import collections import re -from dexbot.views.errors import gui_error +from bitshares.instance import shared_bitshares_instance 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.errors import gui_error +from dexbot.views.notice import NoticeDialog from dexbot.views.strategy_form import StrategyFormWidget - -from bitshares.instance import shared_bitshares_instance from PyQt5 import QtGui class WorkerController: - def __init__(self, view, bitshares_instance, mode): self.view = view self.mode = mode @@ -33,16 +31,10 @@ def strategies(self): 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': '' - } - strategies['dexbot.strategies.king_of_the_hill'] = { - 'name': 'King of the Hill', - 'form_module': '' + 'form_module': 'dexbot.views.ui.forms.relative_orders_widget_ui', } + strategies['dexbot.strategies.staggered_orders'] = {'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 @@ -96,8 +88,9 @@ def get_account(worker_data): @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?') + dialog = ConfirmationDialog( + 'Saving the worker will cancel all the current orders.\n' 'Are you sure you want to do this?' + ) return dialog.exec_() @gui_error @@ -125,8 +118,7 @@ def validate_form(self): old_worker_name = None if self.mode == 'add' else self.view.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)) + error_texts.append('Worker name needs to be unique. "{}" is already in use.'.format(worker_name)) if not self.validator.validate_asset(base_asset): error_texts.append('Field "Base Asset" does not have a valid asset.') if not self.validator.validate_asset(quote_asset): @@ -185,14 +177,13 @@ def handle_save(self): 'fee_asset': fee_asset, 'operational_percent_quote': operational_percent_quote, 'operational_percent_base': operational_percent_base, - **self.view.strategy_widget.values + **self.view.strategy_widget.values, } 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/controllers/worker_details_controller.py b/dexbot/controllers/worker_details_controller.py index f6bfadda2..1791a1488 100644 --- a/dexbot/controllers/worker_details_controller.py +++ b/dexbot/controllers/worker_details_controller.py @@ -1,12 +1,11 @@ import csv import os -from PyQt5.QtWidgets import QTableWidgetItem from PyQt5.QtGui import QTextCursor +from PyQt5.QtWidgets import QTableWidgetItem class WorkerDetailsController: - def __init__(self, view, worker_name, config): """ Initializes controller diff --git a/dexbot/errors.py b/dexbot/errors.py index 8cd19736e..f6b46203d 100644 --- a/dexbot/errors.py +++ b/dexbot/errors.py @@ -1,11 +1,10 @@ import logging + log = logging.getLogger(__name__) def InsufficientFundsError(amount): - log.error( - "[InsufficientFunds] Need {}".format(str(amount)) - ) + log.error("[InsufficientFunds] Need {}".format(str(amount))) class NoWorkersAvailable(Exception): diff --git a/dexbot/gui.py b/dexbot/gui.py index 900342b7a..7bc4fb1eb 100644 --- a/dexbot/gui.py +++ b/dexbot/gui.py @@ -3,7 +3,6 @@ from dexbot.config import Config from dexbot.controllers.main_controller import MainController from dexbot.views.worker_list import MainView - from PyQt5.QtWidgets import QApplication diff --git a/dexbot/helper.py b/dexbot/helper.py index 352616aa4..cc6827607 100644 --- a/dexbot/helper.py +++ b/dexbot/helper.py @@ -1,10 +1,10 @@ -import os -import math -import shutil import errno import logging -from appdirs import user_data_dir +import math +import os +import shutil +from appdirs import user_data_dir from dexbot import APP_NAME, AUTHOR @@ -94,6 +94,7 @@ def find_external_strategies(): 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(): diff --git a/dexbot/migrations/env.py b/dexbot/migrations/env.py index be82b9ecc..131e3422a 100644 --- a/dexbot/migrations/env.py +++ b/dexbot/migrations/env.py @@ -1,7 +1,5 @@ -from sqlalchemy import engine_from_config -from sqlalchemy import pool - from alembic import context +from sqlalchemy import engine_from_config, pool # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -36,9 +34,7 @@ def run_migrations_offline(): """ url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, target_metadata=target_metadata, literal_binds=True - ) + context.configure(url=url, target_metadata=target_metadata, literal_binds=True) with context.begin_transaction(): context.run_migrations() @@ -52,15 +48,11 @@ def run_migrations_online(): """ connectable = engine_from_config( - config.get_section(config.config_ini_section), - prefix="sqlalchemy.", - poolclass=pool.NullPool, + config.get_section(config.config_ini_section), prefix="sqlalchemy.", poolclass=pool.NullPool, ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() diff --git a/dexbot/migrations/versions/d1e6672520b2_extend_orders_table.py b/dexbot/migrations/versions/d1e6672520b2_extend_orders_table.py index 732d5bfb1..84339fa32 100644 --- a/dexbot/migrations/versions/d1e6672520b2_extend_orders_table.py +++ b/dexbot/migrations/versions/d1e6672520b2_extend_orders_table.py @@ -5,9 +5,8 @@ Create Date: 2019-07-29 17:38:09.136485 """ -from alembic import op import sqlalchemy as sa - +from alembic import op # revision identifiers, used by Alembic. revision = 'd1e6672520b2' diff --git a/dexbot/node_manager.py b/dexbot/node_manager.py index 7ba64d849..6bbdc05b0 100644 --- a/dexbot/node_manager.py +++ b/dexbot/node_manager.py @@ -1,18 +1,15 @@ -from websocket import create_connection as wss_create -from time import time -from itertools import repeat import logging import multiprocessing as mp -import subprocess import platform +import subprocess +from itertools import repeat +from time import time +from websocket import create_connection as wss_create log = logging.getLogger(__name__) -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s %(levelname)s %(message)s' -) +logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s') max_timeout = 2.0 # default ping time is set to 2s. use for internal testing. host_ip = '8.8.8.8' # default host to ping to check internet @@ -52,7 +49,7 @@ def wss_test(node, timeout): try: start = time() wss_create(node, timeout=timeout) - latency = (time() - start) + latency = time() - start return latency except Exception as e: log.info('websocket test: {}'.format(e)) @@ -74,7 +71,7 @@ def get_sorted_nodelist(nodelist, timeout): """ print('get_sorted_nodelist max timeout: {}'.format(timeout)) - pool_size = mp.cpu_count()*2 + pool_size = mp.cpu_count() * 2 with mp.Pool(processes=pool_size) as pool: latency_info = pool.starmap(check_node, zip(nodelist, repeat(timeout))) diff --git a/dexbot/orderengines/bitshares_engine.py b/dexbot/orderengines/bitshares_engine.py index 38b36421a..133622a02 100644 --- a/dexbot/orderengines/bitshares_engine.py +++ b/dexbot/orderengines/bitshares_engine.py @@ -1,26 +1,22 @@ -import datetime import copy +import datetime import logging import time -from dexbot.config import Config -from dexbot.storage import Storage -from dexbot.helper import truncate - import bitshares.exceptions import bitsharesapi import bitsharesapi.exceptions - 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 - +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 @@ -39,16 +35,18 @@ class BitsharesOrderEngine(Storage, Events): """ - def __init__(self, - name, - config=None, - _account=None, - _market=None, - fee_asset_symbol=None, - bitshares_instance=None, - bitshares_bundle=None, - *args, - **kwargs): + def __init__( + self, + name, + config=None, + _account=None, + _market=None, + fee_asset_symbol=None, + bitshares_instance=None, + bitshares_bundle=None, + *args, + **kwargs + ): # BitShares instance self.bitshares = bitshares_instance or shared_bitshares_instance() @@ -113,10 +111,7 @@ def _callbackPlaceFillOrders(self, d): def _cancel_orders(self, orders): try: - self.retry_action( - self.bitshares.cancel, - orders, account=self._account, fee_asset=self.fee_asset['id'] - ) + 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 @@ -157,9 +152,7 @@ def account_total_value(self, return_asset): total_value += updated_order['base']['amount'] else: total_value += self.convert_asset( - updated_order['base']['amount'], - updated_order['base']['symbol'], - return_asset + updated_order['base']['amount'], updated_order['base']['symbol'], return_asset ) return total_value @@ -233,8 +226,11 @@ def cancel_all_orders(self, all_markets=False): 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'])) + self.log.info( + 'Canceling all orders on market {}/{}'.format( + self.market['quote']['symbol'], self.market['base']['symbol'] + ) + ) orders_to_cancel = self.own_orders if orders_to_cancel: @@ -544,8 +540,9 @@ 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 with {:.{prec}f} {} @ {:.8f}' - .format(base_amount, symbol, price, prec=precision)) + 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( @@ -600,8 +597,9 @@ def place_market_sell_order(self, amount, price, return_none=False, invert=False self.disabled = True return None - self.log.info('Placing a sell order with {:.{prec}f} {} @ {:.8f}' - .format(quote_amount, symbol, price, prec=precision)) + 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( @@ -662,13 +660,17 @@ def retry_action(self, action, *args, **kwargs): 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()))) + 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.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): @@ -845,7 +847,10 @@ def is_partially_filled(self, order, threshold=0.3): 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)) + 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/pricefeeds/bitshares_feed.py b/dexbot/pricefeeds/bitshares_feed.py index 2e273cdaf..42e63c0b4 100644 --- a/dexbot/pricefeeds/bitshares_feed.py +++ b/dexbot/pricefeeds/bitshares_feed.py @@ -15,9 +15,8 @@ class BitsharesPriceFeed: - Buy orders reserve BASE - Sell orders reserve QUOTE """ - def __init__(self, - market, - bitshares_instance=None): + + def __init__(self, market, bitshares_instance=None): self.market = market self.ticker = self.market.ticker @@ -28,9 +27,7 @@ def __init__(self, # BitShares instance self.bitshares = bitshares_instance or shared_bitshares_instance() - self.log = logging.LoggerAdapter( - logging.getLogger('dexbot.pricefeed_log'), {} - ) + 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. diff --git a/dexbot/qt_queue/idle_queue.py b/dexbot/qt_queue/idle_queue.py index 1dd980d84..d0a16ca62 100644 --- a/dexbot/qt_queue/idle_queue.py +++ b/dexbot/qt_queue/idle_queue.py @@ -6,4 +6,5 @@ def idle_add(func, *args, **kwargs): def idle(): func(*args, **kwargs) + idle_loop.put(idle) diff --git a/dexbot/qt_queue/queue_dispatcher.py b/dexbot/qt_queue/queue_dispatcher.py index e8c972797..843b701b1 100644 --- a/dexbot/qt_queue/queue_dispatcher.py +++ b/dexbot/qt_queue/queue_dispatcher.py @@ -1,7 +1,6 @@ -from PyQt5.QtWidgets import QApplication -from PyQt5.QtCore import QThread, QEvent - from dexbot.qt_queue.idle_queue import idle_loop +from PyQt5.QtCore import QEvent, QThread +from PyQt5.QtWidgets import QApplication class ThreadDispatcher(QThread): diff --git a/dexbot/resources/svg/dexbot-logo.svg b/dexbot/resources/svg/dexbot-logo.svg index 6c1cc14d0..84444153d 100644 --- a/dexbot/resources/svg/dexbot-logo.svg +++ b/dexbot/resources/svg/dexbot-logo.svg @@ -142,4 +142,4 @@ id="tspan108" y="0" x="0 23.390959 46.781918 70.172874 93.563835 116.9548 140.34575 163.73671 187.12767 210.51863 233.90959 257.30054 280.6915 304.08246 327.47342 350.86438 374.25534">MARKET MAKING BOT - \ No newline at end of file + diff --git a/dexbot/resources/svg/dexbot.svg b/dexbot/resources/svg/dexbot.svg index e011220cf..dfd8607a2 100644 --- a/dexbot/resources/svg/dexbot.svg +++ b/dexbot/resources/svg/dexbot.svg @@ -1 +1 @@ -Asset 2dexbot \ No newline at end of file +Asset 2dexbot diff --git a/dexbot/resources/svg/modifystrategy.svg b/dexbot/resources/svg/modifystrategy.svg index 7e30ac73d..3342fa265 100644 --- a/dexbot/resources/svg/modifystrategy.svg +++ b/dexbot/resources/svg/modifystrategy.svg @@ -1 +1 @@ -Asset 1 \ No newline at end of file +Asset 1 diff --git a/dexbot/resources/svg/simplestrategy.svg b/dexbot/resources/svg/simplestrategy.svg index 0a1819df2..17470e4d3 100644 --- a/dexbot/resources/svg/simplestrategy.svg +++ b/dexbot/resources/svg/simplestrategy.svg @@ -1 +1 @@ -Asset 1 \ No newline at end of file +Asset 1 diff --git a/dexbot/storage.py b/dexbot/storage.py index 4072a3fa1..4b76152c0 100644 --- a/dexbot/storage.py +++ b/dexbot/storage.py @@ -1,22 +1,20 @@ +import json import os import os.path +import queue import sys -import json import threading -import queue import uuid + import alembic import alembic.config - from appdirs import user_data_dir - -from . import helper from dexbot import APP_NAME, AUTHOR - -from sqlalchemy import create_engine, Column, String, Integer, Float, Boolean +from sqlalchemy import Boolean, Column, Float, Integer, String, create_engine from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker, load_only +from sqlalchemy.orm import load_only, sessionmaker +from . import helper Base = declarative_base() @@ -227,6 +225,7 @@ def __init__(self, **kwargs): migrations_dir = os.path.join(bundle_dir, 'migrations') else: from pkg_resources import resource_filename + migrations_dir = resource_filename('dexbot', 'migrations') if os.path.exists(sqlite_file) and os.path.getsize(sqlite_file) > 0: diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 13bbff7a5..0dc829059 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -2,20 +2,17 @@ import math import time -from dexbot.config import Config -from dexbot.storage import Storage -from dexbot.qt_queue.idle_queue import idle_add -from dexbot.strategies.config_parts.base_config import BaseConfig - -from dexbot.orderengines.bitshares_engine import BitsharesOrderEngine -from dexbot.pricefeeds.bitshares_feed import BitsharesPriceFeed - import bitshares.exceptions -from bitshares.instance import shared_bitshares_instance -from bitshares.amount import Asset 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 @@ -91,18 +88,20 @@ def configure_details(cls, include_default_tabs=True): 'error_ontick', ] - def __init__(self, - name, - config=None, - onAccount=None, - onOrderMatched=None, - onOrderPlaced=None, - onMarketUpdate=None, - onUpdateCallOrder=None, - ontick=None, - bitshares_instance=None, - *args, - **kwargs): + 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() @@ -190,8 +189,8 @@ def __init__(self, 'worker_name': name, 'account': self.worker['account'], 'market': self.worker['market'], - 'is_disabled': lambda: self.disabled - } + 'is_disabled': lambda: self.disabled, + }, ) self.orders_log = logging.LoggerAdapter(logging.getLogger('dexbot.orders_log'), {}) @@ -256,16 +255,18 @@ def store_profit_estimation_data(self): return None timestamp = time.time() - self.store_balance_entry(account, self.worker_name, base_amount, base_symbol, - quote_amount, quote_symbol, center_price, timestamp) + 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) + 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 @@ -276,8 +277,13 @@ def calc_profit(self): 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) + 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 diff --git a/dexbot/strategies/config_parts/base_config.py b/dexbot/strategies/config_parts/base_config.py index 123ecf3c6..686f3fd05 100644 --- a/dexbot/strategies/config_parts/base_config.py +++ b/dexbot/strategies/config_parts/base_config.py @@ -47,7 +47,6 @@ class BaseConfig: - @classmethod def configure(cls, return_base_config=True): """ Return a list of ConfigElement objects defining the configuration values for this class. @@ -64,19 +63,34 @@ 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', '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', - 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, '%')), + ConfigElement('account', 'string', '', 'Account', 'BitShares account name for the bot to operate with', ''), + 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', 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: diff --git a/dexbot/strategies/config_parts/relative_config.py b/dexbot/strategies/config_parts/relative_config.py index 512ade064..f03cf3abc 100644 --- a/dexbot/strategies/config_parts/relative_config.py +++ b/dexbot/strategies/config_parts/relative_config.py @@ -2,7 +2,6 @@ class RelativeConfig(BaseConfig): - @classmethod def configure(cls, return_base_config=True): """ Return a list of ConfigElement objects defining the configuration values for this class. @@ -24,57 +23,165 @@ def configure(cls, return_base_config=True): ('kraken', 'Kraken'), ('bitfinex', 'Bitfinex'), ('gdax', 'Gdax'), - ('binance', 'Binance') + ('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', 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', - (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_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), - 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, '')) + 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][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, ''), + ), + 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_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, + ), + 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 BaseConfig.configure(return_base_config) + relative_orders_config diff --git a/dexbot/strategies/config_parts/staggered_config.py b/dexbot/strategies/config_parts/staggered_config.py index 6a97124a5..409d2d133 100644 --- a/dexbot/strategies/config_parts/staggered_config.py +++ b/dexbot/strategies/config_parts/staggered_config.py @@ -2,7 +2,6 @@ class StaggeredConfig(BaseConfig): - @classmethod def configure(cls, return_base_config=True): """ Modes description: @@ -30,40 +29,72 @@ def configure(cls, return_base_config=True): ('neutral', 'Neutral'), ('valley', 'Valley'), ('buy_slope', 'Buy Slope'), - ('sell_slope', 'Sell 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), + '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, '%')), + '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, '%')), + '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), + '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', + 'center_price', + 'float', + 0, + 'Manual center price', 'In an immature market, give a center price manually to begin with. BASE/QUOTE', - (0, 1000000000, 8, '')), + (0, 1000000000, 8, ''), + ), ConfigElement( - 'lower_bound', 'float', 1, 'Lower bound', + 'lower_bound', + 'float', + 1, + 'Lower bound', 'The lowest price (Quote/Base) in the range', - (0, 1000000000, 8, '')), + (0, 1000000000, 8, ''), + ), ConfigElement( - 'upper_bound', 'float', 1000000, 'Upper bound', + 'upper_bound', + 'float', + 1000000, + 'Upper bound', 'The highest price (Quote/Base) in the range', - (0, 1000000000, 8, '')), + (0, 1000000000, 8, ''), + ), ConfigElement( - 'instant_fill', 'bool', True, 'Allow instant fill', - 'Allow to execute orders by market', None), + '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)) + 'operational_depth', + 'int', + 10, + 'Operational depth', + 'Order depth to maintain on books', + (2, 9999999, None), + ), ] @classmethod diff --git a/dexbot/strategies/config_parts/strategy_config.py b/dexbot/strategies/config_parts/strategy_config.py index 5c961d3f7..cbf03eafa 100644 --- a/dexbot/strategies/config_parts/strategy_config.py +++ b/dexbot/strategies/config_parts/strategy_config.py @@ -17,12 +17,12 @@ def configure(cls, return_base_config=True): 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, '')), + 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 @@ -37,5 +37,5 @@ def configure_details(cls, include_default_tabs=True): 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') + DetailElement('text', 'Log', 'Log data', 'example.log'), ] diff --git a/dexbot/strategies/external_feeds/ccxt_feed.py b/dexbot/strategies/external_feeds/ccxt_feed.py index b3bcfe2c6..14009e4b1 100644 --- a/dexbot/strategies/external_feeds/ccxt_feed.py +++ b/dexbot/strategies/external_feeds/ccxt_feed.py @@ -22,14 +22,13 @@ async def fetch_ticker(exchange, symbol): try: ticker = await exchange.fetch_ticker(symbol.upper()) except Exception as exception: - print(type(exception).__name__, exception.args, - 'Exchange Error (ignoring)') + print(type(exception).__name__, exception.args, 'Exchange Error (ignoring)') except accxt.RequestTimeout as exception: - print(type(exception).__name__, exception.args, - 'Request Timeout (ignoring)') + 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)') + 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 21b9722ef..030950fff 100644 --- a/dexbot/strategies/external_feeds/gecko_feed.py +++ b/dexbot/strategies/external_feeds/gecko_feed.py @@ -1,7 +1,7 @@ -import requests import asyncio -from dexbot.strategies.external_feeds.process_pair import split_pair, debug +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. For base/quote one must be listed as ticker and the other as fullname, diff --git a/dexbot/strategies/external_feeds/price_feed.py b/dexbot/strategies/external_feeds/price_feed.py old mode 100755 new mode 100644 index 06fa4b49b..2f83853ea --- a/dexbot/strategies/external_feeds/price_feed.py +++ b/dexbot/strategies/external_feeds/price_feed.py @@ -2,9 +2,15 @@ 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.process_pair import ( + debug, + filter_bit_symbol, + filter_prefix_symbol, + get_consolidated_pair, + join_pair, + split_pair, +) 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 class PriceFeed: diff --git a/dexbot/strategies/external_feeds/waves_feed.py b/dexbot/strategies/external_feeds/waves_feed.py index 62bf0b9ed..91749d959 100644 --- a/dexbot/strategies/external_feeds/waves_feed.py +++ b/dexbot/strategies/external_feeds/waves_feed.py @@ -1,6 +1,7 @@ +import asyncio + import dexbot.strategies.external_feeds.process_pair import requests -import asyncio WAVES_URL = 'https://marketdata.wavesplatform.com/api/' SYMBOLS_URL = "/symbols" diff --git a/dexbot/strategies/king_of_the_hill.py b/dexbot/strategies/king_of_the_hill.py index eec01d719..9b4e6365a 100644 --- a/dexbot/strategies/king_of_the_hill.py +++ b/dexbot/strategies/king_of_the_hill.py @@ -1,6 +1,5 @@ # Python imports import copy - from datetime import datetime, timedelta from decimal import Decimal @@ -8,7 +7,6 @@ from dexbot.strategies.base import StrategyBase from dexbot.strategies.config_parts.koth_config import KothConfig - STRATEGY_NAME = 'King of the Hill' diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index cf7585d1c..797162e52 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -127,7 +127,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 @@ -142,8 +142,10 @@ def amount_to_sell(self): 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']): + if ( + amount < 2 * 10 ** -self.market['quote']['precision'] + or amount * self.sell_price < 2 * 10 ** -self.market['base']['precision'] + ): amount = 0 return amount @@ -158,8 +160,10 @@ def amount_to_buy(self): 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']): + if ( + amount < 2 * 10 ** -self.market['quote']['precision'] + or amount * self.buy_price < 2 * 10 ** -self.market['base']['precision'] + ): amount = 0 return amount @@ -211,19 +215,20 @@ def calculate_order_prices(self): self.log.warning('Failed to obtain last trade price') try: center_price = self.get_market_center_price() - self.log.info('Using market center price (failed to obtain last trade): {:.8f}' - .format(center_price)) + self.log.info( + 'Using market center price (failed to obtain last trade): {:.8f}'.format(center_price) + ) except TypeError: self.log.warning('Failed to obtain center price from market') elif self.center_price_depth > 0: # Calculate with quote amount if given center_price = self.get_market_center_price(quote_amount=self.center_price_depth) try: - self.log.info('Using market center price: {:.8f} with depth: {:.{prec}f}'.format( - center_price, - self.center_price_depth, - prec=self.market['quote']['precision'] - )) + self.log.info( + 'Using market center price: {:.8f} with depth: {:.{prec}f}'.format( + center_price, self.center_price_depth, prec=self.market['quote']['precision'] + ) + ) except TypeError: self.log.warning('Failed to obtain depthted center price') else: @@ -234,20 +239,12 @@ def calculate_order_prices(self): self.log.warning('Failed to obtain center price from market') self.center_price = self.calculate_center_price( - center_price, - self.is_asset_offset, - spread, - self['order_ids'], - self.manual_offset + center_price, self.is_asset_offset, spread, self['order_ids'], self.manual_offset ) else: # 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, - spread, - self['order_ids'], - self.manual_offset + self.center_price, self.is_asset_offset, spread, self['order_ids'], self.manual_offset ) try: @@ -461,24 +458,21 @@ def _calculate_center_price(self, suppress_errors=False): 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.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.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): + 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: @@ -605,11 +599,7 @@ def check_orders(self, *args, **kwargs): spread = self.get_market_spread(quote_amount=self.market_depth_amount) * self.dynamic_spread_factor center_price = self.calculate_center_price( - None, - self.is_asset_offset, - spread, - self['order_ids'], - self.manual_offset + None, self.is_asset_offset, spread, self['order_ids'], self.manual_offset ) diff = abs((self.center_price - center_price) / self.center_price) if diff >= self.price_change_threshold: diff --git a/dexbot/strategies/staggered_orders.py b/dexbot/strategies/staggered_orders.py index 85275bea2..fd9864074 100644 --- a/dexbot/strategies/staggered_orders.py +++ b/dexbot/strategies/staggered_orders.py @@ -1,12 +1,12 @@ -import time import math +import time import uuid -import bitsharesapi.exceptions from datetime import datetime, timedelta from functools import reduce -from bitshares.dex import Dex -from bitshares.amount import Amount +import bitsharesapi.exceptions +from bitshares.amount import Amount +from bitshares.dex import Dex from dexbot.strategies.base import StrategyBase from dexbot.strategies.config_parts.staggered_config import StaggeredConfig @@ -57,8 +57,10 @@ def __init__(self, *args, **kwargs): 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.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: @@ -226,19 +228,24 @@ 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]): + 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] + ): # 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.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])): + 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] + ): # 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.log.debug( + 'Reducing check interval to {} seconds because of changed ' 'balances'.format(self.min_check_interval) + ) self.current_check_interval = self.min_check_interval if previous_bootstrap_state is True and self['bootstrapping'] is False: @@ -246,10 +253,12 @@ def maintain_strategy(self, *args, **kwargs): self.dump_initial_orders() # 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] or - trx_executed): + 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] + or trx_executed + ): self.last_check = datetime.now() self.log_maintenance_time() return @@ -278,8 +287,10 @@ def maintain_strategy(self, *args, **kwargs): 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.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() @@ -350,14 +361,24 @@ def refresh_balances(self, use_cached_orders=False): 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'])) + 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'])) + 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 @@ -382,9 +403,7 @@ 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_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 @@ -499,6 +518,7 @@ def restore_virtual_orders(self): 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(): furthest_order = self.real_buy_orders[-1] while furthest_order['price'] > self.lower_bound * (1 + self.increment): @@ -686,8 +706,16 @@ def store_profit_estimation_data(self, force=False): 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) + 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 @@ -763,13 +791,17 @@ def allocate_asset(self, asset, asset_balance): self.replace_partially_filled_order(closest_own_order) return - 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] and - opposite_orders): + 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] + 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') + 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 """ Note: because we're using operations batching, there is possible a situation when we will have @@ -782,8 +814,11 @@ 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)) + 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) elif opposite_orders and self.actual_spread - self.increment < self.target_spread + self.increment: @@ -796,30 +831,49 @@ 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: {:.{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')): + 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: {:.{prec}f} {}' - .format(order_type, own_asset_limit, own_symbol, prec=own_precision)) + 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) + 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 == 'buy_slope' and asset == 'quote') or - (self.mode == 'sell_slope' and asset == 'base')): + 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: {:.{prec}f} {}'.format( - order_type, opposite_asset_limit, opposite_symbol, prec=opposite_precision)) + 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) + self.place_closer_order( + asset, + closest_own_order, + own_asset_limit=own_asset_limit, + 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) @@ -838,8 +892,10 @@ def allocate_asset(self, asset, asset_balance): 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: ' - '{:.{prec}f} {}'.format(funds_to_reserve, own_symbol, prec=own_precision)) + 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): @@ -855,17 +911,17 @@ def allocate_asset(self, asset, asset_balance): 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: ' - '{:.{prec}f} {}'.format(order_type, funds_to_reserve, own_symbol, - prec=own_precision)) + 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) + ) 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 * - (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)) @@ -878,8 +934,11 @@ def allocate_asset(self, asset, asset_balance): else: increase_status = 'done' - if (increase_status == 'done' and not self.check_partial_fill(closest_own_order) - and not self.check_partial_fill(closest_opposite_order, fill_threshold=0)): + if ( + increase_status == 'done' + 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 @@ -895,8 +954,9 @@ 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_status == 'done' and not self.check_partial_fill(closest_opposite_order, fill_threshold=( - 1 - self.partial_fill_threshold))): + elif increase_status == 'done' and not self.check_partial_fill( + closest_opposite_order, fill_threshold=(1 - self.partial_fill_threshold) + ): # 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. @@ -1431,8 +1491,11 @@ def check_partial_fill(self, order, fill_threshold=None): 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_type, order['base']['amount'], order['base']['symbol'], 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 @@ -1465,12 +1528,15 @@ def replace_partially_filled_order(self, order): 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} {}' - .format(order_type, asset_balance['amount'], needed, order['base']['symbol'], - prec=precision)) + 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): + 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: @@ -1530,14 +1596,18 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False # 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')): + 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')): + 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': @@ -1566,17 +1636,25 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False if balance < limiter: if allow_partial or ( # Accept small inaccuracy for full-sized closer order - place_order and not allow_partial and limiter - balance < 20 * 10 ** -precision + place_order + and not allow_partial + and limiter - balance < 20 * 10 ** -precision ): - self.log.debug('Limiting {} order amount to available asset balance: {:.{prec}f} {}' - .format(order_type, balance, symbol, prec=precision)) + 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': quote_amount = balance elif place_order and not allow_partial: - self.log.debug('Not enough balance to place closer {} order; need/avail: {:.{prec}f}/{:.{prec}f}' - .format(order_type, limiter, balance, prec=precision)) + 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 # Make sure new order is bigger than allowed minimum @@ -1592,8 +1670,11 @@ def place_closer_order(self, asset, order, place_order=True, allow_partial=False elif asset == 'quote': hard_limit = quote_amount if balance < hard_limit: - self.log.debug('Not enough balance to place minimal allowed order: {:.{prec}f}/{:.{prec}f} {}' - .format(balance, hard_limit, symbol, prec=precision)) + self.log.debug( + 'Not enough balance to place minimal allowed order: {:.{prec}f}/{:.{prec}f} {}'.format( + balance, hard_limit, symbol, prec=precision + ) + ) place_order = False if place_order and asset == 'base': @@ -1652,14 +1733,18 @@ 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' 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') + ): 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')): + 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': @@ -1680,12 +1765,18 @@ 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: {:.{prec}f}/{:.{prec}f}' - .format(order_type, limiter, balance, prec=precision)) + 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: - self.log.debug('Limiting {} order amount to available asset balance: {:.{prec}f} {}' - .format(order_type, balance, symbol, prec=precision)) + 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': @@ -1704,8 +1795,11 @@ def place_further_order(self, asset, order, place_order=True, allow_partial=Fals elif asset == 'quote': hard_limit = quote_amount if balance < hard_limit: - self.log.debug('Not enough balance to place minimal allowed order: {:.{prec}f}/{:.{prec}f} {}' - .format(balance, hard_limit, symbol, prec=precision)) + self.log.debug( + 'Not enough balance to place minimal allowed order: {:.{prec}f}/{:.{prec}f} {}'.format( + balance, hard_limit, symbol, prec=precision + ) + ) place_order = False if place_order and asset == 'base': @@ -1745,8 +1839,10 @@ 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: {:.8f}' - .format(market_center_price, price, self.upper_bound)) + 'price: {:.8f}, closest order price: {:.8f}, upper_bound: {:.8f}'.format( + market_center_price, price, self.upper_bound + ) + ) return sell_orders_count = self.calc_sell_orders_count(price, self.upper_bound) @@ -1754,8 +1850,9 @@ def place_highest_sell_order(self, quote_balance, place_order=True, market_cente 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) - real_orders_count = min(buy_orders_count, self.operational_depth) + min(sell_orders_count, - self.operational_depth) + 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 * real_orders_count @@ -1868,8 +1965,10 @@ 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}' - .format(market_center_price, price, self.lower_bound)) + '{:.8f}, closest order price: {:.8f}, lower bound: {:.8f}'.format( + market_center_price, price, self.lower_bound + ) + ) return buy_orders_count = self.calc_buy_orders_count(price, self.lower_bound) @@ -1877,8 +1976,9 @@ def place_lowest_buy_order(self, base_balance, place_order=True, market_center_p if self.fee_asset['id'] == self.market['base']['id']: fee = self.get_order_creation_fee(self.fee_asset) 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) + 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 * real_orders_count @@ -1985,10 +2085,12 @@ def check_min_order_size(self, amount, price): if not self.order_min_base or not self.order_min_quote: self.calculate_min_amounts() - 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)) + 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 @@ -2013,8 +2115,11 @@ def place_virtual_buy_order(self, amount, price): order['for_sale'] = base_asset order['price'] = precise_base_amount / precise_quote_amount - self.log.info('Placing a virtual buy order with {:.{prec}f} {} @ {:.8f}' - .format(order['base']['amount'], symbol, order['price'], prec=self.market['base']['precision'])) + self.log.info( + 'Placing a virtual buy order with {:.{prec}f} {} @ {:.8f}'.format( + order['base']['amount'], symbol, order['price'], prec=self.market['base']['precision'] + ) + ) self.virtual_orders.append(order) # Immediately lower avail balance @@ -2045,8 +2150,11 @@ def place_virtual_sell_order(self, amount, price): order['for_sale'] = base_asset order['price'] = precise_base_amount / precise_quote_amount - self.log.info('Placing a virtual sell order with {:.{prec}f} {} @ {:.8f}' - .format(amount, symbol, order['price'], prec=self.market['quote']['precision'])) + self.log.info( + 'Placing a virtual sell order with {:.{prec}f} {} @ {:.8f}'.format( + amount, symbol, order['price'], prec=self.market['quote']['precision'] + ) + ) self.virtual_orders.append(order) # Immediately lower avail balance @@ -2101,5 +2209,6 @@ def tick(self, d): class VirtualOrder(dict): """ 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 f5ec6d426..a5c9ee3f9 100644 --- a/dexbot/strategies/strategy_template.py +++ b/dexbot/strategies/strategy_template.py @@ -40,6 +40,7 @@ class Strategy(StrategyBase): NOTE: Change this comment section to describe the strategy. """ + @classmethod def configure(cls, return_base_config=True): return StrategyConfig.configure(return_base_config) diff --git a/dexbot/ui.py b/dexbot/ui.py index 90d77ff75..244bdbb48 100644 --- a/dexbot/ui.py +++ b/dexbot/ui.py @@ -1,21 +1,19 @@ +import logging +import logging.config import os import os.path import sys -import logging -import logging.config from functools import update_wrapper 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 bitshares.instance import set_shared_bitshares_instance +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__) @@ -23,23 +21,24 @@ def verbose(f): @click.pass_context def new_func(ctx, *args, **kwargs): - verbosity = [ - "critical", "error", "warn", "info", "debug" - ][int(min(ctx.obj.get("verbose", 0), 4))] + verbosity = ["critical", "error", "warn", "info", "debug"][int(min(ctx.obj.get("verbose", 0), 4))] 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_name)s using account %(account)s on %(market)s - %(levelname)s - %(message)s') + '%(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) - %(worker_name)s - %(levelname)s - %(message)s') + '%(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_name)s using account %(account)s on %(market)s - %(levelname)s - %(message)s') + '%(asctime)s - %(worker_name)s using account %(account)s on %(market)s - %(levelname)s - %(message)s' + ) # Use special format for special workers logger logger = logging.getLogger("dexbot.per_worker") @@ -73,22 +72,19 @@ def new_func(ctx, *args, **kwargs): # GrapheneAPI logging if ctx.obj["verbose"] > 4: - verbosity = [ - "critical", "error", "warn", "info", "debug" - ][int(min(ctx.obj.get("verbose", 4) - 4, 4))] + verbosity = ["critical", "error", "warn", "info", "debug"][int(min(ctx.obj.get("verbose", 4) - 4, 4))] 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))] + verbosity = ["critical", "error", "warn", "info", "debug"][int(min(ctx.obj.get("verbose", 8) - 8, 4))] 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) @@ -113,14 +109,10 @@ def chain(f): @click.pass_context def new_func(ctx, *args, **kwargs): nodelist = sort_nodes(ctx) - ctx.bitshares = BitShares( - nodelist, - num_retries=-1, - expiration=60, - **ctx.obj - ) + ctx.bitshares = BitShares(nodelist, num_retries=-1, expiration=60, **ctx.obj) set_shared_bitshares_instance(ctx.bitshares) return ctx.invoke(f, *args, **kwargs) + return update_wrapper(new_func, f) @@ -139,8 +131,7 @@ def new_func(ctx, *args, **kwargs): # No user available to interact with log.critical("Uptick Passphrase not available, exiting") sys.exit(78) # 'configuration error' in sysexits.h - pwd = click.prompt( - "Current Uptick Wallet Passphrase", hide_input=True) + pwd = click.prompt("Current Uptick Wallet Passphrase", hide_input=True) try: ctx.bitshares.wallet.unlock(pwd) except WrongMasterPasswordException: @@ -151,15 +142,15 @@ 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" + - "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) + 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) return ctx.invoke(f, *args, **kwargs) + return update_wrapper(new_func, f) @@ -178,6 +169,7 @@ def new_func(ctx, *args, **kwargs): with open(ctx.obj["configfile"], 'w') as file: yaml.dump(ctx.config, file, default_flow_style=False) return ctx.invoke(f, *args, **kwargs) + return update_wrapper(new_func, f) @@ -188,6 +180,7 @@ def new_func(ctx, *args, **kwargs): 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) @@ -211,35 +204,20 @@ def formatStd(f): def warning(msg): - click.echo( - "[" + - click.style("Warning", fg="yellow") + - "] " + msg - ) + click.echo("[" + click.style("Warning", fg="yellow") + "] " + msg) def confirmwarning(msg): - return click.confirm( - "[" + - click.style("Warning", fg="yellow") + - "] " + msg - ) + return click.confirm("[" + click.style("Warning", fg="yellow") + "] " + msg) def alert(msg): - click.echo( - "[" + - click.style("Alert", fg="red") + - "] " + msg - ) + click.echo("[" + click.style("Alert", fg="red") + "] " + msg) def confirmalert(msg): - return click.confirm( - "[" + - click.style("Alert", fg="red") + - "] " + msg - ) + return click.confirm("[" + click.style("Alert", fg="red") + "] " + msg) + # error message "translation" # here we convert some of the cryptic Graphene API error messages into a longer sentence @@ -248,8 +226,10 @@ 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", - 'now <= trx.expiration': "Your node has difficulty syncing to the blockchain, consider changing nodes"} +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): diff --git a/dexbot/views/confirmation.py b/dexbot/views/confirmation.py index f6af5d81c..31463f5a4 100644 --- a/dexbot/views/confirmation.py +++ b/dexbot/views/confirmation.py @@ -1,10 +1,9 @@ -from .ui.confirmation_window_ui import Ui_Dialog - from PyQt5 import QtWidgets +from .ui.confirmation_window_ui import Ui_Dialog -class ConfirmationDialog(QtWidgets.QDialog): +class ConfirmationDialog(QtWidgets.QDialog): def __init__(self, text): super().__init__() self.ui = Ui_Dialog() diff --git a/dexbot/views/create_wallet.py b/dexbot/views/create_wallet.py index 87a90d52d..5b341f49a 100644 --- a/dexbot/views/create_wallet.py +++ b/dexbot/views/create_wallet.py @@ -1,12 +1,10 @@ -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 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): - def __init__(self, controller): self.controller = controller super().__init__() diff --git a/dexbot/views/create_worker.py b/dexbot/views/create_worker.py index 5ab9d6583..010246ded 100644 --- a/dexbot/views/create_worker.py +++ b/dexbot/views/create_worker.py @@ -1,11 +1,10 @@ -from .ui.create_worker_window_ui import Ui_Dialog -from dexbot.controllers.worker_controller import WorkerController, UppercaseValidator - +from dexbot.controllers.worker_controller import UppercaseValidator, WorkerController from PyQt5 import QtWidgets +from .ui.create_worker_window_ui import Ui_Dialog -class CreateWorkerView(QtWidgets.QDialog, Ui_Dialog): +class CreateWorkerView(QtWidgets.QDialog, Ui_Dialog): def __init__(self, bitshares_instance): super().__init__() self.strategy_widget = None diff --git a/dexbot/views/edit_worker.py b/dexbot/views/edit_worker.py index f0b43b204..a1d3a6edf 100644 --- a/dexbot/views/edit_worker.py +++ b/dexbot/views/edit_worker.py @@ -1,11 +1,10 @@ -from .ui.edit_worker_window_ui import Ui_Dialog -from dexbot.controllers.worker_controller import WorkerController, UppercaseValidator - +from dexbot.controllers.worker_controller import UppercaseValidator, WorkerController from PyQt5 import QtWidgets +from .ui.edit_worker_window_ui import Ui_Dialog -class EditWorkerView(QtWidgets.QDialog, Ui_Dialog): +class EditWorkerView(QtWidgets.QDialog, Ui_Dialog): def __init__(self, parent_widget, bitshares_instance, worker_name, config): super().__init__() self.worker_name = worker_name diff --git a/dexbot/views/errors.py b/dexbot/views/errors.py index bb56ce3f0..b4ac0f40b 100644 --- a/dexbot/views/errors.py +++ b/dexbot/views/errors.py @@ -1,11 +1,11 @@ import logging import traceback -from dexbot.ui import translate_error -from .ui.error_dialog_ui import Ui_Dialog from dexbot.qt_queue.idle_queue import idle_add +from dexbot.ui import translate_error +from PyQt5 import QtCore, QtWidgets -from PyQt5 import QtWidgets, QtCore +from .ui.error_dialog_ui import Ui_Dialog class PyQtHandler(logging.Handler): @@ -44,7 +44,6 @@ def set_info_handler(self, info_handler): class ErrorDialog(QtWidgets.QDialog, Ui_Dialog): - def __init__(self, title, message, extra=None, detail=None): super().__init__() self.setupUi(self) @@ -89,11 +88,12 @@ def hide_details_func(self): def gui_error(func): """ A decorator for GUI handler functions - traps all exceptions and displays the dialog """ + def func_wrapper(*args, **kwargs): try: return func(*args, **kwargs) except BaseException as exc: - show_dialog("DEXBot Error", "An error occurred with DEXBot: \n"+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 diff --git a/dexbot/views/layouts/flow_layout.py b/dexbot/views/layouts/flow_layout.py index 422a470ac..4945e6c33 100644 --- a/dexbot/views/layouts/flow_layout.py +++ b/dexbot/views/layouts/flow_layout.py @@ -3,7 +3,6 @@ class FlowLayout(QtWidgets.QLayout): - def __init__(self, parent=None, margin=0, spacing=-1): super(FlowLayout, self).__init__(parent) @@ -76,8 +75,7 @@ def _do_layout(self, rect, test_only): line_height = 0 if not test_only: - item.setGeometry( - QtCore.QRect(QtCore.QPoint(x, y), item.sizeHint())) + item.setGeometry(QtCore.QRect(QtCore.QPoint(x, y), item.sizeHint())) x = next_x line_height = max(line_height, item.sizeHint().height()) diff --git a/dexbot/views/notice.py b/dexbot/views/notice.py index 262d7f645..d2395f34e 100644 --- a/dexbot/views/notice.py +++ b/dexbot/views/notice.py @@ -1,10 +1,9 @@ -from .ui.notice_window_ui import Ui_Dialog - from PyQt5 import QtWidgets +from .ui.notice_window_ui import Ui_Dialog -class NoticeDialog(QtWidgets.QDialog, Ui_Dialog): +class NoticeDialog(QtWidgets.QDialog, Ui_Dialog): def __init__(self, text): super().__init__() self.setupUi(self) diff --git a/dexbot/views/settings.py b/dexbot/views/settings.py index aeebc7b43..d9847f87a 100644 --- a/dexbot/views/settings.py +++ b/dexbot/views/settings.py @@ -1,11 +1,9 @@ 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): - def __init__(self): super().__init__() diff --git a/dexbot/views/strategy_form.py b/dexbot/views/strategy_form.py index 9a0a5bfce..3f137eb4a 100644 --- a/dexbot/views/strategy_form.py +++ b/dexbot/views/strategy_form.py @@ -1,29 +1,21 @@ import importlib import dexbot.controllers.strategy_controller - -from PyQt5 import QtWidgets, QtCore, QtGui +from PyQt5 import QtCore, QtGui, QtWidgets class StrategyFormWidget(QtWidgets.QWidget): - def __init__(self, controller, strategy_module, worker_config=None): super().__init__() self.controller = controller self.module_name = strategy_module.split('.')[-1] - strategy_class = getattr( - importlib.import_module(strategy_module), - 'Strategy' - ) + strategy_class = getattr(importlib.import_module(strategy_module), 'Strategy') # 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( - importlib.import_module(form_module), - 'Ui_Form' - ) + widget = getattr(importlib.import_module(form_module), 'Ui_Form') self.strategy_widget = widget() self.strategy_widget.setupUi(self) except (ValueError, AttributeError): @@ -37,23 +29,14 @@ def __init__(self, controller, strategy_module, worker_config=None): try: # Try to get the controller from the internal set - strategy_controller = getattr( - dexbot.controllers.strategy_controller, - class_name - ) + strategy_controller = getattr(dexbot.controllers.strategy_controller, class_name) except AttributeError: try: # look in the strategy module itself (external strategies may do this) - strategy_controller = getattr( - importlib.import_module(strategy_module), - 'StrategyController' - ) + 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' - ) + strategy_controller = getattr(dexbot.controllers.strategy_controller, 'StrategyController') self.strategy_controller = strategy_controller(self, configure, controller, worker_config) @@ -98,11 +81,9 @@ def add_element(self, option): extra = option.extra if element_type == 'float': - element = self.add_double_spin_box( - key, title, extra[0], extra[1], extra[2], extra[3], description) + element = self.add_double_spin_box(key, title, extra[0], extra[1], extra[2], extra[3], description) elif element_type == 'int': - element = self.add_spin_box( - key, title, extra[0], extra[1], extra[2], description) + element = self.add_spin_box(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': diff --git a/dexbot/views/unlock_wallet.py b/dexbot/views/unlock_wallet.py index f2ce65d05..90cfef318 100644 --- a/dexbot/views/unlock_wallet.py +++ b/dexbot/views/unlock_wallet.py @@ -1,12 +1,10 @@ -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 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): - def __init__(self, controller): self.controller = controller super().__init__() diff --git a/dexbot/views/worker_details.py b/dexbot/views/worker_details.py index 138fe0e3f..5b56a39b2 100644 --- a/dexbot/views/worker_details.py +++ b/dexbot/views/worker_details.py @@ -1,20 +1,17 @@ +import importlib import os from dexbot.controllers.worker_details_controller import WorkerDetailsController 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 dexbot.views.ui.worker_details_window_ui import Ui_details_dialog from PyQt5 import QtWidgets from PyQt5.QtWidgets import QWidget -import importlib - class WorkerDetailsView(QtWidgets.QDialog, Ui_details_dialog, Ui_Graph_Tab, Ui_Table_Tab, Ui_Text_Tab): - def __init__(self, worker_name, config): super().__init__() diff --git a/dexbot/views/worker_item.py b/dexbot/views/worker_item.py index e17810154..fcaaf7b37 100644 --- a/dexbot/views/worker_item.py +++ b/dexbot/views/worker_item.py @@ -1,18 +1,17 @@ import re -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 +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 +from .ui.worker_item_widget_ui import Ui_widget +from .worker_details import WorkerDetailsView -class WorkerItemWidget(QtWidgets.QWidget, Ui_widget): +class WorkerItemWidget(QtWidgets.QWidget, Ui_widget): def __init__(self, worker_name, config, main_ctrl, view): super().__init__() @@ -55,7 +54,7 @@ def setup_ui_data(self, config): self.set_worker_slider(50) @gui_error - def toggle_worker(self, ): + def toggle_worker(self,): if self.horizontalLayout_5.alignment() != QtCore.Qt.AlignRight: self.start_worker() else: @@ -127,7 +126,7 @@ 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) + 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 @@ -142,8 +141,7 @@ def set_worker_slider(self, value): @gui_error def remove_widget_dialog(self): - dialog = ConfirmationDialog( - 'Are you sure you want to remove worker "{}"?'.format(self.worker_name)) + dialog = ConfirmationDialog('Are you sure you want to remove worker "{}"?'.format(self.worker_name)) return_value = dialog.exec_() if return_value: self.remove_widget() @@ -167,8 +165,9 @@ def handle_open_details(self): @gui_error def handle_edit_worker(self): - edit_worker_dialog = EditWorkerView(self, self.main_ctrl.bitshares_instance, - self.worker_name, self.worker_config) + edit_worker_dialog = EditWorkerView( + self, self.main_ctrl.bitshares_instance, self.worker_name, self.worker_config + ) return_value = edit_worker_dialog.exec_() # User clicked save @@ -176,9 +175,9 @@ def handle_edit_worker(self): 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_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 18b54272d..7890709a2 100644 --- a/dexbot/views/worker_list.py +++ b/dexbot/views/worker_list.py @@ -1,10 +1,12 @@ import time -from threading import Thread import webbrowser +from threading import Thread from dexbot import __version__ from dexbot.config import Config from dexbot.controllers.wallet_controller import WalletController +from dexbot.qt_queue.idle_queue import idle_add +from dexbot.qt_queue.queue_dispatcher import ThreadDispatcher from dexbot.views.create_wallet import CreateWalletView from dexbot.views.create_worker import CreateWorkerView from dexbot.views.errors import gui_error @@ -13,17 +15,13 @@ 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 - +from grapheneapi.exceptions import NumRetriesReached from PyQt5.QtCore import pyqtSlot from PyQt5.QtGui import QFontDatabase from PyQt5.QtWidgets import QMainWindow -from grapheneapi.exceptions import NumRetriesReached class MainView(QMainWindow, Ui_MainWindow): - def __init__(self, main_controller): super().__init__() self.setupUi(self) @@ -61,8 +59,10 @@ def connect_to_bitshares(self): 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.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 @@ -71,8 +71,9 @@ def connect_to_bitshares(self): 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__)) + self.status_bar.showMessage( + 'ver {} - Node(s) not found. ' 'Please add node(s) from settings.'.format(__version__) + ) return False @pyqtSlot(name='handle_login') diff --git a/dexbot/whiptail.py b/dexbot/whiptail.py index 51d16eae7..d64fd3bdc 100644 --- a/dexbot/whiptail.py +++ b/dexbot/whiptail.py @@ -1,14 +1,15 @@ from __future__ import print_function -import sys -import shlex -import shutil + import itertools -import click import os +import shlex +import shutil +import sys import tempfile -from subprocess import Popen, PIPE from collections import namedtuple +from subprocess import PIPE, Popen +import click # whiptail.py - Use whiptail to display dialog boxes from shell scripts # Copyright (C) 2013 Marwan Alsabbagh @@ -23,9 +24,7 @@ def flatten(data): class Whiptail: - - def __init__(self, title='', backtitle='', height=20, width=60, - auto_exit=True): + def __init__(self, title='', backtitle='', height=20, width=60, auto_exit=True): self.title = title self.backtitle = backtitle self.height = height @@ -34,8 +33,15 @@ def __init__(self, title='', backtitle='', height=20, width=60, 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) + 'whiptail', + '--title', + self.title, + '--backtitle', + self.backtitle, + '--' + control, + msg, + str(self.height), + str(self.width), ] cmd += list(extra) p = Popen(cmd, stderr=PIPE) @@ -125,11 +131,7 @@ 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 - ) + click.echo("[" + click.style("alert", fg="yellow") + "] " + msg) def view_text(self, text, pager=True): if pager: diff --git a/dexbot/worker.py b/dexbot/worker.py index c2584ee0e..1f7571353 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -1,15 +1,14 @@ +import copy import importlib -import sys import logging import os.path +import sys import threading -import copy import dexbot.errors as errors -from dexbot.strategies.base import StrategyBase - -from bitshares.notify import Notify from bitshares.instance import shared_bitshares_instance +from bitshares.notify import Notify +from dexbot.strategies.base import StrategyBase log = logging.getLogger(__name__) log_workers = logging.getLogger('dexbot.per_worker') @@ -20,13 +19,7 @@ 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 @@ -52,35 +45,44 @@ def init_workers(self, config): 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={ - 'worker_name': worker_name, 'account': 'unknown', - 'market': 'unknown', 'is_disabled': (lambda: True) - }) + 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) - }) + 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' - ) + strategy_class = getattr(importlib.import_module(worker["module"]), 'Strategy') self.workers[worker_name] = strategy_class( - config=config, - name=worker_name, - bitshares_instance=self.bitshares, - view=self.view + config=config, name=worker_name, bitshares_instance=self.bitshares, view=self.view ) 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) - }) + log_workers.exception( + "Worker initialisation", + extra={ + 'worker_name': worker_name, + 'account': worker['account'], + 'market': 'unknown', + 'is_disabled': (lambda: True), + }, + ) self.config_lock.release() def update_notify(self): @@ -101,7 +103,7 @@ def update_notify(self): on_market=self.on_market, on_account=self.on_account, on_block=self.on_block, - bitshares_instance=self.bitshares + bitshares_instance=self.bitshares, ) # Events diff --git a/docs/conf.py b/docs/conf.py index 57908ba21..7b5585876 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,8 +13,8 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys import os +import sys # 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 @@ -25,7 +25,7 @@ # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -52,7 +52,7 @@ source_suffix = '.rst' # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' @@ -80,9 +80,9 @@ # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -90,27 +90,27 @@ # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -125,26 +125,26 @@ # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -154,62 +154,62 @@ # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' -#html_search_language = 'en' +# html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} +# html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' +# html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'DEXBotdoc' @@ -217,59 +217,52 @@ # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'DEXBot.tex', 'DEXBot Documentation', - 'DEXBot Team', 'manual'), + (master_doc, 'DEXBot.tex', 'DEXBot Documentation', 'DEXBot Team', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'dexbot', 'DEXBot Documentation', - [author], 1) -] +man_pages = [(master_doc, 'dexbot', 'DEXBot Documentation', [author], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -278,19 +271,25 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'DEXBot', 'DEXBot Documentation', - author, 'DEXBot', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + 'DEXBot', + 'DEXBot Documentation', + author, + 'DEXBot', + 'One line description of project.', + 'Miscellaneous', + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False diff --git a/docs/configuration.rst b/docs/configuration.rst index 2cbfe19ee..7ac764f65 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -5,19 +5,19 @@ The configuration consists of a series of questions about the bots you wish to c 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. 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:`follow_orders` My (Ian Haywood) main bot, an extension of stakemachine's `wall`, - it has been used to provide liquidity on AUD:BTS. + 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 @@ -25,14 +25,14 @@ The configuration consists of a series of questions about the bots you wish to c 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. 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. diff --git a/docs/manual.rst b/docs/manual.rst index c5492f92f..fd1589302 100644 --- a/docs/manual.rst +++ b/docs/manual.rst @@ -28,7 +28,7 @@ The config.yml file NAME_OF_BOT: # Python module to look for the strategy (can be custom) - # dexbot will search in ~/bots as well as standard dirs + # dexbot will search in ~/bots as well as standard dirs module: "dexbot.strategies.echo" # The bot class in that module to use @@ -49,6 +49,3 @@ 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 97ca940f3..58ecc8164 100644 --- a/docs/setup.rst +++ b/docs/setup.rst @@ -32,7 +32,7 @@ On CentOS/RedHat:: 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 ------------ @@ -54,8 +54,8 @@ bot's account into a local wallet. This can be done using uptick addkey 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". +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 @@ -76,4 +76,3 @@ Configuration This will walk you through the configuration process. Read more about this in the :doc:`configuration`. - diff --git a/hooks/rthook-Crypto.py b/hooks/rthook-Crypto.py index e18378ec6..38a52b731 100644 --- a/hooks/rthook-Crypto.py +++ b/hooks/rthook-Crypto.py @@ -1,15 +1,18 @@ # Runtime hook for pycryptodome extensions -import Crypto.Util._raw_api import importlib.machinery import os.path import sys +import Crypto.Util._raw_api + + 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 + 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 diff --git a/installer/windows/bundle/bundle.wixproj b/installer/windows/bundle/bundle.wixproj index f3d5cd6ec..3cc387007 100644 --- a/installer/windows/bundle/bundle.wixproj +++ b/installer/windows/bundle/bundle.wixproj @@ -70,4 +70,4 @@ --> - \ 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/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..f9f741f2a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[tool.black] +line-length = 120 +skip-string-normalization = true +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.tox + | \.venv + | _build + | buck-out + | build + | dist +)/ +''' 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/requirements-dev.txt b/requirements-dev.txt index f37fb9cf1..3d59ffefa 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,5 @@ # Packages needed for running testsuite docker==3.7.2 pytest==4.4.0 +# Needed for development +pre-commit==1.20.0 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.