Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI autogeneration #251

Merged
merged 12 commits into from
Jul 27, 2018
56 changes: 27 additions & 29 deletions dexbot/basestrategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .storage import Storage
from .statemachine import StateMachine
from .config import Config
from .helper import truncate

from events import Events
import bitsharesapi
Expand All @@ -18,20 +19,20 @@
from bitshares.price import FilledOrder, Order, UpdateCallOrder
from bitshares.instance import shared_bitshares_instance


MAX_TRIES = 3

ConfigElement = collections.namedtuple('ConfigElement', 'key type default description extra')
# Bots need to specify their own configuration values
# I want this to be UI-agnostic so a future web or GUI interface can use it too
# so each bot can have a class method 'configure' which returns a list of ConfigElement
# named tuples. Tuple fields as follows.
# Key: the key in the bot config dictionary that gets saved back to config.yml
# Type: one of "int", "float", "bool", "string", "choice"
# Default: the default value. must be right type.
# Description: comments to user, full sentences encouraged
# Extra:
# For int & float: a (min, max) tuple
ConfigElement = collections.namedtuple('ConfigElement', 'key type default title description extra')
# Strategies need to specify their own configuration values, so each strategy can have
# a class method 'configure' which returns a list of ConfigElement named tuples.
# Tuple fields as follows:
# - Key: the key in the bot config dictionary that gets saved back to config.yml
# - Type: one of "int", "float", "bool", "string", "choice"
# - Default: the default value. must be right type.
# - Title: name shown to the user, preferably not too long
# - Description: comments to user, full sentences encouraged
# - Extra:
# For int: a (min, max, suffix) tuple
# For float: a (min, max, precision, suffix) tuple
# For string: a regular expression, entries must match it, can be None which equivalent to .*
# For bool, ignored
# For choice: a list of choices, choices are in turn (tag, label) tuples.
Expand Down Expand Up @@ -61,17 +62,17 @@ class BaseStrategy(Storage, StateMachine, Events):
* ``basestrategy.log``: a per-worker logger (actually LoggerAdapter) adds worker-specific context:
worker name & account (Because some UIs might want to display per-worker logs)

Also, Base Strategy inherits :class:`dexbot.storage.Storage`
Also, BaseStrategy inherits :class:`dexbot.storage.Storage`
which allows to permanently store data in a sqlite database
using:

``basestrategy["key"] = "value"``

.. note:: This applies a ``json.loads(json.dumps(value))``!

Workers must never attempt to interact with the user, they must assume they are running unattended
They can log events. If a problem occurs they can't fix they should set self.disabled = True and throw an exception
The framework catches all exceptions thrown from event handlers and logs appropriately.
Workers must never attempt to interact with the user, they must assume they are running unattended.
They can log events. If a problem occurs they can't fix they should set self.disabled = True and
throw an exception. The framework catches all exceptions thrown from event handlers and logs appropriately.
"""

__events__ = [
Expand All @@ -87,7 +88,7 @@ class BaseStrategy(Storage, StateMachine, Events):
]

@classmethod
def configure(cls):
def configure(cls, return_base_config=True):
"""
Return a list of ConfigElement objects defining the configuration values for
this class
Expand All @@ -97,13 +98,16 @@ def configure(cls):
NOTE: when overriding you almost certainly will want to call the ancestor
and then add your config values to the list.
"""
# these configs are common to all bots
return [
ConfigElement("account", "string", "", "BitShares account name for the bot to operate with", ""),
ConfigElement("market", "string", "USD:BTS",
# These configs are common to all bots
base_config = [
ConfigElement("account", "string", "", "Account", "BitShares account name for the bot to operate with", ""),
ConfigElement("market", "string", "USD:BTS", "Market",
"BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"",
r"[A-Z\.]+[:\/][A-Z\.]+")
]
if return_base_config:
return base_config
return []

def __init__(
self,
Expand Down Expand Up @@ -409,7 +413,7 @@ def pause(self):
def market_buy(self, amount, price, return_none=False, *args, **kwargs):
symbol = self.market['base']['symbol']
precision = self.market['base']['precision']
base_amount = self.truncate(price * amount, precision)
base_amount = truncate(price * amount, precision)

# Make sure we have enough balance for the order
if self.balance(self.market['base']) < base_amount:
Expand Down Expand Up @@ -448,7 +452,7 @@ def market_buy(self, amount, price, return_none=False, *args, **kwargs):
def market_sell(self, amount, price, return_none=False, *args, **kwargs):
symbol = self.market['quote']['symbol']
precision = self.market['quote']['precision']
quote_amount = self.truncate(amount, precision)
quote_amount = truncate(amount, precision)

# Make sure we have enough balance for the order
if self.balance(self.market['quote']) < quote_amount:
Expand Down Expand Up @@ -599,12 +603,6 @@ def retry_action(self, action, *args, **kwargs):
else:
raise

@staticmethod
def truncate(number, decimals):
""" Change the decimal point of a number without rounding
"""
return math.floor(number * 10 ** decimals) / 10 ** decimals

def write_order_log(self, worker_name, order):
operation_type = 'TRADE'

Expand Down
34 changes: 20 additions & 14 deletions dexbot/cli_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from dexbot.whiptail import get_whiptail
from dexbot.basestrategy import BaseStrategy


# FIXME: auto-discovery of strategies would be cool but can't figure out a way
STRATEGIES = [
{'tag': 'relative',
Expand Down Expand Up @@ -65,21 +64,26 @@ def process_config_element(elem, whiptail, config):
d: the Dialog object
config: the config dictionary for this worker
"""
if elem.description:
title = '{} - {}'.format(elem.title, elem.description)
else:
title = elem.title

if elem.type == "string":
txt = whiptail.prompt(elem.description, config.get(elem.key, elem.default))
txt = whiptail.prompt(title, config.get(elem.key, elem.default))
if elem.extra:
while not re.match(elem.extra, txt):
whiptail.alert("The value is not valid")
txt = whiptail.prompt(
elem.description, config.get(
title, config.get(
elem.key, elem.default))
config[elem.key] = txt
if elem.type == "bool":
value = config.get(elem.key, elem.default)
value = 'yes' if value else 'no'
config[elem.key] = whiptail.confirm(elem.description, value)
config[elem.key] = whiptail.confirm(title, value)
if elem.type in ("float", "int"):
txt = whiptail.prompt(elem.description, str(config.get(elem.key, elem.default)))
txt = whiptail.prompt(title, str(config.get(elem.key, elem.default)))
while True:
try:
if elem.type == "int":
Expand All @@ -94,10 +98,10 @@ def process_config_element(elem, whiptail, config):
break
except ValueError:
whiptail.alert("Not a valid value")
txt = whiptail.prompt(elem.description, str(config.get(elem.key, elem.default)))
txt = whiptail.prompt(title, str(config.get(elem.key, elem.default)))
config[elem.key] = val
if elem.type == "choice":
config[elem.key] = whiptail.radiolist(elem.description, select_choice(
config[elem.key] = whiptail.radiolist(title, select_choice(
config.get(elem.key, elem.default), elem.extra))


Expand Down Expand Up @@ -173,8 +177,9 @@ def configure_worker(whiptail, worker):
for c in configs:
process_config_element(c, whiptail, worker)
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")
whiptail.alert(
"This worker type does not have configuration information. "
"You will have to check the worker code and add configuration values to config.yml if required")
return worker


Expand All @@ -190,11 +195,12 @@ def configure_dexbot(config, ctx):
setup_systemd(whiptail, config)
else:
bitshares_instance = ctx.bitshares
action = whiptail.menu("You have an existing configuration.\nSelect an action:",
[('NEW', 'Create a new worker'),
('DEL', 'Delete a worker'),
('EDIT', 'Edit a worker'),
('CONF', 'Redo general config')])
action = whiptail.menu(
"You have an existing configuration.\nSelect an action:",
[('NEW', 'Create a new worker'),
('DEL', 'Delete a worker'),
('EDIT', 'Edit a worker'),
('CONF', 'Redo general config')])

if action == 'EDIT':
worker_name = whiptail.menu("Select worker to edit", [(i, i) for i in workers])
Expand Down