From 7ebd3951c7b4c3200b51d2ac4ed861689d9ce50f Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 31 Aug 2018 16:51:35 +0300 Subject: [PATCH 01/42] Add base.py as new base for strategies --- dexbot/strategies/base.py | 1079 +++++++++++++++++++++++++++++++++++++ 1 file changed, 1079 insertions(+) create mode 100644 dexbot/strategies/base.py diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py new file mode 100644 index 000000000..154bcbbf8 --- /dev/null +++ b/dexbot/strategies/base.py @@ -0,0 +1,1079 @@ +import datetime +import copy +import collections +import logging +import math +import time + +from dexbot.config import Config +from dexbot.storage import Storage +from dexbot.statemachine import StateMachine +from dexbot.helper import truncate + +from events import Events +import bitshares.exceptions +import bitsharesapi +import bitsharesapi.exceptions +from bitshares.account import Account +from bitshares.amount import Amount, Asset +from bitshares.instance import shared_bitshares_instance +from bitshares.market import Market +from bitshares.price import FilledOrder, Order, UpdateCallOrder + +# Number of maximum retries used to retry action before failing +MAX_TRIES = 3 + +""" Strategies need to specify their own configuration values, so each strategy can have a class method 'configure' + which returns a list of ConfigElement named tuples. + + Tuple fields as follows: + - Key: The key in the bot config dictionary that gets saved back to config.yml + - Type: "int", "float", "bool", "string" or "choice" + - Default: The default value, must be same type as the Type defined + - Title: Name shown to the user, preferably not too long + - Description: Comments to user, full sentences encouraged + - Extra: + :int: a (min, max, suffix) tuple + :float: a (min, max, precision, suffix) tuple + :string: a regular expression, entries must match it, can be None which equivalent to .* + :bool, ignored + :choice: a list of choices, choices are in turn (tag, label) tuples. + labels get presented to user, and tag is used as the value saved back to the config dict +""" +ConfigElement = collections.namedtuple('ConfigElement', 'key type default title description extra') + + +class StrategyBase(Storage, StateMachine, Events): + """ A strategy based on this class is intended to work in one market. This class contains + most common methods needed by the strategy. + + All prices are passed and returned as BASE/QUOTE. + (In the BREAD:USD market that would be USD/BREAD, 2.5 USD / 1 BREAD). + - Buy orders reserve BASE + - Sell orders reserve QUOTE + + Todo: This is copy / paste from old, update this if needed! + Strategy inherits: + * :class:`dexbot.storage.Storage` : Stores data to sqlite database + * :class:`dexbot.statemachine.StateMachine` + * ``Events`` + + Todo: This is copy / paste from old, update this if needed! + Available attributes: + * ``worker.bitshares``: instance of ´`bitshares.BitShares()`` + * ``worker.add_state``: Add a specific state + * ``worker.set_state``: Set finite state machine + * ``worker.get_state``: Change state of state machine + * ``worker.account``: The Account object of this worker + * ``worker.market``: The market used by this worker + * ``worker.orders``: List of open orders of the worker's account in the worker's market + * ``worker.balance``: List of assets and amounts available in the worker's account + * ``worker.log``: a per-worker logger (actually LoggerAdapter) adds worker-specific context: + worker name & account (Because some UIs might want to display per-worker logs) + + Also, Worker inherits :class:`dexbot.storage.Storage` + which allows to permanently store data in a sqlite database + using: + + ``worker["key"] = "value"`` + + .. note:: This applies a ``json.loads(json.dumps(value))``! + + Workers must never attempt to interact with the user, they must assume they are running unattended. + They can log events. If a problem occurs they can't fix they should set self.disabled = True and + throw an exception. The framework catches all exceptions thrown from event handlers and logs appropriately. + """ + + __events__ = [ + 'onAccount', + 'onMarketUpdate', + 'onOrderMatched', + 'onOrderPlaced', + 'ontick', + 'onUpdateCallOrder', + 'error_onAccount', + 'error_onMarketUpdate', + 'error_ontick', + ] + + @classmethod + def configure(cls, return_base_config=True): + """ Return a list of ConfigElement objects defining the configuration values for this class. + + User interfaces should then generate widgets based on these values, gather data and save back to + the config dictionary for the worker. + + NOTE: When overriding you almost certainly will want to call the ancestor and then + add your config values to the list. + + :param return_base_config: bool: + :return: Returns a list of config elements + """ + + # Common configs + base_config = [ + ConfigElement("account", "string", "", "Account", + "BitShares account name for the bot to operate with", + ""), + ConfigElement("market", "string", "USD:BTS", "Market", + "BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"", + r"[A-Z\.]+[:\/][A-Z\.]+"), + ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset', + 'Asset to be used to pay transaction fees', + r'[A-Z\.]+') + ] + + # Todo: Is there any case / strategy where the base config would NOT be needed, making this unnecessary? + if return_base_config: + return base_config + return [] + + def __init__(self, + worker_name, + config=None, + on_account=None, + on_order_matched=None, + on_order_placed=None, + on_market_update=None, + on_update_call_order=None, + ontick=None, + bitshares_instance=None, + *args, + **kwargs): + + # BitShares instance + self.bitshares = bitshares_instance or shared_bitshares_instance() + + # Storage + Storage.__init__(self, worker_name) + + # Statemachine + StateMachine.__init__(self, worker_name) + + # Events + Events.__init__(self) + + if ontick: + self.ontick += ontick + if on_market_update: + self.onMarketUpdate += on_market_update + if on_account: + self.onAccount += on_account + if on_order_matched: + self.onOrderMatched += on_order_matched + if on_order_placed: + self.onOrderPlaced += on_order_placed + if on_update_call_order: + self.onUpdateCallOrder += on_update_call_order + + # Redirect this event to also call order placed and order matched + self.onMarketUpdate += self._callbackPlaceFillOrders + + if config: + self.config = config + else: + self.config = config = Config.get_worker_config_file(worker_name) + + # Get worker's parameters from the config + self.worker = config["workers"][worker_name] + + # Get Bitshares account and market for this worker + self._account = Account(self.worker["account"], full=True, bitshares_instance=self.bitshares) + self._market = Market(config["workers"][worker_name]["market"], bitshares_instance=self.bitshares) + + # Recheck flag - Tell the strategy to check for updated orders + self.recheck_orders = False + + # Set fee asset + fee_asset_symbol = self.worker.get('fee_asset') + + if fee_asset_symbol: + try: + self.fee_asset = Asset(fee_asset_symbol) + except bitshares.exceptions.AssetDoesNotExistsException: + self.fee_asset = Asset('1.3.0') + else: + # If there is no fee asset, use BTS + self.fee_asset = Asset('1.3.0') + + # Settings for bitshares instance + self.bitshares.bundle = bool(self.worker.get("bundle", False)) + + # Disabled flag - this flag can be flipped to True by a worker and will be reset to False after reset only + self.disabled = False + + # Order expiration time in seconds + self.expiration = 60 * 60 * 24 * 365 * 5 + + # A private logger that adds worker identify data to the LogRecord + self.log = logging.LoggerAdapter( + logging.getLogger('dexbot.per_worker'), + { + 'worker_name': worker_name, + 'account': self.worker['account'], + 'market': self.worker['market'], + 'is_disabled': lambda: self.disabled + } + ) + + self.orders_log = logging.LoggerAdapter( + logging.getLogger('dexbot.orders_log'), {} + ) + + def _calculate_center_price(self, suppress_errors=False): + """ + + :param suppress_errors: + :return: + """ + # Todo: Add documentation + ticker = self.market.ticker() + highest_bid = ticker.get("highestBid") + lowest_ask = ticker.get("lowestAsk") + + if highest_bid is None or highest_bid == 0.0: + if not suppress_errors: + self.log.critical( + "Cannot estimate center price, there is no highest bid." + ) + self.disabled = True + return None + elif lowest_ask is None or lowest_ask == 0.0: + if not suppress_errors: + self.log.critical( + "Cannot estimate center price, there is no lowest ask." + ) + self.disabled = True + return None + + center_price = highest_bid['price'] * math.sqrt(lowest_ask['price'] / highest_bid['price']) + return center_price + + def _callbackPlaceFillOrders(self, d): + """ This method distinguishes notifications caused by Matched orders from those caused by placed orders + Todo: can this be renamed to _instantFill()? + """ + # Todo: Add documentation + if isinstance(d, FilledOrder): + self.onOrderMatched(d) + elif isinstance(d, Order): + self.onOrderPlaced(d) + elif isinstance(d, UpdateCallOrder): + self.onUpdateCallOrder(d) + else: + pass + + def _cancel_orders(self, orders): + """ + + :param orders: + :return: + """ + # Todo: Add documentation + try: + self.retry_action( + self.bitshares.cancel, + orders, account=self.account, fee_asset=self.fee_asset['id'] + ) + except bitsharesapi.exceptions.UnhandledRPCError as exception: + if str(exception).startswith('Assert Exception: maybe_found != nullptr: Unable to find Object'): + # The order(s) we tried to cancel doesn't exist + self.bitshares.txbuffer.clear() + return False + else: + self.log.exception("Unable to cancel order") + except bitshares.exceptions.MissingKeyError: + self.log.exception('Unable to cancel order(s), private key missing.') + + return True + + def account_total_value(self, return_asset): + """ Returns the total value of the account in given asset + + :param string | return_asset: Balance is returned as this asset + :return: float: Value of the account in one asset + """ + total_value = 0 + + # Total balance calculation + for balance in self.balances: + if balance['symbol'] != return_asset: + # Convert to asset if different + total_value += self.convert_asset(balance['amount'], balance['symbol'], return_asset) + else: + total_value += balance['amount'] + + # Orders balance calculation + for order in self.all_own_orders: + updated_order = self.get_updated_order(order['id']) + + if not order: + continue + if updated_order['base']['symbol'] == return_asset: + total_value += updated_order['base']['amount'] + else: + total_value += self.convert_asset( + updated_order['base']['amount'], + updated_order['base']['symbol'], + return_asset + ) + + return total_value + + def balance(self, asset, fee_reservation=False): + """ Return the balance of your worker's account for a specific asset + + :param bool | fee_reservation: + :return: Balance of specific asset + """ + # Todo: Add documentation + return self._account.balance(asset) + + def calculate_center_price(self, center_price=None, asset_offset=False, spread=None, + order_ids=None, manual_offset=0, suppress_errors=False): + # Todo: Fix comment + """ Calculate center price which shifts based on available funds + """ + if center_price is None: + # No center price was given so we simply calculate the center price + calculated_center_price = self._calculate_center_price(suppress_errors) + else: + # Center price was given so we only use the calculated center price + # for quote to base asset conversion + calculated_center_price = self._calculate_center_price(True) + if not calculated_center_price: + calculated_center_price = center_price + + if center_price: + calculated_center_price = center_price + + if asset_offset: + total_balance = self.get_allocated_assets(order_ids) + total = (total_balance['quote'] * calculated_center_price) + total_balance['base'] + + if not total: # Prevent division by zero + balance = 0 + else: + # Returns a value between -1 and 1 + balance = (total_balance['base'] / total) * 2 - 1 + + if balance < 0: + # With less of base asset center price should be offset downward + calculated_center_price = calculated_center_price / math.sqrt(1 + spread * (balance * -1)) + elif balance > 0: + # With more of base asset center price will be offset upwards + calculated_center_price = calculated_center_price * math.sqrt(1 + spread * balance) + else: + calculated_center_price = calculated_center_price + + # Calculate final_offset_price if manual center price offset is given + if manual_offset: + calculated_center_price = calculated_center_price + (calculated_center_price * manual_offset) + + return calculated_center_price + + def calculate_order_data(self, order, amount, price): + quote_asset = Amount(amount, self.market['quote']['symbol']) + order['quote'] = quote_asset + order['price'] = price + base_asset = Amount(amount * price, self.market['base']['symbol']) + order['base'] = base_asset + return order + + def calculate_worker_value(self, unit_of_measure, refresh=True): + """ Returns the combined value of allocated and available QUOTE and BASE, measured in "unit_of_measure". + + :param unit_of_measure: + :param refresh: + :return: + """ + # Todo: Insert logic here + + def cancel_all_orders(self): + """ Cancel all orders of the worker's account + """ + self.log.info('Canceling all orders') + + if self.all_own_orders: + self.cancel(self.all_own_orders) + + self.log.info("Orders canceled") + + def cancel_orders(self, orders, batch_only=False): + """ Cancel specific order(s) + + :param list | orders: List of orders to cancel + :param bool | batch_only: Try cancel orders only in batch mode without one-by-one fallback + :return: + """ + # Todo: Add documentation + if not isinstance(orders, (list, set, tuple)): + orders = [orders] + + orders = [order['id'] for order in orders if 'id' in order] + + success = self._cancel_orders(orders) + if not success and batch_only: + return False + if not success and len(orders) > 1 and not batch_only: + # One of the order cancels failed, cancel the orders one by one + for order in orders: + self._cancel_orders(order) + return True + + def count_asset(self, order_ids=None, return_asset=False, refresh=True): + """ Returns the combined amount of the given order ids and the account balance + The amounts are returned in quote and base assets of the market + + :param list | order_ids: list of order ids to be added to the balance + :param bool | return_asset: true if returned values should be Amount instances + :return: dict with keys quote and base + Todo: When would we want the sum of a subset of orders? Why order_ids? Maybe just specify asset? + """ + quote = 0 + base = 0 + quote_asset = self.market['quote']['id'] + base_asset = self.market['base']['id'] + + # Total balance calculation + for balance in self.balances: + if balance.asset['id'] == quote_asset: + quote += balance['amount'] + elif balance.asset['id'] == base_asset: + base += balance['amount'] + + if order_ids is None: + # Get all orders from Blockchain + order_ids = [order['id'] for order in self.current_market_own_orders] + if order_ids: + orders_balance = self.orders_balance(order_ids) + quote += orders_balance['quote'] + base += orders_balance['base'] + + if return_asset: + quote = Amount(quote, quote_asset) + base = Amount(base, base_asset) + + return {'quote': quote, 'base': base} + + def get_allocated_assets(self, order_ids, return_asset=False, refresh=True): + # Todo: + """ Returns the amount of QUOTE and BASE allocated in orders, and that do not show up in available balance + + :param order_ids: + :param return_asset: + :param refresh: + :return: + """ + # Todo: Add documentation + if not order_ids: + order_ids = [] + elif isinstance(order_ids, str): + order_ids = [order_ids] + + quote = 0 + base = 0 + quote_asset = self.market['quote']['id'] + base_asset = self.market['base']['id'] + + for order_id in order_ids: + order = self.get_updated_order(order_id) + if not order: + continue + asset_id = order['base']['asset']['id'] + if asset_id == quote_asset: + quote += order['base']['amount'] + elif asset_id == base_asset: + base += order['base']['amount'] + + if return_asset: + quote = Amount(quote, quote_asset) + base = Amount(base, base_asset) + + return {'quote': quote, 'base': base} + + def get_lowest_market_sell(self, refresh=False): + """ Returns the lowest sell order that is not own, regardless of order size. + + :param refresh: + :return: + """ + # Todo: Insert logic here + + def get_highest_market_buy(self, refresh=False): + """ Returns the highest buy order not owned by worker account, regardless of order size. + + :param refresh: + :return: + """ + # Todo: Insert logic here + + def get_lowest_own_sell(self, refresh=False): + """ Returns lowest own sell order. + + :param refresh: + :return: + """ + # Todo: Insert logic here + + def get_highest_own_buy(self, refresh=False): + """ Returns highest own buy order. + + :param refresh: + :return: + """ + # Todo: Insert logic here + + def get_price_for_amount_buy(self, amount=None, refresh=False): + """ Returns the cumulative price for which you could buy the specified amount of QUOTE. + This method must take into account market fee. + + :param amount: + :param refresh: + :return: + """ + # Todo: Insert logic here + + def get_price_for_amount_sell(self, amount=None, refresh=False): + """ Returns the cumulative price for which you could sell the specified amount of QUOTE + + :param amount: + :param refresh: + :return: + """ + # Todo: Insert logic here + + def get_market_center_price(self, depth=0, refresh=False): + """ Returns the center price of market including own orders. + + :param depth: 0 = calculate from closest opposite orders. non-zero = calculate from specified depth (quote or base?) + :param refresh: + :return: + """ + # Todo: Insert logic here + + def get_external_price(self, source): + """ Returns the center price of market including own orders. + + :param source: + :return: + """ + # Todo: Insert logic here + + def get_market_spread(self, method, refresh=False): + """ Get spread from closest opposite orders, including own. + + :param method: + :param refresh: + :return: float: Market spread in BASE + """ + # Todo: Insert logic here + + def get_own_spread(self, method, refresh=False): + """ Returns the difference between own closest opposite orders. + lowest_own_sell_price / highest_own_buy_price - 1 + + :param method: + :param refresh: + :return: + """ + # Todo: Insert logic here + + def get_order_creation_fee(self, fee_asset): + """ Returns the cost of creating an order in the asset specified + + :param fee_asset: QUOTE, BASE, BTS, or any other + :return: + """ + # Todo: Insert logic here + + def get_order_cancellation_fee(self, fee_asset): + """ Returns the order cancellation fee in the specified asset. + :param fee_asset: + :return: + """ + # Todo: Insert logic here + + def get_market_fee(self, asset): + """ Returns the fee percentage for buying specified asset. + :param asset: + :return: Fee percentage in decimal form (0.025) + """ + # Todo: Insert logic here + + def get_own_buy_orders(self, sort=None, orders=None): + """ Return own buy orders from list of orders. Can be used to pick buy orders from a list + that is not up to date with the blockchain data. + + :param string | sort: DESC or ASC will sort the orders accordingly, default None. + :param list | orders: List of orders. If None given get all orders from Blockchain. + :return list | buy_orders: List of buy orders only. + """ + buy_orders = [] + + if not orders: + orders = self.current_market_own_orders + + # Find buy orders + for order in orders: + if not self.is_sell_order(order): + buy_orders.append(order) + if sort: + buy_orders = self.sort_orders(buy_orders, sort) + + return buy_orders + + def get_own_sell_orders(self, sort=None, orders=None): + """ Return own sell orders from list of orders. Can be used to pick sell orders from a list + that is not up to date with the blockchain data. + + :param string | sort: DESC or ASC will sort the orders accordingly, default None. + :param list | orders: List of orders. If None given get all orders from Blockchain. + :return list | sell_orders: List of sell orders only. + """ + sell_orders = [] + + if not orders: + orders = self.current_market_own_orders + + # Find sell orders + for order in orders: + if self.is_sell_order(order): + sell_orders.append(order) + + if sort: + sell_orders = self.sort_orders(sell_orders, sort) + + return sell_orders + + def get_updated_order(self, order_id): + """ Tries to get the updated order from the API. Returns None if the order doesn't exist + + :param str|dict order_id: blockchain object id of the order + can be an order dict with the id key in it + """ + if isinstance(order_id, dict): + order_id = order_id['id'] + + # Get the limited order by id + order = None + for limit_order in self.account['limit_orders']: + if order_id == limit_order['id']: + order = limit_order + break + else: + return order + + order = self.get_updated_limit_order(order) + return Order(order, bitshares_instance=self.bitshares) + + def enhance_center_price(self, reference=None, manual_offset=False, balance_based_offset=False, + moving_average=0, weighted_average=0): + """ Returns the passed reference price shifted up or down based on arguments. + + :param float | reference: Center price to enhance + :param bool | manual_offset: + :param bool | balance_based_offset: + :param int or float | moving_average: + :param int or float | weighted_average: + :return: + """ + # Todo: Insert logic here + + def execute_bundle(self): + # Todo: Is this still needed? + # Apparently old naming was "execute", and was used by walls strategy. + """ Execute a bundle of operations + """ + self.bitshares.blocking = "head" + r = self.bitshares.txbuffer.broadcast() + self.bitshares.blocking = False + return r + + def is_buy_order(self, order): + """ Checks if the order is a buy order. Returns False if not. + + :param order: Buy / Sell order + :return: + """ + if order['base']['symbol'] == self.market['base']['symbol']: + return True + return False + + def is_sell_order(self, order): + """ Checks if the order is Sell order. Returns False if Buy order + + :param order: Buy / Sell order + :return: bool: True = Sell order, False = Buy order + """ + if order['base']['symbol'] != self.market['base']['symbol']: + return True + return False + + def is_current_market(self, base_asset_id, quote_asset_id): + """ Returns True if given asset id's are of the current market + + :return: bool: True = Current market, False = Not current market + """ + if quote_asset_id == self.market['quote']['id']: + if base_asset_id == self.market['base']['id']: + return True + return False + # Todo: Should we return true if market is opposite? + if quote_asset_id == self.market['base']['id']: + if base_asset_id == self.market['quote']['id']: + return True + return False + return False + + def pause_worker(self): + """ Pause the worker + + Note: By default, just call cancel_all(), strategies may override this method. + """ + # Cancel all orders from the market + self.cancel_all() + + # Removes worker's orders from local database + self.clear_orders() + + def purge_all_worker_data(self): + """ Clear all the worker data from the database and cancel all orders + """ + # Removes worker's orders from local database + self.clear_orders() + + # Cancel all orders from the market + self.cancel_all() + + # Finally clear all worker data from the database + self.clear() + + def place_market_buy_order(self, amount, price, return_none=False, *args, **kwargs): + """ Places a buy order in the market + + :param float | amount: Order amount in QUOTE + :param float | price: Order price in BASE + :param bool | return_none: + :param args: + :param kwargs: + :return: + """ + symbol = self.market['base']['symbol'] + precision = self.market['base']['precision'] + base_amount = truncate(price * amount, precision) + + # Don't try to place an order of size 0 + if not base_amount: + self.log.critical('Trying to buy 0') + self.disabled = True + return None + + # Make sure we have enough balance for the order + if self.balance(self.market['base']) < base_amount: + self.log.critical("Insufficient buy balance, needed {} {}".format(base_amount, symbol)) + self.disabled = True + return None + + self.log.info('Placing a buy order for {} {} @ {}'.format(base_amount, symbol, round(price, 8))) + + # Place the order + buy_transaction = self.retry_action( + self.market.buy, + price, + Amount(amount=amount, asset=self.market["quote"]), + account=self.account.name, + expiration=self.expiration, + returnOrderId="head", + fee_asset=self.fee_asset['id'], + *args, + **kwargs + ) + + self.log.debug('Placed buy order {}'.format(buy_transaction)) + buy_order = self.get_order(buy_transaction['orderid'], return_none=return_none) + if buy_order and buy_order['deleted']: + # The API doesn't return data on orders that don't exist + # We need to calculate the data on our own + buy_order = self.calculate_order_data(buy_order, amount, price) + self.recheck_orders = True + + return buy_order + + def place_market_sell_order(self, amount, price, return_none=False, *args, **kwargs): + """ Places a sell order in the market + + :param float | amount: Order amount in QUOTE + :param float | price: Order price in BASE + :param bool | return_none: + :param args: + :param kwargs: + :return: + """ + symbol = self.market['quote']['symbol'] + precision = self.market['quote']['precision'] + quote_amount = truncate(amount, precision) + + # Don't try to place an order of size 0 + if not quote_amount: + self.log.critical('Trying to sell 0') + self.disabled = True + return None + + # Make sure we have enough balance for the order + if self.balance(self.market['quote']) < quote_amount: + self.log.critical("Insufficient sell balance, needed {} {}".format(amount, symbol)) + self.disabled = True + return None + + self.log.info('Placing a sell order for {} {} @ {}'.format(quote_amount, symbol, round(price, 8))) + + # Place the order + sell_transaction = self.retry_action( + self.market.sell, + price, + Amount(amount=amount, asset=self.market["quote"]), + account=self.account.name, + expiration=self.expiration, + returnOrderId="head", + fee_asset=self.fee_asset['id'], + *args, + **kwargs + ) + + self.log.debug('Placed sell order {}'.format(sell_transaction)) + sell_order = self.get_order(sell_transaction['orderid'], return_none=return_none) + if sell_order and sell_order['deleted']: + # The API doesn't return data on orders that don't exist, we need to calculate the data on our own + sell_order = self.calculate_order_data(sell_order, amount, price) + sell_order.invert() + self.recheck_orders = True + + return sell_order + + def restore_order(self, order): + """ If an order is partially or completely filled, this will make a new order of original size and price. + + :param order: + :return: + """ + # Todo: Insert logic here + + def retry_action(self, action, *args, **kwargs): + """ Perform an action, and if certain suspected-to-be-spurious grapheme bugs occur, + instead of bubbling the exception, it is quietly logged (level WARN), and try again + tries a fixed number of times (MAX_TRIES) before failing + + :param action: + :return: + """ + tries = 0 + while True: + try: + return action(*args, **kwargs) + except bitsharesapi.exceptions.UnhandledRPCError as exception: + if "Assert Exception: amount_to_sell.amount > 0" in str(exception): + if tries > MAX_TRIES: + raise + else: + tries += 1 + self.log.warning("Ignoring: '{}'".format(str(exception))) + self.bitshares.txbuffer.clear() + self.account.refresh() + time.sleep(2) + elif "now <= trx.expiration" in str(exception): # Usually loss of sync to blockchain + if tries > MAX_TRIES: + raise + else: + tries += 1 + self.log.warning("retrying on '{}'".format(str(exception))) + self.bitshares.txbuffer.clear() + time.sleep(6) # Wait at least a BitShares block + else: + raise + + def write_order_log(self, worker_name, order): + """ + + :param string | worker_name: Name of the worker + :param object | order: Order that was traded + """ + # Todo: Add documentation + operation_type = 'TRADE' + + if order['base']['symbol'] == self.market['base']['symbol']: + base_symbol = order['base']['symbol'] + base_amount = -order['base']['amount'] + quote_symbol = order['quote']['symbol'] + quote_amount = order['quote']['amount'] + else: + base_symbol = order['quote']['symbol'] + base_amount = order['quote']['amount'] + quote_symbol = order['base']['symbol'] + quote_amount = -order['base']['amount'] + + message = '{};{};{};{};{};{};{};{}'.format( + worker_name, + order['id'], + operation_type, + base_symbol, + base_amount, + quote_symbol, + quote_amount, + datetime.datetime.now().isoformat() + ) + + self.orders_log.info(message) + + @property + def account(self): + """ Return the full account as :class:`bitshares.account.Account` object! + Can be refreshed by using ``x.refresh()`` + + :return: object | Account + """ + return self._account + + @property + def balances(self): + """ Returns all the balances of the account assigned for the worker. + + :return: list: Balances in list where each asset is in their own Amount object + """ + return self._account.balances + + @property + def all_own_orders(self, refresh=True): + """ Return the worker's open orders in all markets + + :param bool | refresh: Use most resent data + :return: list: List of Order objects + """ + # Refresh account data + if refresh: + self.account.refresh() + + return [order for order in self.account.openorders] + + @property + def current_market_own_orders(self, refresh=False): + """ Return the account's open orders in the current market + + :return: list: List of Order objects + """ + orders = [] + + # Refresh account data + if refresh: + self.account.refresh() + + for order in self.account.openorders: + if self.worker["market"] == order.market and self.account.openorders: + orders.append(order) + + return orders + + @property + def get_updated_orders(self): + """ Returns all open orders as updated orders + Todo: What exactly? When orders are needed who wants out of date info? + """ + self.account.refresh() + + limited_orders = [] + for order in self.account['limit_orders']: + base_asset_id = order['sell_price']['base']['asset_id'] + quote_asset_id = order['sell_price']['quote']['asset_id'] + # Check if the order is in the current market + if not self.is_current_market(base_asset_id, quote_asset_id): + continue + + limited_orders.append(self.get_updated_limit_order(order)) + + return [Order(order, bitshares_instance=self.bitshares) for order in limited_orders] + + @property + def market(self): + """ Return the market object as :class:`bitshares.market.Market` + """ + return self._market + + @staticmethod + def convert_asset(from_value, from_asset, to_asset, refresh=False): + """ Converts asset to another based on the latest market value + + :param float | from_value: Amount of the input asset + :param string | from_asset: Symbol of the input asset + :param string | to_asset: Symbol of the output asset + :param bool | refresh: + :return: float Asset converted to another asset as float value + """ + market = Market('{}/{}'.format(from_asset, to_asset)) + ticker = market.ticker() + latest_price = ticker.get('latest', {}).get('price', None) + return from_value * latest_price + + @staticmethod + def get_original_order(order_id, return_none=True): + """ Returns the Order object for the order_id + + :param dict | order_id: blockchain object id of the order can be an order dict with the id key in it + :param bool | return_none: return None instead of an empty Order object when the order doesn't exist + :return: + """ + if not order_id: + return None + + if 'id' in order_id: + order_id = order_id['id'] + + order = Order(order_id) + + if return_none and order['deleted']: + return None + + return order + + @staticmethod + def get_updated_limit_order(limit_order): + """ Returns a modified limit_order so that when passed to Order class, + will return an Order object with updated amount values + + :param limit_order: an item of Account['limit_orders'] + :return: Order + Todo: When would we not want an updated order? + """ + order = copy.deepcopy(limit_order) + price = order['sell_price']['base']['amount'] / order['sell_price']['quote']['amount'] + base_amount = order['for_sale'] + quote_amount = base_amount / price + order['sell_price']['base']['amount'] = base_amount + order['sell_price']['quote']['amount'] = quote_amount + return order + + @staticmethod + def purge_all_local_worker_data(worker_name): + # Todo: Confirm this being correct + """ Removes worker's data and orders from local sqlite database + + :param worker_name: Name of the worker to be removed + """ + Storage.clear_worker_data(worker_name) + + @staticmethod + def sort_orders(orders, sort='DESC'): + """ Return list of orders sorted ascending or descending + + :param list | orders: list of orders to be sorted + :param string | sort: ASC or DESC. Default DESC + :return list: Sorted list of orders + """ + if sort.upper() == 'ASC': + reverse = False + elif sort.upper() == 'DESC': + reverse = True + else: + return None + + # Sort orders by price + return sorted(orders, key=lambda order: order['price'], reverse=reverse) From d66bb04cbfc7814893f12d5897e8f3526cf23a5d Mon Sep 17 00:00:00 2001 From: joelva Date: Mon, 3 Sep 2018 15:32:21 +0300 Subject: [PATCH 02/42] WIP Add logic to functions --- dexbot/strategies/base.py | 185 +++++++++++++++++++++++++++----------- 1 file changed, 135 insertions(+), 50 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 154bcbbf8..aa71b38e4 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -129,13 +129,13 @@ def configure(cls, return_base_config=True): return [] def __init__(self, - worker_name, + name, config=None, - on_account=None, - on_order_matched=None, - on_order_placed=None, - on_market_update=None, - on_update_call_order=None, + onAccount=None, + onOrderMatched=None, + onOrderPlaced=None, + onMarketUpdate=None, + onUpdateCallOrder=None, ontick=None, bitshares_instance=None, *args, @@ -145,26 +145,26 @@ def __init__(self, self.bitshares = bitshares_instance or shared_bitshares_instance() # Storage - Storage.__init__(self, worker_name) + Storage.__init__(self, name) # Statemachine - StateMachine.__init__(self, worker_name) + StateMachine.__init__(self, name) # Events Events.__init__(self) if ontick: self.ontick += ontick - if on_market_update: - self.onMarketUpdate += on_market_update - if on_account: - self.onAccount += on_account - if on_order_matched: - self.onOrderMatched += on_order_matched - if on_order_placed: - self.onOrderPlaced += on_order_placed - if on_update_call_order: - self.onUpdateCallOrder += on_update_call_order + if onMarketUpdate: + self.onMarketUpdate += onMarketUpdate + if onAccount: + self.onAccount += onAccount + if onOrderMatched: + self.onOrderMatched += onOrderMatched + if onOrderPlaced: + self.onOrderPlaced += onOrderPlaced + if onUpdateCallOrder: + self.onUpdateCallOrder += onUpdateCallOrder # Redirect this event to also call order placed and order matched self.onMarketUpdate += self._callbackPlaceFillOrders @@ -172,14 +172,14 @@ def __init__(self, if config: self.config = config else: - self.config = config = Config.get_worker_config_file(worker_name) + self.config = config = Config.get_worker_config_file(name) # Get worker's parameters from the config - self.worker = config["workers"][worker_name] + self.worker = config["workers"][name] # Get Bitshares account and market for this worker self._account = Account(self.worker["account"], full=True, bitshares_instance=self.bitshares) - self._market = Market(config["workers"][worker_name]["market"], bitshares_instance=self.bitshares) + self._market = Market(config["workers"][name]["market"], bitshares_instance=self.bitshares) # Recheck flag - Tell the strategy to check for updated orders self.recheck_orders = False @@ -209,7 +209,7 @@ def __init__(self, self.log = logging.LoggerAdapter( logging.getLogger('dexbot.per_worker'), { - 'worker_name': worker_name, + 'worker_name': name, 'account': self.worker['account'], 'market': self.worker['market'], 'is_disabled': lambda: self.disabled @@ -492,21 +492,37 @@ def get_allocated_assets(self, order_ids, return_asset=False, refresh=True): return {'quote': quote, 'base': base} - def get_lowest_market_sell(self, refresh=False): + def get_lowest_market_sell(self): """ Returns the lowest sell order that is not own, regardless of order size. - :param refresh: - :return: + :return: order or None: Lowest market sell order. """ - # Todo: Insert logic here + orders = self.market.orderbook(1) - def get_highest_market_buy(self, refresh=False): - """ Returns the highest buy order not owned by worker account, regardless of order size. + try: + order = orders['asks'][0] + self.log.info('Lowest market ask @ {}'.format(order.get('price'))) + except IndexError: + self.log.info('Market has no lowest ask.') + return None - :param refresh: - :return: + return order + + def get_highest_market_buy(self): + """ Returns the highest buy order that is not own, regardless of order size. + + :return: order or None: Highest market buy order. """ - # Todo: Insert logic here + orders = self.market.orderbook(1) + + try: + order = orders['bids'][0] + self.log.info('Highest market bid @ {}'.format(order.get('price'))) + except IndexError: + self.log.info('Market has no highest bid.') + return None + + return order def get_lowest_own_sell(self, refresh=False): """ Returns lowest own sell order. @@ -543,42 +559,107 @@ def get_price_for_amount_sell(self, amount=None, refresh=False): """ # Todo: Insert logic here - def get_market_center_price(self, depth=0, refresh=False): + def get_external_price(self, source): """ Returns the center price of market including own orders. - :param depth: 0 = calculate from closest opposite orders. non-zero = calculate from specified depth (quote or base?) - :param refresh: + :param source: :return: """ # Todo: Insert logic here - def get_external_price(self, source): - """ Returns the center price of market including own orders. + def get_market_ask(self, depth=0, moving_average=0, weighted_moving_average=0, refresh=False): + """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with moving average + or weighted moving average - :param source: + :param float | depth: + :param float | moving_average: + :param float | weighted_moving_average: + :param bool | refresh: :return: """ # Todo: Insert logic here - def get_market_spread(self, method, refresh=False): - """ Get spread from closest opposite orders, including own. + def get_market_bid(self, depth=0, moving_average=0, weighted_moving_average=0, refresh=False): + """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be sold, enhanced with moving average or + weighted moving average. - :param method: - :param refresh: - :return: float: Market spread in BASE + Depth = 0 means highest regardless of size + + :param float | depth: + :param float | moving_average: + :param float | weighted_moving_average: + :param bool | refresh: + :return: """ # Todo: Insert logic here - def get_own_spread(self, method, refresh=False): - """ Returns the difference between own closest opposite orders. - lowest_own_sell_price / highest_own_buy_price - 1 + def get_market_center_price(self, depth=0, refresh=False): + """ Returns the center price of market including own orders. - :param method: - :param refresh: + Depth: 0 = calculate from closest opposite orders. + Depth: non-zero = calculate from specified depth + + :param float | depth: + :param bool | refresh: :return: """ # Todo: Insert logic here + def get_market_spread(self, highest_market_buy_price=None, lowest_market_sell_price=None, + depth=0, refresh=False): + """ Returns the market spread %, including own orders, from specified depth, enhanced with moving average or + weighted moving average + + :param float | highest_market_buy_price: + :param float | lowest_market_sell_price: + :param float | depth: + :param bool | refresh: Use most resent data from Bitshares + :return: float or None: Market spread + """ + # Todo: Add depth + if refresh: + try: + # Try fetching orders from market + highest_market_buy_price = self.get_highest_own_buy().get('price') + lowest_market_sell_price = self.get_highest_own_buy().get('price') + except AttributeError: + # This error is given if there is no market buy or sell order + return None + else: + # If orders are given, use them instead newest data from the blockchain + highest_market_buy_price = highest_market_buy_price + lowest_market_sell_price = lowest_market_sell_price + + # Calculate market spread + market_spread = lowest_market_sell_price / highest_market_buy_price - 1 + return market_spread + + def get_own_spread(self, highest_own_buy_price=None, lowest_own_sell_price=None, depth=0, refresh=False): + """ Returns the difference between own closest opposite orders. + + :param float | highest_own_buy_price: + :param float | lowest_own_sell_price: + :param float | depth: Use most resent data from Bitshares + :param bool | refresh: + :return: float or None: Own spread + """ + # Todo: Add depth + if refresh: + try: + # Try fetching own orders + highest_own_buy_price = self.get_highest_market_buy().get('price') + lowest_own_sell_price = self.get_lowest_own_sell().get('price') + except AttributeError: + return None + else: + # If orders are given, use them instead newest data from the blockchain + highest_own_buy_price = highest_own_buy_price + lowest_own_sell_price = lowest_own_sell_price + + # Calculate actual spread + actual_spread = lowest_own_sell_price / highest_own_buy_price - 1 + return actual_spread + def get_order_creation_fee(self, fee_asset): """ Returns the cost of creating an order in the asset specified @@ -602,8 +683,10 @@ def get_market_fee(self, asset): # Todo: Insert logic here def get_own_buy_orders(self, sort=None, orders=None): + # Todo: I might combine this with the get_own_sell_orders and have 2 functions to call it with different returns """ Return own buy orders from list of orders. Can be used to pick buy orders from a list - that is not up to date with the blockchain data. + that is not up to date with the blockchain data. If list of orders is not passed, orders are fetched from + blockchain. :param string | sort: DESC or ASC will sort the orders accordingly, default None. :param list | orders: List of orders. If None given get all orders from Blockchain. @@ -625,7 +708,8 @@ def get_own_buy_orders(self, sort=None, orders=None): def get_own_sell_orders(self, sort=None, orders=None): """ Return own sell orders from list of orders. Can be used to pick sell orders from a list - that is not up to date with the blockchain data. + that is not up to date with the blockchain data. If list of orders is not passed, orders are fetched from + blockchain. :param string | sort: DESC or ASC will sort the orders accordingly, default None. :param list | orders: List of orders. If None given get all orders from Blockchain. @@ -647,6 +731,7 @@ def get_own_sell_orders(self, sort=None, orders=None): return sell_orders def get_updated_order(self, order_id): + # Todo: This needed? """ Tries to get the updated order from the API. Returns None if the order doesn't exist :param str|dict order_id: blockchain object id of the order @@ -729,7 +814,7 @@ def is_current_market(self, base_asset_id, quote_asset_id): def pause_worker(self): """ Pause the worker - Note: By default, just call cancel_all(), strategies may override this method. + Note: By default pause cancels orders, but this can be overridden by strategy """ # Cancel all orders from the market self.cancel_all() From b1b119b17e9fc7533586acdf4813a14e71ffa9ed Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 6 Sep 2018 10:29:08 +0300 Subject: [PATCH 03/42] Add documentation to truncate() in helper.py --- dexbot/helper.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dexbot/helper.py b/dexbot/helper.py index 8812cd7c4..832ddba6e 100644 --- a/dexbot/helper.py +++ b/dexbot/helper.py @@ -35,6 +35,10 @@ def remove(path): def truncate(number, decimals): """ Change the decimal point of a number without rounding + + :param float | number: A float number to be cut down + :param int | decimals: Number of decimals to be left to the float number + :return: Price with specified precision """ return math.floor(number * 10 ** decimals) / 10 ** decimals From 9cccdd87e35ea431e685b18b5537858996531d54 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 6 Sep 2018 10:31:43 +0300 Subject: [PATCH 04/42] Refactor double to single quotes in ConfigElemet --- dexbot/strategies/base.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index aa71b38e4..f34a7d3e1 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -112,12 +112,12 @@ def configure(cls, return_base_config=True): # Common configs base_config = [ - ConfigElement("account", "string", "", "Account", - "BitShares account name for the bot to operate with", - ""), - ConfigElement("market", "string", "USD:BTS", "Market", - "BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"", - r"[A-Z\.]+[:\/][A-Z\.]+"), + ConfigElement('account', 'string', '', 'Account', + 'BitShares account name for the bot to operate with', + ''), + ConfigElement('market', 'string', 'USD:BTS', 'Market', + 'BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"', + r'[A-Z\.]+[:\/][A-Z\.]+'), ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset', 'Asset to be used to pay transaction fees', r'[A-Z\.]+') From d2ab2e482f08ef1b50ad6deca02841eda1fd37b3 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 6 Sep 2018 10:33:01 +0300 Subject: [PATCH 05/42] WIP Change base.py functions and structure --- dexbot/strategies/base.py | 466 ++++++++++++++++++++++---------------- 1 file changed, 272 insertions(+), 194 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index f34a7d3e1..70d62be84 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -38,7 +38,7 @@ :string: a regular expression, entries must match it, can be None which equivalent to .* :bool, ignored :choice: a list of choices, choices are in turn (tag, label) tuples. - labels get presented to user, and tag is used as the value saved back to the config dict + NOTE: 'labels' get presented to user, and 'tag' is used as the value saved back to the config dict! """ ConfigElement = collections.namedtuple('ConfigElement', 'key type default title description extra') @@ -184,6 +184,9 @@ def __init__(self, # Recheck flag - Tell the strategy to check for updated orders self.recheck_orders = False + # Count of orders to be fetched from the API + self.fetch_depth = 8 + # Set fee asset fee_asset_symbol = self.worker.get('fee_asset') @@ -323,10 +326,12 @@ def account_total_value(self, return_asset): def balance(self, asset, fee_reservation=False): """ Return the balance of your worker's account for a specific asset + :param string | asset: :param bool | fee_reservation: :return: Balance of specific asset """ # Todo: Add documentation + # Todo: Add logic here, fee_reservation return self._account.balance(asset) def calculate_center_price(self, center_price=None, asset_offset=False, spread=None, @@ -492,147 +497,283 @@ def get_allocated_assets(self, order_ids, return_asset=False, refresh=True): return {'quote': quote, 'base': base} - def get_lowest_market_sell(self): - """ Returns the lowest sell order that is not own, regardless of order size. + def get_external_price(self, source): + """ Returns the center price of market including own orders. - :return: order or None: Lowest market sell order. + :param source: + :return: """ - orders = self.market.orderbook(1) + # Todo: Insert logic here - try: - order = orders['asks'][0] - self.log.info('Lowest market ask @ {}'.format(order.get('price'))) - except IndexError: - self.log.info('Market has no lowest ask.') - return None + def get_market_fee(self): + """ Returns the fee percentage for buying specified asset - return order + :return: Fee percentage in decimal form (0.025) + """ + return self.fee_asset.market_fee_percent - def get_highest_market_buy(self): + def get_market_buy_orders(self): + """ + + :return: List of market buy orders + """ + return self.get_market_orders()['bids'] + + def get_market_sell_orders(self, depth=1): + """ + + :return: List of market sell orders + """ + return self.get_market_orders(depth=depth)['asks'] + + def get_highest_market_buy_order(self, orders=None): """ Returns the highest buy order that is not own, regardless of order size. - :return: order or None: Highest market buy order. + :param list | orders: Optional list of orders, if none given fetch newest from market + :return: Highest market buy order or None """ - orders = self.market.orderbook(1) + if not orders: + orders = self.market.orderbook(1) try: order = orders['bids'][0] - self.log.info('Highest market bid @ {}'.format(order.get('price'))) except IndexError: - self.log.info('Market has no highest bid.') + self.log.info('Market has no buy orders.') return None return order - def get_lowest_own_sell(self, refresh=False): - """ Returns lowest own sell order. + def get_highest_own_buy(self, orders=None): + """ Returns highest own buy order. - :param refresh: - :return: + :param list | orders: + :return: Highest own buy order by price at the market or None """ - # Todo: Insert logic here + if not orders: + orders = self.get_own_buy_orders() - def get_highest_own_buy(self, refresh=False): - """ Returns highest own buy order. + try: + return orders[0] + except IndexError: + return None - :param refresh: - :return: + def get_lowest_market_sell_order(self, orders=None): + """ Returns the lowest sell order that is not own, regardless of order size. + + :param list | orders: Optional list of orders, if none given fetch newest from market + :return: Lowest market sell order or None """ - # Todo: Insert logic here + if not orders: + orders = self.market.orderbook(1) - def get_price_for_amount_buy(self, amount=None, refresh=False): - """ Returns the cumulative price for which you could buy the specified amount of QUOTE. - This method must take into account market fee. + try: + order = orders['asks'][0] + except IndexError: + self.log.info('Market has no sell orders.') + return None - :param amount: - :param refresh: - :return: - """ - # Todo: Insert logic here + return order - def get_price_for_amount_sell(self, amount=None, refresh=False): - """ Returns the cumulative price for which you could sell the specified amount of QUOTE + def get_lowest_own_sell_order(self, orders=None): + """ Returns lowest own sell order. - :param amount: - :param refresh: - :return: + :param list | orders: + :return: Lowest own sell order by price at the market """ - # Todo: Insert logic here + if not orders: + orders = self.get_own_sell_orders() - def get_external_price(self, source): + try: + return orders[0] + except IndexError: + return None + + def get_market_center_price(self, quote_amount=0, refresh=False): """ Returns the center price of market including own orders. - :param source: + Depth: 0 = calculate from closest opposite orders. + Depth: non-zero = calculate from specified depth + + :param float | quote_amount: + :param bool | refresh: :return: """ # Todo: Insert logic here - def get_market_ask(self, depth=0, moving_average=0, weighted_moving_average=0, refresh=False): - """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with moving average - or weighted moving average + def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0, + refresh=False): + """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with + moving average or weighted moving average - :param float | depth: + :param float | quote_amount: + :param float | base_amount: :param float | moving_average: :param float | weighted_moving_average: :param bool | refresh: :return: """ - # Todo: Insert logic here + # Todo: Logic here - def get_market_bid(self, depth=0, moving_average=0, weighted_moving_average=0, refresh=False): - """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be sold, enhanced with moving average or - weighted moving average. + def get_market_orders(self, depth=1): + """ Returns orders from the current market split in bids and asks. Orders are sorted by price. + + bids = buy orders + asks = sell orders + + :param int | depth: Amount of orders per side will be fetched, default=1 + :return: Returns a dictionary of orders or None + """ + return self.market.orderbook(depth) + + def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0, + refresh=False): + """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be sold, enhanced with moving + average or weighted moving average. Depth = 0 means highest regardless of size - :param float | depth: + :param float | quote_amount: + :param float | base_amount: :param float | moving_average: :param float | weighted_moving_average: :param bool | refresh: :return: """ - # Todo: Insert logic here + # In case depth is not given, return price of the lowest sell order on the market + if depth == 0: + lowest_market_sell_order = self.get_lowest_market_sell_order() + return lowest_market_sell_order['price'] + + sum_quote = 0 + sum_base = 0 + order_number = 0 + + market_sell_orders = self.get_market_sell_orders(depth=depth) + market_fee = self.get_market_fee() + lacking = depth * (1 + market_fee) + + while lacking > 0: + sell_quote = float(market_sell_orders[order_number]['quote']) + + if sell_quote > lacking: + sum_quote += lacking + sum_base += lacking * market_sell_orders[order_number]['price'] # Make sure price is not inverted + lacking = 0 + else: + sum_quote += float(market_sell_orders[order_number]['quote']) + sum_base += float(market_sell_orders[order_number]['base']) + lacking = depth - sell_quote - def get_market_center_price(self, depth=0, refresh=False): - """ Returns the center price of market including own orders. + price = sum_base / sum_quote - Depth: 0 = calculate from closest opposite orders. - Depth: non-zero = calculate from specified depth + return price - :param float | depth: - :param bool | refresh: + def get_market_spread(self, quote_amount=0): + """ Returns the market spread %, including own orders, from specified depth, enhanced with moving average or + weighted moving average. + + :param int | quote_amount: + :return: Market spread as float or None + """ + # Decides how many orders need to be fetched for the market spread calculation + if quote_amount == 0: + # Get only the closest orders + fetch_depth = 1 + elif quote_amount > 0: + fetch_depth = self.fetch_depth + + # Raise the fetch depth each time. int() rounds the count + self.fetch_depth = int(self.fetch_depth * 1.5) + + market_orders = self.get_market_orders(fetch_depth) + + ask = self.get_market_ask() + bid = self.get_market_bid() + + # Calculate market spread + market_spread = ask / bid - 1 + + return market_spread + + def get_order_cancellation_fee(self, fee_asset): + """ Returns the order cancellation fee in the specified asset. + :param fee_asset: :return: """ # Todo: Insert logic here - def get_market_spread(self, highest_market_buy_price=None, lowest_market_sell_price=None, - depth=0, refresh=False): - """ Returns the market spread %, including own orders, from specified depth, enhanced with moving average or - weighted moving average + def get_order_creation_fee(self, fee_asset): + """ Returns the cost of creating an order in the asset specified - :param float | highest_market_buy_price: - :param float | lowest_market_sell_price: - :param float | depth: - :param bool | refresh: Use most resent data from Bitshares - :return: float or None: Market spread + :param fee_asset: QUOTE, BASE, BTS, or any other + :return: """ - # Todo: Add depth - if refresh: - try: - # Try fetching orders from market - highest_market_buy_price = self.get_highest_own_buy().get('price') - lowest_market_sell_price = self.get_highest_own_buy().get('price') - except AttributeError: - # This error is given if there is no market buy or sell order - return None - else: - # If orders are given, use them instead newest data from the blockchain - highest_market_buy_price = highest_market_buy_price - lowest_market_sell_price = lowest_market_sell_price + # Todo: Insert logic here - # Calculate market spread - market_spread = lowest_market_sell_price / highest_market_buy_price - 1 - return market_spread + def filter_buy_orders(self, orders, sort=None): + """ Return own buy orders from list of orders. Can be used to pick buy orders from a list + that is not up to date with the blockchain data. + + :param list | orders: List of orders + :param string | sort: DESC or ASC will sort the orders accordingly, default None + :return list | buy_orders: List of buy orders only + """ + buy_orders = [] + + # Filter buy orders + for order in orders: + # Check if the order is buy order, by comparing asset symbol of the order and the market + if order['base']['symbol'] == self.market['base']['symbol']: + buy_orders.append(order) + + if sort: + buy_orders = self.sort_orders_by_price(buy_orders, sort) + + return buy_orders + + def filter_sell_orders(self, orders, sort=None): + """ Return sell orders from list of orders. Can be used to pick sell orders from a list + that is not up to date with the blockchain data. + + :param list | orders: List of orders + :param string | sort: DESC or ASC will sort the orders accordingly, default None + :return list | sell_orders: List of sell orders only + """ + sell_orders = [] + + # Filter sell orders + for order in orders: + # Check if the order is buy order, by comparing asset symbol of the order and the market + if order['base']['symbol'] != self.market['base']['symbol']: + # Invert order before appending to the list, this gives easier comparison in strategy logic + sell_orders.append(order.invert()) + + if sort: + sell_orders = self.sort_orders_by_price(sell_orders, sort) + + return sell_orders + + def get_own_buy_orders(self, orders=None): + """ Get own buy orders from current market + + :return: List of buy orders + """ + if not orders: + # List of orders was not given so fetch everything from the market + orders = self.current_market_own_orders + + return self.filter_buy_orders(orders) + + def get_own_sell_orders(self, orders=None): + """ Get own sell orders from current market + + :return: List of sell orders + """ + if not orders: + # List of orders was not given so fetch everything from the market + orders = self.current_market_own_orders + + return self.filter_sell_orders(orders) def get_own_spread(self, highest_own_buy_price=None, lowest_own_sell_price=None, depth=0, refresh=False): """ Returns the difference between own closest opposite orders. @@ -647,8 +788,8 @@ def get_own_spread(self, highest_own_buy_price=None, lowest_own_sell_price=None, if refresh: try: # Try fetching own orders - highest_own_buy_price = self.get_highest_market_buy().get('price') - lowest_own_sell_price = self.get_lowest_own_sell().get('price') + highest_own_buy_price = self.get_highest_market_buy_order().get('price') + lowest_own_sell_price = self.get_lowest_own_sell_order().get('price') except AttributeError: return None else: @@ -660,76 +801,23 @@ def get_own_spread(self, highest_own_buy_price=None, lowest_own_sell_price=None, actual_spread = lowest_own_sell_price / highest_own_buy_price - 1 return actual_spread - def get_order_creation_fee(self, fee_asset): - """ Returns the cost of creating an order in the asset specified + def get_price_for_amount_buy(self, amount=None): + """ Returns the cumulative price for which you could buy the specified amount of QUOTE. + This method must take into account market fee. - :param fee_asset: QUOTE, BASE, BTS, or any other + :param amount: :return: """ # Todo: Insert logic here - def get_order_cancellation_fee(self, fee_asset): - """ Returns the order cancellation fee in the specified asset. - :param fee_asset: - :return: - """ - # Todo: Insert logic here + def get_price_for_amount_sell(self, amount=None): + """ Returns the cumulative price for which you could sell the specified amount of QUOTE - def get_market_fee(self, asset): - """ Returns the fee percentage for buying specified asset. - :param asset: - :return: Fee percentage in decimal form (0.025) + :param amount: + :return: """ # Todo: Insert logic here - def get_own_buy_orders(self, sort=None, orders=None): - # Todo: I might combine this with the get_own_sell_orders and have 2 functions to call it with different returns - """ Return own buy orders from list of orders. Can be used to pick buy orders from a list - that is not up to date with the blockchain data. If list of orders is not passed, orders are fetched from - blockchain. - - :param string | sort: DESC or ASC will sort the orders accordingly, default None. - :param list | orders: List of orders. If None given get all orders from Blockchain. - :return list | buy_orders: List of buy orders only. - """ - buy_orders = [] - - if not orders: - orders = self.current_market_own_orders - - # Find buy orders - for order in orders: - if not self.is_sell_order(order): - buy_orders.append(order) - if sort: - buy_orders = self.sort_orders(buy_orders, sort) - - return buy_orders - - def get_own_sell_orders(self, sort=None, orders=None): - """ Return own sell orders from list of orders. Can be used to pick sell orders from a list - that is not up to date with the blockchain data. If list of orders is not passed, orders are fetched from - blockchain. - - :param string | sort: DESC or ASC will sort the orders accordingly, default None. - :param list | orders: List of orders. If None given get all orders from Blockchain. - :return list | sell_orders: List of sell orders only. - """ - sell_orders = [] - - if not orders: - orders = self.current_market_own_orders - - # Find sell orders - for order in orders: - if self.is_sell_order(order): - sell_orders.append(order) - - if sort: - sell_orders = self.sort_orders(sell_orders, sort) - - return sell_orders - def get_updated_order(self, order_id): # Todo: This needed? """ Tries to get the updated order from the API. Returns None if the order doesn't exist @@ -775,26 +863,6 @@ def execute_bundle(self): self.bitshares.blocking = False return r - def is_buy_order(self, order): - """ Checks if the order is a buy order. Returns False if not. - - :param order: Buy / Sell order - :return: - """ - if order['base']['symbol'] == self.market['base']['symbol']: - return True - return False - - def is_sell_order(self, order): - """ Checks if the order is Sell order. Returns False if Buy order - - :param order: Buy / Sell order - :return: bool: True = Sell order, False = Buy order - """ - if order['base']['symbol'] != self.market['base']['symbol']: - return True - return False - def is_current_market(self, base_asset_id, quote_asset_id): """ Returns True if given asset id's are of the current market @@ -1023,28 +1091,40 @@ def account(self): def balances(self): """ Returns all the balances of the account assigned for the worker. - :return: list: Balances in list where each asset is in their own Amount object + :return: Balances in list where each asset is in their own Amount object """ return self._account.balances + @property + def base_asset(self): + return self.worker['market'].split('/')[1] + + @property + def quote_asset(self): + return self.worker['market'].split('/')[0] + @property def all_own_orders(self, refresh=True): """ Return the worker's open orders in all markets :param bool | refresh: Use most resent data - :return: list: List of Order objects + :return: List of Order objects """ # Refresh account data if refresh: self.account.refresh() - return [order for order in self.account.openorders] + orders = [] + for order in self.account.openorders: + orders.append(order) + + return orders @property def current_market_own_orders(self, refresh=False): """ Return the account's open orders in the current market - :return: list: List of Order objects + :return: List of Order objects """ orders = [] @@ -1058,25 +1138,6 @@ def current_market_own_orders(self, refresh=False): return orders - @property - def get_updated_orders(self): - """ Returns all open orders as updated orders - Todo: What exactly? When orders are needed who wants out of date info? - """ - self.account.refresh() - - limited_orders = [] - for order in self.account['limit_orders']: - base_asset_id = order['sell_price']['base']['asset_id'] - quote_asset_id = order['sell_price']['quote']['asset_id'] - # Check if the order is in the current market - if not self.is_current_market(base_asset_id, quote_asset_id): - continue - - limited_orders.append(self.get_updated_limit_order(order)) - - return [Order(order, bitshares_instance=self.bitshares) for order in limited_orders] - @property def market(self): """ Return the market object as :class:`bitshares.market.Market` @@ -1084,13 +1145,12 @@ def market(self): return self._market @staticmethod - def convert_asset(from_value, from_asset, to_asset, refresh=False): + def convert_asset(from_value, from_asset, to_asset): """ Converts asset to another based on the latest market value :param float | from_value: Amount of the input asset :param string | from_asset: Symbol of the input asset :param string | to_asset: Symbol of the output asset - :param bool | refresh: :return: float Asset converted to another asset as float value """ market = Market('{}/{}'.format(from_asset, to_asset)) @@ -1098,6 +1158,24 @@ def convert_asset(from_value, from_asset, to_asset, refresh=False): latest_price = ticker.get('latest', {}).get('price', None) return from_value * latest_price + @staticmethod + def get_order(order_id, return_none=True): + """ Returns the Order object for the order_id + + :param str|dict order_id: blockchain object id of the order + can be an order dict with the id key in it + :param bool return_none: return None instead of an empty + Order object when the order doesn't exist + """ + if not order_id: + return None + if 'id' in order_id: + order_id = order_id['id'] + order = Order(order_id) + if return_none and order['deleted']: + return None + return order + @staticmethod def get_original_order(order_id, return_none=True): """ Returns the Order object for the order_id @@ -1146,8 +1224,8 @@ def purge_all_local_worker_data(worker_name): Storage.clear_worker_data(worker_name) @staticmethod - def sort_orders(orders, sort='DESC'): - """ Return list of orders sorted ascending or descending + def sort_orders_by_price(orders, sort='DESC'): + """ Return list of orders sorted ascending or descending by price :param list | orders: list of orders to be sorted :param string | sort: ASC or DESC. Default DESC From bc4cdce7202b9f7d4274e3544a7e14e12ca3497f Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 6 Sep 2018 10:48:48 +0300 Subject: [PATCH 06/42] Refactor get market buy and sell orders --- dexbot/strategies/base.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 70d62be84..ffaff3ce3 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -512,16 +512,18 @@ def get_market_fee(self): """ return self.fee_asset.market_fee_percent - def get_market_buy_orders(self): - """ + def get_market_buy_orders(self, depth=10): + """ Fetches most reset data and returns list of buy orders. - :return: List of market buy orders + :param int | depth: Amount of buy orders returned, Default=10 + :return: List of market sell orders """ - return self.get_market_orders()['bids'] + return self.get_market_orders(depth=depth)['bids'] - def get_market_sell_orders(self, depth=1): - """ + def get_market_sell_orders(self, depth=10): + """ Fetches most reset data and returns list of sell orders. + :param int | depth: Amount of sell orders returned, Default=10 :return: List of market sell orders """ return self.get_market_orders(depth=depth)['asks'] @@ -533,10 +535,10 @@ def get_highest_market_buy_order(self, orders=None): :return: Highest market buy order or None """ if not orders: - orders = self.market.orderbook(1) + orders = self.get_market_buy_orders(1) try: - order = orders['bids'][0] + order = orders[0] except IndexError: self.log.info('Market has no buy orders.') return None @@ -564,10 +566,10 @@ def get_lowest_market_sell_order(self, orders=None): :return: Lowest market sell order or None """ if not orders: - orders = self.market.orderbook(1) + orders = self.get_market_sell_orders(1) try: - order = orders['asks'][0] + order = orders[0] except IndexError: self.log.info('Market has no sell orders.') return None From ad6b57ce0b0cf4208c653f5178d92a8f8d2edd60 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 6 Sep 2018 12:40:31 +0300 Subject: [PATCH 07/42] Remove execute_bundle() from base.py --- dexbot/strategies/base.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index ffaff3ce3..0f2b0607b 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -855,16 +855,6 @@ def enhance_center_price(self, reference=None, manual_offset=False, balance_base """ # Todo: Insert logic here - def execute_bundle(self): - # Todo: Is this still needed? - # Apparently old naming was "execute", and was used by walls strategy. - """ Execute a bundle of operations - """ - self.bitshares.blocking = "head" - r = self.bitshares.txbuffer.broadcast() - self.bitshares.blocking = False - return r - def is_current_market(self, base_asset_id, quote_asset_id): """ Returns True if given asset id's are of the current market From af6c7ed40280b09d5ccf8d39ff698d160c26af3c Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 7 Sep 2018 13:26:14 +0300 Subject: [PATCH 08/42] Add calculate_worker_value() to base.py --- dexbot/strategies/base.py | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 0f2b0607b..a2c31ae05 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -385,14 +385,41 @@ def calculate_order_data(self, order, amount, price): order['base'] = base_asset return order - def calculate_worker_value(self, unit_of_measure, refresh=True): - """ Returns the combined value of allocated and available QUOTE and BASE, measured in "unit_of_measure". + def calculate_worker_value(self, unit_of_measure): + """ Returns the combined value of allocated and available BASE and QUOTE. Total value is + measured in "unit_of_measure", which is either BASE or QUOTE symbol. - :param unit_of_measure: - :param refresh: - :return: + :param string | unit_of_measure: Asset symbol + :return: Value of the worker as float """ - # Todo: Insert logic here + base_total = 0 + quote_total = 0 + + # Calculate total balances + balances = self.balances + for balance in balances: + if balance['symbol'] == self.base_asset: + base_total += balance['amount'] + elif balance['symbol'] == self.quote_asset: + quote_total += balance['amount'] + + # Calculate value of the orders in unit of measure + orders = self.current_market_own_orders + for order in orders: + if order['base']['symbol'] == self.quote_asset: + # Pick sell orders order's BASE amount, which is same as worker's QUOTE, to worker's BASE + quote_total += order['base']['amount'] + else: + base_total += order['base']['amount'] + + # Finally convert asset to another and return the sum + if unit_of_measure == self.base_asset: + quote_total = self.convert_asset(quote_total, self.quote_asset, unit_of_measure) + elif unit_of_measure == self.quote_asset: + base_total = self.convert_asset(base_total, self.base_asset, unit_of_measure) + + # Fixme: Make sure that decimal precision is correct. + return base_total + quote_total def cancel_all_orders(self): """ Cancel all orders of the worker's account From 3f0fa5c91c3c86f409c1cbddc4b1228b4e4132de Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 7 Sep 2018 13:26:42 +0300 Subject: [PATCH 09/42] Add fee_reservation parameter to balance() --- dexbot/strategies/base.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index a2c31ae05..62948c235 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -323,7 +323,7 @@ def account_total_value(self, return_asset): return total_value - def balance(self, asset, fee_reservation=False): + def balance(self, asset, fee_reservation=0): """ Return the balance of your worker's account for a specific asset :param string | asset: @@ -331,8 +331,13 @@ def balance(self, asset, fee_reservation=False): :return: Balance of specific asset """ # Todo: Add documentation - # Todo: Add logic here, fee_reservation - return self._account.balance(asset) + # Todo: Check that fee reservation was as intended, having it true / false made no sense + balance = self._account.balance(asset) + + if fee_reservation > 0: + balance['amount'] = balance['amount'] - fee_reservation + + return balance def calculate_center_price(self, center_price=None, asset_offset=False, spread=None, order_ids=None, manual_offset=0, suppress_errors=False): From b58a9e9feeab7081e40a3ee6e9e3f65ed5840f36 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 7 Sep 2018 13:53:43 +0300 Subject: [PATCH 10/42] Remove duplicate function for getting order --- dexbot/strategies/base.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 62948c235..fc681a088 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1182,23 +1182,6 @@ def convert_asset(from_value, from_asset, to_asset): latest_price = ticker.get('latest', {}).get('price', None) return from_value * latest_price - @staticmethod - def get_order(order_id, return_none=True): - """ Returns the Order object for the order_id - - :param str|dict order_id: blockchain object id of the order - can be an order dict with the id key in it - :param bool return_none: return None instead of an empty - Order object when the order doesn't exist - """ - if not order_id: - return None - if 'id' in order_id: - order_id = order_id['id'] - order = Order(order_id) - if return_none and order['deleted']: - return None - return order @staticmethod def get_original_order(order_id, return_none=True): From 9e386c7637d57359653b136a316b1d8221d273fc Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 7 Sep 2018 13:55:16 +0300 Subject: [PATCH 11/42] Remove old calculate center price method --- dexbot/strategies/base.py | 72 --------------------------------------- 1 file changed, 72 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index fc681a088..f899744bb 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -223,35 +223,6 @@ def __init__(self, logging.getLogger('dexbot.orders_log'), {} ) - def _calculate_center_price(self, suppress_errors=False): - """ - - :param suppress_errors: - :return: - """ - # Todo: Add documentation - ticker = self.market.ticker() - highest_bid = ticker.get("highestBid") - lowest_ask = ticker.get("lowestAsk") - - if highest_bid is None or highest_bid == 0.0: - if not suppress_errors: - self.log.critical( - "Cannot estimate center price, there is no highest bid." - ) - self.disabled = True - return None - elif lowest_ask is None or lowest_ask == 0.0: - if not suppress_errors: - self.log.critical( - "Cannot estimate center price, there is no lowest ask." - ) - self.disabled = True - return None - - center_price = highest_bid['price'] * math.sqrt(lowest_ask['price'] / highest_bid['price']) - return center_price - def _callbackPlaceFillOrders(self, d): """ This method distinguishes notifications caused by Matched orders from those caused by placed orders Todo: can this be renamed to _instantFill()? @@ -339,49 +310,6 @@ def balance(self, asset, fee_reservation=0): return balance - def calculate_center_price(self, center_price=None, asset_offset=False, spread=None, - order_ids=None, manual_offset=0, suppress_errors=False): - # Todo: Fix comment - """ Calculate center price which shifts based on available funds - """ - if center_price is None: - # No center price was given so we simply calculate the center price - calculated_center_price = self._calculate_center_price(suppress_errors) - else: - # Center price was given so we only use the calculated center price - # for quote to base asset conversion - calculated_center_price = self._calculate_center_price(True) - if not calculated_center_price: - calculated_center_price = center_price - - if center_price: - calculated_center_price = center_price - - if asset_offset: - total_balance = self.get_allocated_assets(order_ids) - total = (total_balance['quote'] * calculated_center_price) + total_balance['base'] - - if not total: # Prevent division by zero - balance = 0 - else: - # Returns a value between -1 and 1 - balance = (total_balance['base'] / total) * 2 - 1 - - if balance < 0: - # With less of base asset center price should be offset downward - calculated_center_price = calculated_center_price / math.sqrt(1 + spread * (balance * -1)) - elif balance > 0: - # With more of base asset center price will be offset upwards - calculated_center_price = calculated_center_price * math.sqrt(1 + spread * balance) - else: - calculated_center_price = calculated_center_price - - # Calculate final_offset_price if manual center price offset is given - if manual_offset: - calculated_center_price = calculated_center_price + (calculated_center_price * manual_offset) - - return calculated_center_price - def calculate_order_data(self, order, amount, price): quote_asset = Amount(amount, self.market['quote']['symbol']) order['quote'] = quote_asset From c7be01bc71d4bc6192c11142350f8cfd5de2bd06 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 7 Sep 2018 14:27:57 +0300 Subject: [PATCH 12/42] WIP Change base.py comments and functions --- dexbot/strategies/base.py | 68 ++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index f899744bb..f3780c935 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -227,7 +227,6 @@ def _callbackPlaceFillOrders(self, d): """ This method distinguishes notifications caused by Matched orders from those caused by placed orders Todo: can this be renamed to _instantFill()? """ - # Todo: Add documentation if isinstance(d, FilledOrder): self.onOrderMatched(d) elif isinstance(d, Order): @@ -279,6 +278,7 @@ def account_total_value(self, return_asset): # Orders balance calculation for order in self.all_own_orders: + # Todo: What is the purpose of this? updated_order = self.get_updated_order(order['id']) if not order: @@ -421,13 +421,11 @@ def count_asset(self, order_ids=None, return_asset=False, refresh=True): return {'quote': quote, 'base': base} - def get_allocated_assets(self, order_ids, return_asset=False, refresh=True): - # Todo: + def get_allocated_assets(self, order_ids=None, return_asset=False): """ Returns the amount of QUOTE and BASE allocated in orders, and that do not show up in available balance :param order_ids: :param return_asset: - :param refresh: :return: """ # Todo: Add documentation @@ -574,7 +572,7 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, :param bool | refresh: :return: """ - # Todo: Logic here + # Todo: Insert logic here def get_market_orders(self, depth=1): """ Returns orders from the current market split in bids and asks. Orders are sorted by price. @@ -589,10 +587,10 @@ def get_market_orders(self, depth=1): def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0, refresh=False): - """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be sold, enhanced with moving - average or weighted moving average. + """ Returns the BASE/QUOTE price for which [quote_amount] worth of QUOTE could be bought, + enhanced with moving average or weighted moving average. - Depth = 0 means highest regardless of size + [quote/base]_amount = 0 means lowest regardless of size :param float | quote_amount: :param float | base_amount: @@ -601,18 +599,20 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, :param bool | refresh: :return: """ - # In case depth is not given, return price of the lowest sell order on the market - if depth == 0: + # Todo: Work in progress + # In case amount is not given, return price of the lowest sell order on the market + if base_amount == 0 or quote_amount == 0: lowest_market_sell_order = self.get_lowest_market_sell_order() return lowest_market_sell_order['price'] + # This calculation is for when quote_amount is given sum_quote = 0 sum_base = 0 order_number = 0 - market_sell_orders = self.get_market_sell_orders(depth=depth) + market_sell_orders = self.get_market_sell_orders(depth=self.fetch_depth) market_fee = self.get_market_fee() - lacking = depth * (1 + market_fee) + lacking = quote_amount * (1 + market_fee) while lacking > 0: sell_quote = float(market_sell_orders[order_number]['quote']) @@ -624,7 +624,7 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, else: sum_quote += float(market_sell_orders[order_number]['quote']) sum_base += float(market_sell_orders[order_number]['base']) - lacking = depth - sell_quote + lacking -= sell_quote price = sum_base / sum_quote @@ -649,8 +649,8 @@ def get_market_spread(self, quote_amount=0): market_orders = self.get_market_orders(fetch_depth) - ask = self.get_market_ask() - bid = self.get_market_bid() + ask = self.get_market_sell_price() + bid = self.get_market_buy_price() # Calculate market spread market_spread = ask / bid - 1 @@ -659,6 +659,7 @@ def get_market_spread(self, quote_amount=0): def get_order_cancellation_fee(self, fee_asset): """ Returns the order cancellation fee in the specified asset. + :param fee_asset: :return: """ @@ -764,10 +765,10 @@ def get_own_spread(self, highest_own_buy_price=None, lowest_own_sell_price=None, return actual_spread def get_price_for_amount_buy(self, amount=None): - """ Returns the cumulative price for which you could buy the specified amount of QUOTE. - This method must take into account market fee. + """ Returns the cumulative price for which you could buy the specified amount of QUOTE with market fee taken in + account. - :param amount: + :param float | amount: Amount to buy in QUOTE :return: """ # Todo: Insert logic here @@ -775,7 +776,7 @@ def get_price_for_amount_buy(self, amount=None): def get_price_for_amount_sell(self, amount=None): """ Returns the cumulative price for which you could sell the specified amount of QUOTE - :param amount: + :param float | amount: Amount to sell in QUOTE :return: """ # Todo: Insert logic here @@ -784,8 +785,7 @@ def get_updated_order(self, order_id): # Todo: This needed? """ Tries to get the updated order from the API. Returns None if the order doesn't exist - :param str|dict order_id: blockchain object id of the order - can be an order dict with the id key in it + :param str|dict order_id: blockchain object id of the order can be an order dict with the id key in it """ if isinstance(order_id, dict): order_id = order_id['id'] @@ -816,6 +816,7 @@ def enhance_center_price(self, reference=None, manual_offset=False, balance_base # Todo: Insert logic here def is_current_market(self, base_asset_id, quote_asset_id): + # Todo: Is this useful? """ Returns True if given asset id's are of the current market :return: bool: True = Current market, False = Not current market @@ -824,11 +825,13 @@ def is_current_market(self, base_asset_id, quote_asset_id): if base_asset_id == self.market['base']['id']: return True return False + # Todo: Should we return true if market is opposite? if quote_asset_id == self.market['base']['id']: if base_asset_id == self.market['quote']['id']: return True return False + return False def pause_worker(self): @@ -963,6 +966,7 @@ def restore_order(self, order): :return: """ # Todo: Insert logic here + # Todo: Is this something that is commonly used and thus needed? def retry_action(self, action, *args, **kwargs): """ Perform an action, and if certain suspected-to-be-spurious grapheme bugs occur, @@ -998,10 +1002,10 @@ def retry_action(self, action, *args, **kwargs): raise def write_order_log(self, worker_name, order): - """ + """ F :param string | worker_name: Name of the worker - :param object | order: Order that was traded + :param object | order: Order that was fulfilled """ # Todo: Add documentation operation_type = 'TRADE' @@ -1108,28 +1112,25 @@ def convert_asset(from_value, from_asset, to_asset): market = Market('{}/{}'.format(from_asset, to_asset)) ticker = market.ticker() latest_price = ticker.get('latest', {}).get('price', None) - return from_value * latest_price + precision = market['base']['precision'] + return truncate((from_value * latest_price), precision) @staticmethod - def get_original_order(order_id, return_none=True): - """ Returns the Order object for the order_id + def get_order(order_id, return_none=True): + """ Get Order object with order_id - :param dict | order_id: blockchain object id of the order can be an order dict with the id key in it - :param bool | return_none: return None instead of an empty Order object when the order doesn't exist - :return: + :param str | dict order_id: blockchain object id of the order can be an order dict with the id key in it + :param bool return_none: return None instead of an empty Order object when the order doesn't exist + :return: Order object """ if not order_id: return None - if 'id' in order_id: order_id = order_id['id'] - order = Order(order_id) - if return_none and order['deleted']: return None - return order @staticmethod @@ -1140,6 +1141,7 @@ def get_updated_limit_order(limit_order): :param limit_order: an item of Account['limit_orders'] :return: Order Todo: When would we not want an updated order? + Todo: If get_updated_order is removed, this can be removed as well. """ order = copy.deepcopy(limit_order) price = order['sell_price']['base']['amount'] / order['sell_price']['quote']['amount'] From b0aef0ac69a85e6fa6e0193dd0232c3de127e0f8 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 11 Sep 2018 15:28:41 +0300 Subject: [PATCH 13/42] Change comments on functions --- dexbot/strategies/base.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index f3780c935..77df6511f 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -301,7 +301,6 @@ def balance(self, asset, fee_reservation=0): :param bool | fee_reservation: :return: Balance of specific asset """ - # Todo: Add documentation # Todo: Check that fee reservation was as intended, having it true / false made no sense balance = self._account.balance(asset) @@ -311,6 +310,14 @@ def balance(self, asset, fee_reservation=0): return balance def calculate_order_data(self, order, amount, price): + """ + + :param order: + :param amount: + :param price: + :return: + """ + # Todo: Add documentation quote_asset = Amount(amount, self.market['quote']['symbol']) order['quote'] = quote_asset order['price'] = price @@ -365,13 +372,12 @@ def cancel_all_orders(self): self.log.info("Orders canceled") def cancel_orders(self, orders, batch_only=False): - """ Cancel specific order(s) + """ Cancel specific order or orders :param list | orders: List of orders to cancel :param bool | batch_only: Try cancel orders only in batch mode without one-by-one fallback - :return: + :return: Todo: Add documentation """ - # Todo: Add documentation if not isinstance(orders, (list, set, tuple)): orders = [orders] @@ -424,11 +430,10 @@ def count_asset(self, order_ids=None, return_asset=False, refresh=True): def get_allocated_assets(self, order_ids=None, return_asset=False): """ Returns the amount of QUOTE and BASE allocated in orders, and that do not show up in available balance - :param order_ids: - :param return_asset: - :return: + :param list | order_ids: + :param bool | return_asset: + :return: Dictionary of QUOTE and BASE amounts """ - # Todo: Add documentation if not order_ids: order_ids = [] elif isinstance(order_ids, str): @@ -449,6 +454,7 @@ def get_allocated_assets(self, order_ids=None, return_asset=False): elif asset_id == base_asset: base += order['base']['amount'] + # Return as Amount objects instead of only float values if return_asset: quote = Amount(quote, quote_asset) base = Amount(base, base_asset) @@ -596,7 +602,6 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, :param float | base_amount: :param float | moving_average: :param float | weighted_moving_average: - :param bool | refresh: :return: """ # Todo: Work in progress @@ -619,6 +624,7 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, if sell_quote > lacking: sum_quote += lacking + # Fixme: Price is inverted to same format as buy orders. Should this be inverted for this calculation? sum_base += lacking * market_sell_orders[order_number]['price'] # Make sure price is not inverted lacking = 0 else: @@ -637,6 +643,7 @@ def get_market_spread(self, quote_amount=0): :param int | quote_amount: :return: Market spread as float or None """ + # Todo: Work in progress # Decides how many orders need to be fetched for the market spread calculation if quote_amount == 0: # Get only the closest orders @@ -813,7 +820,7 @@ def enhance_center_price(self, reference=None, manual_offset=False, balance_base :param int or float | weighted_average: :return: """ - # Todo: Insert logic here + # Todo: Remove this after market center price is done def is_current_market(self, base_asset_id, quote_asset_id): # Todo: Is this useful? From 7eecdfdc5b3bca3ac5bfd70d826b584746d48640 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 11 Sep 2018 15:29:20 +0300 Subject: [PATCH 14/42] Add functions to get order fees --- dexbot/strategies/base.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 77df6511f..479491763 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -16,6 +16,7 @@ import bitsharesapi.exceptions from bitshares.account import Account from bitshares.amount import Amount, Asset +from bitshares.dex import Dex from bitshares.instance import shared_bitshares_instance from bitshares.market import Market from bitshares.price import FilledOrder, Order, UpdateCallOrder @@ -144,6 +145,9 @@ def __init__(self, # BitShares instance self.bitshares = bitshares_instance or shared_bitshares_instance() + # Dex instance used to get different fees for the market + self.dex = Dex(self.bitshares) + # Storage Storage.__init__(self, name) @@ -667,10 +671,16 @@ def get_market_spread(self, quote_amount=0): def get_order_cancellation_fee(self, fee_asset): """ Returns the order cancellation fee in the specified asset. - :param fee_asset: - :return: + :param string | fee_asset: Asset in which the fee is wanted + :return: Cancellation fee as fee asset """ - # Todo: Insert logic here + # Get fee + fees = self.dex.returnFees() + limit_order_cancel = fees['limit_order_cancel'] + + # Convert fee + # Todo: Change 'TEST' to 'BTS' + return self.convert_asset(limit_order_cancel['fee'], 'TEST', fee_asset) def get_order_creation_fee(self, fee_asset): """ Returns the cost of creating an order in the asset specified @@ -678,7 +688,13 @@ def get_order_creation_fee(self, fee_asset): :param fee_asset: QUOTE, BASE, BTS, or any other :return: """ - # Todo: Insert logic here + # Get fee + fees = self.dex.returnFees() + limit_order_create = fees['limit_order_create'] + + # Convert fee + # Todo: Change 'TEST' to 'BTS' + return self.convert_asset(limit_order_create['fee'], 'TEST', fee_asset) def filter_buy_orders(self, orders, sort=None): """ Return own buy orders from list of orders. Can be used to pick buy orders from a list From 54a7460c4f13ae535e07c0185a187d65319d28fb Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 11 Sep 2018 15:30:05 +0300 Subject: [PATCH 15/42] Remove unused functions --- dexbot/strategies/base.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 479491763..8d6c9ee91 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -787,23 +787,6 @@ def get_own_spread(self, highest_own_buy_price=None, lowest_own_sell_price=None, actual_spread = lowest_own_sell_price / highest_own_buy_price - 1 return actual_spread - def get_price_for_amount_buy(self, amount=None): - """ Returns the cumulative price for which you could buy the specified amount of QUOTE with market fee taken in - account. - - :param float | amount: Amount to buy in QUOTE - :return: - """ - # Todo: Insert logic here - - def get_price_for_amount_sell(self, amount=None): - """ Returns the cumulative price for which you could sell the specified amount of QUOTE - - :param float | amount: Amount to sell in QUOTE - :return: - """ - # Todo: Insert logic here - def get_updated_order(self, order_id): # Todo: This needed? """ Tries to get the updated order from the API. Returns None if the order doesn't exist From be6e0128044920f7449647165890f2240a86b368 Mon Sep 17 00:00:00 2001 From: joelva Date: Tue, 11 Sep 2018 15:30:34 +0300 Subject: [PATCH 16/42] WIP Change get_market_center_price() --- dexbot/strategies/base.py | 77 ++++++++++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 14 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 8d6c9ee91..b9216aa42 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -203,6 +203,9 @@ def __init__(self, # If there is no fee asset, use BTS self.fee_asset = Asset('1.3.0') + # Ticker + self.ticker = self.market.ticker() + # Settings for bitshares instance self.bitshares.bundle = bool(self.worker.get("bundle", False)) @@ -558,31 +561,78 @@ def get_lowest_own_sell_order(self, orders=None): except IndexError: return None - def get_market_center_price(self, quote_amount=0, refresh=False): + def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors=False): """ Returns the center price of market including own orders. Depth: 0 = calculate from closest opposite orders. - Depth: non-zero = calculate from specified depth + Depth: non-zero = calculate from specified depth of orders + :param float | base_amount: :param float | quote_amount: - :param bool | refresh: - :return: + :param bool | suppress_errors: + :return: Market center price as float """ - # Todo: Insert logic here + buy_price = 0 + sell_price = 0 + + if base_amount == 0: + # Get highest buy order from the market + highest_buy_order = self.ticker.get("highestBid") - def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0, - refresh=False): + if highest_buy_order is None or highest_buy_order == 0.0: + if not suppress_errors: + self.log.critical("Cannot estimate center price, there is no highest bid.") + self.disabled = True + return None + + # The highest buy price + buy_price = highest_buy_order['price'] + elif base_amount > 0: + buy_price = self.get_market_buy_price(base_amount=base_amount) + + if quote_amount == 0: + # Get lowest sell order from the market + lowest_sell_order = self.ticker.get("lowestAsk") + + if lowest_sell_order is None or lowest_sell_order == 0.0: + if not suppress_errors: + self.log.critical("Cannot estimate center price, there is no lowest ask.") + self.disabled = True + return None + + # The lowest sell price + sell_price = lowest_sell_order['price'] + elif quote_amount > 0: + sell_price = self.get_market_sell_price(quote_amount=quote_amount) + + # Calculate and return market center price + return buy_price * math.sqrt(sell_price / buy_price) + + def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0): """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with moving average or weighted moving average :param float | quote_amount: :param float | base_amount: - :param float | moving_average: - :param float | weighted_moving_average: - :param bool | refresh: + :param int | moving_average: Count of orders to be taken in to the calculations + :param int | weighted_moving_average: Count of orders to be taken in to the calculations :return: """ - # Todo: Insert logic here + """ + Buy orders: + 10 CODACOIN for 20 TEST + 15 CODACOIN for 30 TEST + 20 CODACOIN for 40 TEST + + (price + price + price) / moving_average + moving average = (2 + 3 + 4) / 3 = 3 + + ((amount * price) + (amount * price) + (amount * price)) / amount_total + weighted moving average = ((10 * 2) + (15 * 3) + (20 * 4)) / 45 = 3,222222 + + """ + # Todo: Work in progress + pass def get_market_orders(self, depth=1): """ Returns orders from the current market split in bids and asks. Orders are sorted by price. @@ -595,8 +645,7 @@ def get_market_orders(self, depth=1): """ return self.market.orderbook(depth) - def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0, - refresh=False): + def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0): """ Returns the BASE/QUOTE price for which [quote_amount] worth of QUOTE could be bought, enhanced with moving average or weighted moving average. @@ -851,7 +900,7 @@ def pause_worker(self): # Removes worker's orders from local database self.clear_orders() - def purge_all_worker_data(self): + def clear_all_worker_data(self): """ Clear all the worker data from the database and cancel all orders """ # Removes worker's orders from local database From b7d3b727fa1ff704412a6bd007a0e6a2cf6c47e8 Mon Sep 17 00:00:00 2001 From: Marko Paasila Date: Tue, 11 Sep 2018 21:48:23 +0300 Subject: [PATCH 17/42] Made center price and spread methods use get_market_sell/buy_price() logic for depth/amount --- dexbot/strategies/base.py | 111 +++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 61 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index b9216aa42..36b500074 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -572,38 +572,20 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors :param bool | suppress_errors: :return: Market center price as float """ - buy_price = 0 - sell_price = 0 - - if base_amount == 0: - # Get highest buy order from the market - highest_buy_order = self.ticker.get("highestBid") - - if highest_buy_order is None or highest_buy_order == 0.0: - if not suppress_errors: - self.log.critical("Cannot estimate center price, there is no highest bid.") - self.disabled = True - return None - - # The highest buy price - buy_price = highest_buy_order['price'] - elif base_amount > 0: - buy_price = self.get_market_buy_price(base_amount=base_amount) - - if quote_amount == 0: - # Get lowest sell order from the market - lowest_sell_order = self.ticker.get("lowestAsk") + if highest_buy_order is None or highest_buy_order == 0.0: + if not suppress_errors: + self.log.critical("Cannot estimate center price, there is no highest bid.") + self.disabled = True + return None - if lowest_sell_order is None or lowest_sell_order == 0.0: - if not suppress_errors: - self.log.critical("Cannot estimate center price, there is no lowest ask.") - self.disabled = True - return None + if lowest_sell_order is None or lowest_sell_order == 0.0: + if not suppress_errors: + self.log.critical("Cannot estimate center price, there is no lowest ask.") + self.disabled = True + return None - # The lowest sell price - sell_price = lowest_sell_order['price'] - elif quote_amount > 0: - sell_price = self.get_market_sell_price(quote_amount=quote_amount) + buy_price = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount) + sell_price = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount) # Calculate and return market center price return buy_price * math.sqrt(sell_price / buy_price) @@ -632,6 +614,7 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, """ # Todo: Work in progress + # Like get_market_sell_price(), but defaulting to base_amount if both base and quote are specified. pass def get_market_orders(self, depth=1): @@ -659,37 +642,54 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, """ # Todo: Work in progress # In case amount is not given, return price of the lowest sell order on the market - if base_amount == 0 or quote_amount == 0: - lowest_market_sell_order = self.get_lowest_market_sell_order() - return lowest_market_sell_order['price'] + if base_amount == 0 and quote_amount == 0: + return self.ticker.get("lowestAsk") - # This calculation is for when quote_amount is given sum_quote = 0 sum_base = 0 order_number = 0 market_sell_orders = self.get_market_sell_orders(depth=self.fetch_depth) market_fee = self.get_market_fee() - lacking = quote_amount * (1 + market_fee) - - while lacking > 0: - sell_quote = float(market_sell_orders[order_number]['quote']) - if sell_quote > lacking: - sum_quote += lacking - # Fixme: Price is inverted to same format as buy orders. Should this be inverted for this calculation? - sum_base += lacking * market_sell_orders[order_number]['price'] # Make sure price is not inverted - lacking = 0 - else: - sum_quote += float(market_sell_orders[order_number]['quote']) - sum_base += float(market_sell_orders[order_number]['base']) - lacking -= sell_quote + # This calculation is for when quote_amount is given (whether or not base_amount was given + if quote_amount > 0: + lacking = quote_amount * (1 + market_fee) + while lacking > 0: + sell_quote = float(market_sell_orders[order_number]['quote']) + + if sell_quote > lacking: + sum_quote += lacking + # Fixme: Price is inverted to same format as buy orders. Should this be inverted for this calculation? + sum_base += lacking / market_sell_orders[order_number]['price'] # I swapped * to /. Is it right now? + lacking = 0 + else: + sum_quote += float(market_sell_orders[order_number]['quote']) + sum_base += float(market_sell_orders[order_number]['base']) + lacking -= sell_quote + + # This calculation is for when quote_amount isn't given, so we go with the given base_amount + if quote_amount = 0: + lacking = base_amount * (1 + market_fee) + while lacking > 0: + buy_base = float(market_sell_orders[order_number]['base']) + + if buy_base > lacking: + sum_base += lacking + # Fixme: Price is inverted to same format as buy orders. Should this be inverted for this calculation? + sum_quote += lacking * market_sell_orders[order_number][ + 'price'] # Make sure price is not inverted + lacking = 0 + else: + sum_quote += float(market_sell_orders[order_number]['quote']) + sum_base += float(market_sell_orders[order_number]['base']) + lacking -= buy_base price = sum_base / sum_quote return price - def get_market_spread(self, quote_amount=0): + def get_market_spread(self, quote_amount=0, base_amount=0): """ Returns the market spread %, including own orders, from specified depth, enhanced with moving average or weighted moving average. @@ -697,20 +697,9 @@ def get_market_spread(self, quote_amount=0): :return: Market spread as float or None """ # Todo: Work in progress - # Decides how many orders need to be fetched for the market spread calculation - if quote_amount == 0: - # Get only the closest orders - fetch_depth = 1 - elif quote_amount > 0: - fetch_depth = self.fetch_depth - - # Raise the fetch depth each time. int() rounds the count - self.fetch_depth = int(self.fetch_depth * 1.5) - - market_orders = self.get_market_orders(fetch_depth) - ask = self.get_market_sell_price() - bid = self.get_market_buy_price() + ask = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount) + bid = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount) # Calculate market spread market_spread = ask / bid - 1 From 07695bd153c266ec989aa12f60080808f8fdec47 Mon Sep 17 00:00:00 2001 From: joelva Date: Wed, 12 Sep 2018 16:58:43 +0300 Subject: [PATCH 18/42] WIP Change market price calculations --- dexbot/strategies/base.py | 127 ++++++++++++++++++++++---------------- 1 file changed, 75 insertions(+), 52 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 36b500074..002de14cc 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -564,14 +564,15 @@ def get_lowest_own_sell_order(self, orders=None): def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors=False): """ Returns the center price of market including own orders. - Depth: 0 = calculate from closest opposite orders. - Depth: non-zero = calculate from specified depth of orders - :param float | base_amount: :param float | quote_amount: :param bool | suppress_errors: :return: Market center price as float """ + + highest_buy_order = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount) + lowest_sell_order = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount) + if highest_buy_order is None or highest_buy_order == 0.0: if not suppress_errors: self.log.critical("Cannot estimate center price, there is no highest bid.") @@ -584,11 +585,8 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors self.disabled = True return None - buy_price = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount) - sell_price = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount) - # Calculate and return market center price - return buy_price * math.sqrt(sell_price / buy_price) + return highest_buy_order * math.sqrt(lowest_sell_order / highest_buy_order) def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0): """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with @@ -615,7 +613,42 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, """ # Todo: Work in progress # Like get_market_sell_price(), but defaulting to base_amount if both base and quote are specified. - pass + # In case amount is not given, return price of the lowest sell order on the market + if quote_amount == 0 and base_amount == 0: + return self.ticker.get('highestBid') + + asset_amount = base_amount + + """ Since the purpose is never get both quote and base amounts, favor base amount if both given because + this function is looking for buy price. + """ + if base_amount > quote_amount: + base = True + else: + asset_amount = quote_amount + base = False + + market_buy_orders = self.get_market_buy_orders(depth=self.fetch_depth) + market_fee = self.get_market_fee() + + target_amount = asset_amount * (1 + market_fee) + + quote_amount = 0 + base_amount = 0 + # Todo: Work in progress, THIS IS EXPERIEMENTAL WAY TO CALCULATE THIS. Revert if incorrect. + for order in market_buy_orders: + if base: + # BASE amount was given + if base_amount < target_amount: + quote_amount += order['quote']['amount'] + base_amount += order['base']['amount'] + elif not base: + # QUOTE amount was given + if quote_amount < target_amount: + quote_amount += order['quote']['amount'] + base_amount += order['base']['amount'] + + return base_amount / quote_amount def get_market_orders(self, depth=1): """ Returns orders from the current market split in bids and asks. Orders are sorted by price. @@ -642,69 +675,59 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, """ # Todo: Work in progress # In case amount is not given, return price of the lowest sell order on the market - if base_amount == 0 and quote_amount == 0: - return self.ticker.get("lowestAsk") + if quote_amount == 0 and base_amount == 0: + return self.ticker.get('lowestAsk') - sum_quote = 0 - sum_base = 0 - order_number = 0 + asset_amount = quote_amount + + """ Since the purpose is never get both quote and base amounts, favor quote amount if both given because + this function is looking for sell price. + """ + if quote_amount > base_amount: + quote = True + else: + asset_amount = base_amount + quote = False market_sell_orders = self.get_market_sell_orders(depth=self.fetch_depth) market_fee = self.get_market_fee() - # This calculation is for when quote_amount is given (whether or not base_amount was given - if quote_amount > 0: - lacking = quote_amount * (1 + market_fee) - while lacking > 0: - sell_quote = float(market_sell_orders[order_number]['quote']) - - if sell_quote > lacking: - sum_quote += lacking - # Fixme: Price is inverted to same format as buy orders. Should this be inverted for this calculation? - sum_base += lacking / market_sell_orders[order_number]['price'] # I swapped * to /. Is it right now? - lacking = 0 - else: - sum_quote += float(market_sell_orders[order_number]['quote']) - sum_base += float(market_sell_orders[order_number]['base']) - lacking -= sell_quote - - # This calculation is for when quote_amount isn't given, so we go with the given base_amount - if quote_amount = 0: - lacking = base_amount * (1 + market_fee) - while lacking > 0: - buy_base = float(market_sell_orders[order_number]['base']) - - if buy_base > lacking: - sum_base += lacking - # Fixme: Price is inverted to same format as buy orders. Should this be inverted for this calculation? - sum_quote += lacking * market_sell_orders[order_number][ - 'price'] # Make sure price is not inverted - lacking = 0 - else: - sum_quote += float(market_sell_orders[order_number]['quote']) - sum_base += float(market_sell_orders[order_number]['base']) - lacking -= buy_base + target_amount = asset_amount * (1 + market_fee) + + quote_amount = 0 + base_amount = 0 - price = sum_base / sum_quote + # Todo: Work in progress, THIS IS EXPERIEMENTAL WAY TO CALCULATE THIS. Revert if incorrect. + for order in market_sell_orders: + if quote: + # QUOTE amount was given + if quote_amount < target_amount: + quote_amount += order['quote']['amount'] + base_amount += order['base']['amount'] + elif not quote: + # BASE amount was given + if base_amount < target_amount: + quote_amount += order['quote']['amount'] + base_amount += order['base']['amount'] - return price + return base_amount / quote_amount def get_market_spread(self, quote_amount=0, base_amount=0): """ Returns the market spread %, including own orders, from specified depth, enhanced with moving average or weighted moving average. - :param int | quote_amount: + :param float | quote_amount: + :param float | base_amount: :return: Market spread as float or None """ - # Todo: Work in progress - ask = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount) bid = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount) # Calculate market spread - market_spread = ask / bid - 1 + if ask == 0 or bid == 0: + return None - return market_spread + return ask / bid - 1 def get_order_cancellation_fee(self, fee_asset): """ Returns the order cancellation fee in the specified asset. From e0d9c3df2f7cdfb2481dd8645c8166e324379ba8 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 08:04:22 +0300 Subject: [PATCH 19/42] Refactor variables in get_market_center_price() --- dexbot/strategies/base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 002de14cc..d55ba207a 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -570,23 +570,23 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors :return: Market center price as float """ - highest_buy_order = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount) - lowest_sell_order = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount) + buy_price = self.get_market_buy_price(quote_amount=quote_amount, base_amount=base_amount) + sell_price = self.get_market_sell_price(quote_amount=quote_amount, base_amount=base_amount) - if highest_buy_order is None or highest_buy_order == 0.0: + if buy_price is None or buy_price == 0.0: if not suppress_errors: self.log.critical("Cannot estimate center price, there is no highest bid.") self.disabled = True return None - if lowest_sell_order is None or lowest_sell_order == 0.0: + if sell_price is None or sell_price == 0.0: if not suppress_errors: self.log.critical("Cannot estimate center price, there is no lowest ask.") self.disabled = True return None # Calculate and return market center price - return highest_buy_order * math.sqrt(lowest_sell_order / highest_buy_order) + return buy_price * math.sqrt(sell_price / buy_price) def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0): """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with From 6d0e9084fb24a8b7581c22e356a2b6c7c44f3e47 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 08:05:11 +0300 Subject: [PATCH 20/42] Remove todo comments --- dexbot/strategies/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index d55ba207a..950e11cd1 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -635,7 +635,7 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, quote_amount = 0 base_amount = 0 - # Todo: Work in progress, THIS IS EXPERIEMENTAL WAY TO CALCULATE THIS. Revert if incorrect. + for order in market_buy_orders: if base: # BASE amount was given @@ -697,7 +697,6 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, quote_amount = 0 base_amount = 0 - # Todo: Work in progress, THIS IS EXPERIEMENTAL WAY TO CALCULATE THIS. Revert if incorrect. for order in market_sell_orders: if quote: # QUOTE amount was given From fea24d6edc18dea3ad055fbf942a4f603391f312 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 10:31:51 +0300 Subject: [PATCH 21/42] Remove base for restore_order() --- dexbot/strategies/base.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 950e11cd1..a36a5041a 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -1025,15 +1025,6 @@ def place_market_sell_order(self, amount, price, return_none=False, *args, **kwa return sell_order - def restore_order(self, order): - """ If an order is partially or completely filled, this will make a new order of original size and price. - - :param order: - :return: - """ - # Todo: Insert logic here - # Todo: Is this something that is commonly used and thus needed? - def retry_action(self, action, *args, **kwargs): """ Perform an action, and if certain suspected-to-be-spurious grapheme bugs occur, instead of bubbling the exception, it is quietly logged (level WARN), and try again From 9a1dfdbf667247f6baff43fff54909a0abf71b12 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 10:32:35 +0300 Subject: [PATCH 22/42] Change get_market_buy_price() --- dexbot/strategies/base.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index a36a5041a..de4b20b53 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -588,30 +588,14 @@ def get_market_center_price(self, base_amount=0, quote_amount=0, suppress_errors # Calculate and return market center price return buy_price * math.sqrt(sell_price / buy_price) - def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0): + def get_market_buy_price(self, quote_amount=0, base_amount=0): """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with moving average or weighted moving average :param float | quote_amount: :param float | base_amount: - :param int | moving_average: Count of orders to be taken in to the calculations - :param int | weighted_moving_average: Count of orders to be taken in to the calculations :return: """ - """ - Buy orders: - 10 CODACOIN for 20 TEST - 15 CODACOIN for 30 TEST - 20 CODACOIN for 40 TEST - - (price + price + price) / moving_average - moving average = (2 + 3 + 4) / 3 = 3 - - ((amount * price) + (amount * price) + (amount * price)) / amount_total - weighted moving average = ((10 * 2) + (15 * 3) + (20 * 4)) / 45 = 3,222222 - - """ - # Todo: Work in progress # Like get_market_sell_price(), but defaulting to base_amount if both base and quote are specified. # In case amount is not given, return price of the lowest sell order on the market if quote_amount == 0 and base_amount == 0: @@ -635,6 +619,7 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, quote_amount = 0 base_amount = 0 + missing_amount = target_amount for order in market_buy_orders: if base: @@ -642,11 +627,21 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0, moving_average=0, if base_amount < target_amount: quote_amount += order['quote']['amount'] base_amount += order['base']['amount'] + missing_amount -= order['base']['amount'] + elif base_amount > missing_amount: + base_amount += missing_amount + quote_amount += missing_amount / order['price'] + break elif not base: # QUOTE amount was given if quote_amount < target_amount: quote_amount += order['quote']['amount'] base_amount += order['base']['amount'] + missing_amount -= order['quote']['amount'] + elif quote_amount > missing_amount: + base_amount += missing_amount * order['price'] + quote_amount += missing_amount + break return base_amount / quote_amount From 1f981f55f669c4d6e07cbb05c60a3156e58671e2 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 10:33:08 +0300 Subject: [PATCH 23/42] Change get_market_sell_price() --- dexbot/strategies/base.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index de4b20b53..96f49e482 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -656,7 +656,7 @@ def get_market_orders(self, depth=1): """ return self.market.orderbook(depth) - def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, weighted_moving_average=0): + def get_market_sell_price(self, quote_amount=0, base_amount=00): """ Returns the BASE/QUOTE price for which [quote_amount] worth of QUOTE could be bought, enhanced with moving average or weighted moving average. @@ -664,11 +664,8 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, :param float | quote_amount: :param float | base_amount: - :param float | moving_average: - :param float | weighted_moving_average: :return: """ - # Todo: Work in progress # In case amount is not given, return price of the lowest sell order on the market if quote_amount == 0 and base_amount == 0: return self.ticker.get('lowestAsk') @@ -691,6 +688,7 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, quote_amount = 0 base_amount = 0 + missing_amount = target_amount for order in market_sell_orders: if quote: @@ -698,11 +696,22 @@ def get_market_sell_price(self, quote_amount=0, base_amount=0, moving_average=0, if quote_amount < target_amount: quote_amount += order['quote']['amount'] base_amount += order['base']['amount'] + missing_amount -= order['quote']['amount'] + elif quote_amount > missing_amount: + base_amount += missing_amount * order['price'] + quote_amount += missing_amount + break + elif not quote: # BASE amount was given if base_amount < target_amount: quote_amount += order['quote']['amount'] base_amount += order['base']['amount'] + missing_amount -= order['base']['amount'] + elif base_amount > missing_amount: + base_amount += missing_amount + quote_amount += missing_amount / order['price'] + break return base_amount / quote_amount From 982fb5a8da9d9d74df75d152f99cfaeb98b76884 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 10:34:52 +0300 Subject: [PATCH 24/42] Change fee asset for get_x_fee functions to BTS --- dexbot/strategies/base.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 96f49e482..97ce9c14f 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -743,8 +743,7 @@ def get_order_cancellation_fee(self, fee_asset): limit_order_cancel = fees['limit_order_cancel'] # Convert fee - # Todo: Change 'TEST' to 'BTS' - return self.convert_asset(limit_order_cancel['fee'], 'TEST', fee_asset) + return self.convert_asset(limit_order_cancel['fee'], 'BTS', fee_asset) def get_order_creation_fee(self, fee_asset): """ Returns the cost of creating an order in the asset specified @@ -757,8 +756,7 @@ def get_order_creation_fee(self, fee_asset): limit_order_create = fees['limit_order_create'] # Convert fee - # Todo: Change 'TEST' to 'BTS' - return self.convert_asset(limit_order_create['fee'], 'TEST', fee_asset) + return self.convert_asset(limit_order_create['fee'], 'BTS', fee_asset) def filter_buy_orders(self, orders, sort=None): """ Return own buy orders from list of orders. Can be used to pick buy orders from a list From 30ea21cd2f22a7d3db51ff93b25e3bc1c72ce745 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 10:35:12 +0300 Subject: [PATCH 25/42] Change comments on functions --- dexbot/strategies/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 97ce9c14f..9b72ce34c 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -802,7 +802,7 @@ def filter_sell_orders(self, orders, sort=None): return sell_orders def get_own_buy_orders(self, orders=None): - """ Get own buy orders from current market + """ Get own buy orders from current market, or from a set of orders passed for this function. :return: List of buy orders """ @@ -1212,7 +1212,6 @@ def get_updated_limit_order(limit_order): @staticmethod def purge_all_local_worker_data(worker_name): - # Todo: Confirm this being correct """ Removes worker's data and orders from local sqlite database :param worker_name: Name of the worker to be removed From 7efc45a6b5e693a43011145dcd2782cfa415a7e9 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 13:36:22 +0300 Subject: [PATCH 26/42] Change get_own_spread() by removing depth --- dexbot/strategies/base.py | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 9b72ce34c..6567237b5 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -823,27 +823,18 @@ def get_own_sell_orders(self, orders=None): return self.filter_sell_orders(orders) - def get_own_spread(self, highest_own_buy_price=None, lowest_own_sell_price=None, depth=0, refresh=False): + def get_own_spread(self): """ Returns the difference between own closest opposite orders. - :param float | highest_own_buy_price: - :param float | lowest_own_sell_price: - :param float | depth: Use most resent data from Bitshares - :param bool | refresh: :return: float or None: Own spread """ - # Todo: Add depth - if refresh: - try: - # Try fetching own orders - highest_own_buy_price = self.get_highest_market_buy_order().get('price') - lowest_own_sell_price = self.get_lowest_own_sell_order().get('price') - except AttributeError: - return None - else: - # If orders are given, use them instead newest data from the blockchain - highest_own_buy_price = highest_own_buy_price - lowest_own_sell_price = lowest_own_sell_price + + try: + # Try fetching own orders + highest_own_buy_price = self.get_highest_market_buy_order().get('price') + lowest_own_sell_price = self.get_lowest_own_sell_order().get('price') + except AttributeError: + return None # Calculate actual spread actual_spread = lowest_own_sell_price / highest_own_buy_price - 1 @@ -884,7 +875,6 @@ def enhance_center_price(self, reference=None, manual_offset=False, balance_base # Todo: Remove this after market center price is done def is_current_market(self, base_asset_id, quote_asset_id): - # Todo: Is this useful? """ Returns True if given asset id's are of the current market :return: bool: True = Current market, False = Not current market @@ -1061,12 +1051,11 @@ def retry_action(self, action, *args, **kwargs): raise def write_order_log(self, worker_name, order): - """ F + """ Write order log to csv file :param string | worker_name: Name of the worker :param object | order: Order that was fulfilled """ - # Todo: Add documentation operation_type = 'TRADE' if order['base']['symbol'] == self.market['base']['symbol']: From 4f9108b7ba0fa25cd3f8ccd9f011cd567eab107e Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 14:44:18 +0300 Subject: [PATCH 27/42] Add doc for WorkerController strategies() --- dexbot/controllers/worker_controller.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dexbot/controllers/worker_controller.py b/dexbot/controllers/worker_controller.py index 5b8dbc4a2..4d79e3211 100644 --- a/dexbot/controllers/worker_controller.py +++ b/dexbot/controllers/worker_controller.py @@ -25,6 +25,14 @@ def __init__(self, view, bitshares_instance, mode): @property def strategies(self): + """ Defines strategies that are configurable from the GUI. + + key: Strategy location in the project + name: The name that is shown in the GUI for user + form_module: If there is custom form module created with QTDesigner + + :return: List of strategies + """ strategies = collections.OrderedDict() strategies['dexbot.strategies.relative_orders'] = { 'name': 'Relative Orders', From 18cd2a0cfaeaa2d8350259f3c38dc0ef39c4a380 Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 14:53:33 +0300 Subject: [PATCH 28/42] Refactor base.py variable names and ticker --- dexbot/strategies/base.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 6567237b5..e2205666e 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -204,7 +204,7 @@ def __init__(self, self.fee_asset = Asset('1.3.0') # Ticker - self.ticker = self.market.ticker() + self.ticker = self.market.ticker # Settings for bitshares instance self.bitshares.bundle = bool(self.worker.get("bundle", False)) @@ -351,7 +351,7 @@ def calculate_worker_value(self, unit_of_measure): quote_total += balance['amount'] # Calculate value of the orders in unit of measure - orders = self.current_market_own_orders + orders = self.get_own_orders for order in orders: if order['base']['symbol'] == self.quote_asset: # Pick sell orders order's BASE amount, which is same as worker's QUOTE, to worker's BASE @@ -399,7 +399,7 @@ def cancel_orders(self, orders, batch_only=False): self._cancel_orders(order) return True - def count_asset(self, order_ids=None, return_asset=False, refresh=True): + def count_asset(self, order_ids=None, return_asset=False): """ Returns the combined amount of the given order ids and the account balance The amounts are returned in quote and base assets of the market @@ -422,9 +422,9 @@ def count_asset(self, order_ids=None, return_asset=False, refresh=True): if order_ids is None: # Get all orders from Blockchain - order_ids = [order['id'] for order in self.current_market_own_orders] + order_ids = [order['id'] for order in self.get_own_orders] if order_ids: - orders_balance = self.orders_balance(order_ids) + orders_balance = self.get_allocated_assets(order_ids) quote += orders_balance['quote'] base += orders_balance['base'] @@ -599,7 +599,7 @@ def get_market_buy_price(self, quote_amount=0, base_amount=0): # Like get_market_sell_price(), but defaulting to base_amount if both base and quote are specified. # In case amount is not given, return price of the lowest sell order on the market if quote_amount == 0 and base_amount == 0: - return self.ticker.get('highestBid') + return self.ticker().get('highestBid') asset_amount = base_amount @@ -668,7 +668,7 @@ def get_market_sell_price(self, quote_amount=0, base_amount=00): """ # In case amount is not given, return price of the lowest sell order on the market if quote_amount == 0 and base_amount == 0: - return self.ticker.get('lowestAsk') + return self.ticker().get('lowestAsk') asset_amount = quote_amount @@ -808,7 +808,7 @@ def get_own_buy_orders(self, orders=None): """ if not orders: # List of orders was not given so fetch everything from the market - orders = self.current_market_own_orders + orders = self.get_own_orders return self.filter_buy_orders(orders) @@ -819,7 +819,7 @@ def get_own_sell_orders(self, orders=None): """ if not orders: # List of orders was not given so fetch everything from the market - orders = self.current_market_own_orders + orders = self.get_own_orders return self.filter_sell_orders(orders) @@ -1125,7 +1125,7 @@ def all_own_orders(self, refresh=True): return orders @property - def current_market_own_orders(self, refresh=False): + def get_own_orders(self): """ Return the account's open orders in the current market :return: List of Order objects @@ -1133,8 +1133,7 @@ def current_market_own_orders(self, refresh=False): orders = [] # Refresh account data - if refresh: - self.account.refresh() + self.account.refresh() for order in self.account.openorders: if self.worker["market"] == order.market and self.account.openorders: @@ -1192,8 +1191,8 @@ def get_updated_limit_order(limit_order): Todo: If get_updated_order is removed, this can be removed as well. """ order = copy.deepcopy(limit_order) - price = order['sell_price']['base']['amount'] / order['sell_price']['quote']['amount'] - base_amount = order['for_sale'] + price = float(order['sell_price']['base']['amount']) / float(order['sell_price']['quote']['amount']) + base_amount = float(order['for_sale']) quote_amount = base_amount / price order['sell_price']['base']['amount'] = base_amount order['sell_price']['quote']['amount'] = quote_amount From ce01996fb110a95c44e3c011c573fe46b85b95cc Mon Sep 17 00:00:00 2001 From: joelva Date: Thu, 13 Sep 2018 15:13:48 +0300 Subject: [PATCH 29/42] Add strategy_template.py --- dexbot/strategies/strategy_template.py | 241 +++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 dexbot/strategies/strategy_template.py diff --git a/dexbot/strategies/strategy_template.py b/dexbot/strategies/strategy_template.py new file mode 100644 index 000000000..44ca214ec --- /dev/null +++ b/dexbot/strategies/strategy_template.py @@ -0,0 +1,241 @@ +# Python imports +import math + +# Project imports +from dexbot.strategies.base import StrategyBase, ConfigElement +from dexbot.qt_queue.idle_queue import idle_add + +# Third party imports +from bitshares.market import Market + +STRATEGY_NAME = 'Strategy Template' + + +class Strategy(StrategyBase): + """ + + Replace with the name of the strategy. + + This is a template strategy which can be used to create custom strategies easier. The base for the strategy is + ready. It is recommended comment the strategy and functions to help other developers to make changes. + + Adding strategy to GUI + In dexbot.controller.strategy_controller add new strategy inside strategies() as show below: + + strategies['dexbot.strategies.strategy_template'] = { + 'name': '', + 'form_module': '' + } + + key: Strategy location in the project + name: The name that is shown in the GUI for user + form_module: If there is custom form module created with QTDesigner + + Adding strategy to CLI + In dexbot.cli_conf add strategy in to the STRATEGIES list + + {'tag': 'strategy_temp', + 'class': 'dexbot.strategies.strategy_template', + 'name': 'Template Strategy'}, + + NOTE: Change this comment section to describe the strategy. + """ + + @classmethod + def configure(cls, return_base_config=True): + """ This function is used to auto generate fields for GUI + + :param return_base_config: If base config is used in addition to this configuration. + :return: List of ConfigElement(s) + """ + + """ As a demonstration this template has two fields in the worker configuration. Upper and lower bound. + Documentation of ConfigElements can be found from base.py. + """ + return StrategyBase.configure(return_base_config) + [ + ConfigElement('upper_bound', 'float', 1, 'Max buy price', + 'Maximum price to pay in BASE.', + (0, None, 8, '')), + ConfigElement('lower_bound', 'float', 1, 'Min buy price', + 'Minimum price to pay in BASE.', + (0, None, 8, '')) + ] + + def __init__(self, *args, **kwargs): + # Initializes StrategyBase class + super().__init__(*args, **kwargs) + + """ Using self.log.info() you can print text on the GUI to inform user on what is the bot currently doing. This + is also written in the dexbot.log file. + """ + self.log.info("Initializing {}...".format(STRATEGY_NAME)) + + # Tick counter + self.counter = 0 + + # Define Callbacks + self.onMarketUpdate += self.maintain_strategy + self.onAccount += self.maintain_strategy + self.ontick += self.tick + + self.error_ontick = self.error + self.error_onMarketUpdate = self.error + self.error_onAccount = self.error + """ Define what strategy does on the following events + - Bitshares account has been modified = self.onAccount + - Market has been updated = self.onMarketUpdate + + These events are tied to methods which decide how the loop goes, unless the strategy is static, which + means that it will only do one thing and never do + """ + + # Get view + self.view = kwargs.get('view') + + """ Worker parameters + + There values are taken from the worker's config file. + Name of the worker is passed in the **kwargs. + """ + self.worker_name = kwargs.get('name') + + self.upper_bound = self.worker.get('upper_bound') + self.lower_bound = self.worker.get('lower_bound') + + """ Strategy variables + + These variables are for the strategy only and should be initialized here if wanted into self's scope. + """ + self.market_center_price = 0 + + if self.view: + self.update_gui_slider() + + def maintain_strategy(self): + """ Strategy main loop + + This method contains the strategy's logic. Keeping this function as simple as possible is recommended. + + Note: All orders are "buy" orders, since they are flipped to be easier to handle. Keep them separated to + avoid confusion on problems. + """ + # Todo: Clean this up once ready to release. + asset = 'TEST' + worker_value = self.calculate_worker_value(asset) + print('worker_value: {} as {}'.format(worker_value, asset)) + + asset = 'CODACOIN' + worker_value = self.calculate_worker_value(asset) + print('worker_value: {} as {}\n'.format(worker_value, asset)) + + print('BASE asset for this strategy is {}'.format(self.base_asset)) + print('QUOTE asset for this strategy is {}'.format(self.quote_asset)) + print('Market is (QUOTE/BASE) ({})'.format(self.worker.get('market'))) + + market_orders = self.get_market_orders(10) + + print('\nMarket buy orders') + for order in market_orders['bids']: + print(order) + + print('\nMarket sell orders') + for order in market_orders['asks']: + print(order) + + all_orders = self.get_own_orders + print('\nUser\'s orders') + for order in all_orders: + print(order) + + print('\nGet own BUY orders') + buy_orders = self.get_own_buy_orders() + for order in buy_orders: + print(order) + + print('\nHighest own buy order') + highest_own_buy_order = self.get_highest_own_buy(buy_orders) + print(highest_own_buy_order) + + print('\nGet own SELL orders') + sell_orders = self.get_own_sell_orders() + for order in sell_orders: + print(order) + + print('\nLowest own sell order') + lowest_own_sell_order = self.get_lowest_own_sell_order(sell_orders) + print(lowest_own_sell_order) + + print('Testing get_market_sell_price()') + quote_amount = 200 + base_amount = 1200 + sell_price = self.get_market_sell_price(quote_amount=quote_amount) + print('Sell price for {} CODACOIN {}'.format(quote_amount, sell_price)) + + sell_price = self.get_market_sell_price(base_amount=base_amount) + print('Sell price for {} TEST {}\n'.format(base_amount, sell_price)) + + print('Testing get_market_buy_price()') + buy_price = self.get_market_buy_price(quote_amount=quote_amount) + print('Buy price for {} CODACOIN is {}'.format(quote_amount, buy_price)) + + buy_price = self.get_market_buy_price(base_amount=base_amount) + print('Buy price for {} TEST is {}'.format(base_amount, buy_price)) + + self.market_center_price = self.get_market_center_price() + + """ Placing an order to the market has been made simple. Placing a buy order for example requires two values: + Amount (as QUOTE asset) and price (which is BASE amount divided by QUOTE amount) + + "Placing to buy 100 ASSET_A with price of 10 ASSET_A / ASSET_B" would be place_market_buy_order(100, 10). + This would then cost 1000 USD to fulfil. + + Further documentation can be found from the function's documentation. + """ + + # Place BUY order to the market + # self.place_market_buy_order(highest_own_buy_order['quote']['amount'], highest_own_buy_order['price']) + # self.place_market_buy_order(100, 10) + + # Place SELL order to the market + # self.place_market_sell_order(lowest_own_sell_order['quote']['amount'], lowest_own_sell_order['price']) + + def check_orders(self, *args, **kwargs): + """ """ + pass + + def error(self, *args, **kwargs): + """ Defines what happens when error occurs """ + self.disabled = False + + def pause(self): + """ Override pause() in StrategyBase """ + pass + + def tick(self, d): + """ Ticks come in on every block """ + if not (self.counter or 0) % 3: + self.maintain_strategy() + self.counter += 1 + + def update_gui_slider(self): + """ Updates GUI slider on the workers list """ + # Todo: Need's fixing? + latest_price = self.ticker().get('latest', {}).get('price', None) + if not latest_price: + return + + order_ids = None + orders = self.get_own_orders + + if orders: + order_ids = [order['id'] for order in orders if 'id' in order] + + total_balance = self.count_asset(order_ids) + total = (total_balance['quote'] * latest_price) + total_balance['base'] + + if not total: # Prevent division by zero + percentage = 50 + else: + percentage = (total_balance['base'] / total) * 100 + idle_add(self.view.set_worker_slider, self.worker_name, percentage) + self['slider'] = percentage From 9da827c96cd525f05ea094dc8fbf12c640f569b4 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 09:34:27 +0300 Subject: [PATCH 30/42] Update cli_conf.py to use new StrategyBase --- dexbot/cli_conf.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dexbot/cli_conf.py b/dexbot/cli_conf.py index e2ca4c9b1..e3260b480 100644 --- a/dexbot/cli_conf.py +++ b/dexbot/cli_conf.py @@ -7,7 +7,7 @@ Requires the 'whiptail' tool for text-based configuration (so UNIX only) if not available, falls back to a line-based configurator ("NoWhiptail") -Note there is some common cross-UI configuration stuff: look in basestrategy.py +Note there is some common cross-UI configuration stuff: look in base.py It's expected GUI/web interfaces will be re-implementing code in this file, but they should understand the common code so worker strategy writers can define their configuration once for each strategy class. @@ -22,7 +22,7 @@ import subprocess from dexbot.whiptail import get_whiptail -from dexbot.basestrategy import BaseStrategy +from dexbot.strategies.base import StrategyBase import dexbot.helper STRATEGIES = [ @@ -217,14 +217,14 @@ def configure_dexbot(config, ctx): worker_name = whiptail.menu("Select worker to edit", [(i, i) for i in workers]) config['workers'][worker_name] = configure_worker(whiptail, config['workers'][worker_name]) - strategy = BaseStrategy(worker_name, bitshares_instance=bitshares_instance, config=config) - strategy.purge() + strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) + strategy.clear_all_worker_data() elif action == 'DEL': worker_name = whiptail.menu("Select worker to delete", [(i, i) for i in workers]) del config['workers'][worker_name] - strategy = BaseStrategy(worker_name, bitshares_instance=bitshares_instance, config=config) - strategy.purge() + strategy = StrategyBase(worker_name, bitshares_instance=bitshares_instance, config=config) + strategy.clear_all_worker_data() elif action == 'NEW': txt = whiptail.prompt("Your name for the new worker") config['workers'][txt] = configure_worker(whiptail, {}) From 6de9454126a0aa4b3edfba3489ce991819bd76ff Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 09:35:08 +0300 Subject: [PATCH 31/42] Update worker.py to use new StrategyBase --- dexbot/worker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/worker.py b/dexbot/worker.py index 15f2d011c..6af9f742f 100644 --- a/dexbot/worker.py +++ b/dexbot/worker.py @@ -6,7 +6,7 @@ import copy import dexbot.errors as errors -from dexbot.basestrategy import BaseStrategy +from dexbot.strategies.base import StrategyBase from bitshares import BitShares from bitshares.notify import Notify @@ -225,12 +225,12 @@ def remove_market(self, worker_name): @staticmethod def remove_offline_worker(config, worker_name, bitshares_instance): # Initialize the base strategy to get control over the data - strategy = BaseStrategy(worker_name, config, bitshares_instance=bitshares_instance) + strategy = StrategyBase(worker_name, config, bitshares_instance=bitshares_instance) strategy.purge() @staticmethod def remove_offline_worker_data(worker_name): - BaseStrategy.purge_worker_data(worker_name) + StrategyBase.purge_all_local_worker_data(worker_name) def do_next_tick(self, job): """ Add a callable to be executed on the next tick """ From 0310770ab25c42d7a84c59e0563b9b5c948750ca Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 09:35:33 +0300 Subject: [PATCH 32/42] Refactor Relative Orders to work with StrategyBase --- dexbot/strategies/relative_orders.py | 78 +++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 7 deletions(-) diff --git a/dexbot/strategies/relative_orders.py b/dexbot/strategies/relative_orders.py index a9f638a90..8e59b0cf5 100644 --- a/dexbot/strategies/relative_orders.py +++ b/dexbot/strategies/relative_orders.py @@ -1,17 +1,17 @@ import math from datetime import datetime, timedelta -from dexbot.basestrategy import BaseStrategy, ConfigElement +from dexbot.strategies.base import StrategyBase, ConfigElement from dexbot.qt_queue.idle_queue import idle_add -class Strategy(BaseStrategy): +class Strategy(StrategyBase): """ Relative Orders strategy """ @classmethod def configure(cls, return_base_config=True): - return BaseStrategy.configure(return_base_config) + [ + return StrategyBase.configure(return_base_config) + [ ConfigElement('amount', 'float', 1, 'Amount', 'Fixed order size, expressed in quote asset, unless "relative order size" selected', (0, None, 8, '')), @@ -166,7 +166,7 @@ def update_orders(self): self.calculate_order_prices() # Cancel the orders before redoing them - self.cancel_all() + self.cancel_all_orders() self.clear_orders() order_ids = [] @@ -177,7 +177,7 @@ def update_orders(self): # Buy Side if amount_base: - buy_order = self.market_buy(amount_base, self.buy_price, True) + buy_order = self.place_market_buy_order(amount_base, self.buy_price, True) if buy_order: self.save_order(buy_order) order_ids.append(buy_order['id']) @@ -185,7 +185,7 @@ def update_orders(self): # Sell Side if amount_quote: - sell_order = self.market_sell(amount_quote, self.sell_price, True) + sell_order = self.place_market_sell_order(amount_quote, self.sell_price, True) if sell_order: self.save_order(sell_order) order_ids.append(sell_order['id']) @@ -199,6 +199,70 @@ def update_orders(self): if len(order_ids) < expected_num_orders and not self.disabled: self.update_orders() + def _calculate_center_price(self, suppress_errors=False): + ticker = self.market.ticker() + highest_bid = ticker.get("highestBid") + lowest_ask = ticker.get("lowestAsk") + if highest_bid is None or highest_bid == 0.0: + if not suppress_errors: + self.log.critical( + "Cannot estimate center price, there is no highest bid." + ) + self.disabled = True + return None + elif lowest_ask is None or lowest_ask == 0.0: + if not suppress_errors: + self.log.critical( + "Cannot estimate center price, there is no lowest ask." + ) + self.disabled = True + return None + + center_price = highest_bid['price'] * math.sqrt(lowest_ask['price'] / highest_bid['price']) + return center_price + + def calculate_center_price(self, center_price=None, asset_offset=False, spread=None, + order_ids=None, manual_offset=0, suppress_errors=False): + """ Calculate center price which shifts based on available funds + """ + if center_price is None: + # No center price was given so we simply calculate the center price + calculated_center_price = self._calculate_center_price(suppress_errors) + else: + # Center price was given so we only use the calculated center price + # for quote to base asset conversion + calculated_center_price = self._calculate_center_price(True) + if not calculated_center_price: + calculated_center_price = center_price + + if center_price: + calculated_center_price = center_price + + if asset_offset: + total_balance = self.count_asset(order_ids) + total = (total_balance['quote'] * calculated_center_price) + total_balance['base'] + + if not total: # Prevent division by zero + balance = 0 + else: + # Returns a value between -1 and 1 + balance = (total_balance['base'] / total) * 2 - 1 + + if balance < 0: + # With less of base asset center price should be offset downward + calculated_center_price = calculated_center_price / math.sqrt(1 + spread * (balance * -1)) + elif balance > 0: + # With more of base asset center price will be offset upwards + calculated_center_price = calculated_center_price * math.sqrt(1 + spread * balance) + else: + calculated_center_price = calculated_center_price + + # Calculate final_offset_price if manual center price offset is given + if manual_offset: + calculated_center_price = calculated_center_price + (calculated_center_price * manual_offset) + + return calculated_center_price + def check_orders(self, *args, **kwargs): """ Tests if the orders need updating """ @@ -287,7 +351,7 @@ def update_gui_slider(self): if orders: order_ids = orders.keys() - total_balance = self.total_balance(order_ids) + total_balance = self.count_asset(order_ids) total = (total_balance['quote'] * latest_price) + total_balance['base'] if not total: # Prevent division by zero From 90e1c447c972feea82e01d0054b6d4ceb6d9b9e3 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 09:39:24 +0300 Subject: [PATCH 33/42] Change basestrategy.rst to strategybase.rst --- docs/{basestrategy.rst => strategybase.rst} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename docs/{basestrategy.rst => strategybase.rst} (50%) diff --git a/docs/basestrategy.rst b/docs/strategybase.rst similarity index 50% rename from docs/basestrategy.rst rename to docs/strategybase.rst index b3cb61578..910239d64 100644 --- a/docs/basestrategy.rst +++ b/docs/strategybase.rst @@ -1,13 +1,13 @@ ************* -Base Strategy +Strategy Base ************* All strategies should inherit -:class:`dexbot.basestrategy.BaseStrategy` which simplifies and +:class:`dexbot.strategies.StrategyBase` which simplifies and unifies the development of new strategies. API --- -.. autoclass:: dexbot.basestrategy.BaseStrategy +.. autoclass:: dexbot.strategies.StrategyBase :members: From 5d70892126e4c2766add3b7cef6af0c59b6a0d61 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 09:40:18 +0300 Subject: [PATCH 34/42] Remove enchance_center_price() --- dexbot/strategies/base.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index e2205666e..fcc7b352f 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -861,19 +861,6 @@ def get_updated_order(self, order_id): order = self.get_updated_limit_order(order) return Order(order, bitshares_instance=self.bitshares) - def enhance_center_price(self, reference=None, manual_offset=False, balance_based_offset=False, - moving_average=0, weighted_average=0): - """ Returns the passed reference price shifted up or down based on arguments. - - :param float | reference: Center price to enhance - :param bool | manual_offset: - :param bool | balance_based_offset: - :param int or float | moving_average: - :param int or float | weighted_average: - :return: - """ - # Todo: Remove this after market center price is done - def is_current_market(self, base_asset_id, quote_asset_id): """ Returns True if given asset id's are of the current market From 3a0df0ce03b69374d80b8cf862097d79fac6d5d5 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 09:41:22 +0300 Subject: [PATCH 35/42] Refactor pause_worker() to pause() --- dexbot/strategies/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index fcc7b352f..c6556198e 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -879,7 +879,7 @@ def is_current_market(self, base_asset_id, quote_asset_id): return False - def pause_worker(self): + def pause(self): """ Pause the worker Note: By default pause cancels orders, but this can be overridden by strategy From a2acbfd870aff55c4010a8570d5acc3c951bfab7 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 09:51:27 +0300 Subject: [PATCH 36/42] Update echo.py to use StrategyBase --- dexbot/strategies/echo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dexbot/strategies/echo.py b/dexbot/strategies/echo.py index c7a732aaa..264f1027c 100644 --- a/dexbot/strategies/echo.py +++ b/dexbot/strategies/echo.py @@ -1,7 +1,7 @@ -from dexbot.basestrategy import BaseStrategy +from dexbot.strategies.base import StrategyBase -class Strategy(BaseStrategy): +class Strategy(StrategyBase): """ Echo strategy Strategy that logs all events within the blockchain """ From 2bd72c2fb415482deae99a29e9ce6329a32a4a36 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 13:16:58 +0300 Subject: [PATCH 37/42] Remove get_external_price() --- dexbot/strategies/base.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index c6556198e..09279fc4d 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -468,14 +468,6 @@ def get_allocated_assets(self, order_ids=None, return_asset=False): return {'quote': quote, 'base': base} - def get_external_price(self, source): - """ Returns the center price of market including own orders. - - :param source: - :return: - """ - # Todo: Insert logic here - def get_market_fee(self): """ Returns the fee percentage for buying specified asset From f5eb0608d6bcc94e4585e2ac0dc7751ac4a61319 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 13:17:32 +0300 Subject: [PATCH 38/42] Change comments on function descriptions --- dexbot/strategies/base.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index 09279fc4d..e51704cb8 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -53,13 +53,11 @@ class StrategyBase(Storage, StateMachine, Events): - Buy orders reserve BASE - Sell orders reserve QUOTE - Todo: This is copy / paste from old, update this if needed! Strategy inherits: * :class:`dexbot.storage.Storage` : Stores data to sqlite database * :class:`dexbot.statemachine.StateMachine` * ``Events`` - Todo: This is copy / paste from old, update this if needed! Available attributes: * ``worker.bitshares``: instance of ´`bitshares.BitShares()`` * ``worker.add_state``: Add a specific state @@ -124,7 +122,6 @@ def configure(cls, return_base_config=True): r'[A-Z\.]+') ] - # Todo: Is there any case / strategy where the base config would NOT be needed, making this unnecessary? if return_base_config: return base_config return [] @@ -232,7 +229,6 @@ def __init__(self, def _callbackPlaceFillOrders(self, d): """ This method distinguishes notifications caused by Matched orders from those caused by placed orders - Todo: can this be renamed to _instantFill()? """ if isinstance(d, FilledOrder): self.onOrderMatched(d) @@ -285,7 +281,6 @@ def account_total_value(self, return_asset): # Orders balance calculation for order in self.all_own_orders: - # Todo: What is the purpose of this? updated_order = self.get_updated_order(order['id']) if not order: @@ -302,13 +297,12 @@ def account_total_value(self, return_asset): return total_value def balance(self, asset, fee_reservation=0): - """ Return the balance of your worker's account for a specific asset + """ Return the balance of your worker's account in a specific asset. - :param string | asset: - :param bool | fee_reservation: + :param string | asset: In what asset the balance is wanted to be returned + :param float | fee_reservation: How much is saved in reserve for the fees :return: Balance of specific asset """ - # Todo: Check that fee reservation was as intended, having it true / false made no sense balance = self._account.balance(asset) if fee_reservation > 0: @@ -379,11 +373,11 @@ def cancel_all_orders(self): self.log.info("Orders canceled") def cancel_orders(self, orders, batch_only=False): - """ Cancel specific order or orders + """ Cancel specific order(s) :param list | orders: List of orders to cancel :param bool | batch_only: Try cancel orders only in batch mode without one-by-one fallback - :return: Todo: Add documentation + :return: """ if not isinstance(orders, (list, set, tuple)): orders = [orders] @@ -820,7 +814,6 @@ def get_own_spread(self): :return: float or None: Own spread """ - try: # Try fetching own orders highest_own_buy_price = self.get_highest_market_buy_order().get('price') From 03e48526a2b9de7391f5695d613a2c4e0746cf1a Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 13:17:56 +0300 Subject: [PATCH 39/42] Fix problem where old cancel method was called --- dexbot/strategies/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index e51704cb8..c9711eaff 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -368,7 +368,7 @@ def cancel_all_orders(self): self.log.info('Canceling all orders') if self.all_own_orders: - self.cancel(self.all_own_orders) + self.cancel_orders(self.all_own_orders) self.log.info("Orders canceled") @@ -870,7 +870,7 @@ def pause(self): Note: By default pause cancels orders, but this can be overridden by strategy """ # Cancel all orders from the market - self.cancel_all() + self.cancel_all_orders() # Removes worker's orders from local database self.clear_orders() @@ -882,7 +882,7 @@ def clear_all_worker_data(self): self.clear_orders() # Cancel all orders from the market - self.cancel_all() + self.cancel_all_orders() # Finally clear all worker data from the database self.clear() From 05841583d04763f5092e322c3d60fcfee20ff934 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 13:18:23 +0300 Subject: [PATCH 40/42] Remove code that was used for testing --- dexbot/strategies/strategy_template.py | 85 +++----------------------- 1 file changed, 9 insertions(+), 76 deletions(-) diff --git a/dexbot/strategies/strategy_template.py b/dexbot/strategies/strategy_template.py index 44ca214ec..71ae28d29 100644 --- a/dexbot/strategies/strategy_template.py +++ b/dexbot/strategies/strategy_template.py @@ -20,7 +20,7 @@ class Strategy(StrategyBase): ready. It is recommended comment the strategy and functions to help other developers to make changes. Adding strategy to GUI - In dexbot.controller.strategy_controller add new strategy inside strategies() as show below: + In dexbot.controller.worker_controller add new strategy inside strategies() as show below: strategies['dexbot.strategies.strategy_template'] = { 'name': '', @@ -111,6 +111,8 @@ def __init__(self, *args, **kwargs): if self.view: self.update_gui_slider() + self.log.info("{} initialized.".format(STRATEGY_NAME)) + def maintain_strategy(self): """ Strategy main loop @@ -118,86 +120,18 @@ def maintain_strategy(self): Note: All orders are "buy" orders, since they are flipped to be easier to handle. Keep them separated to avoid confusion on problems. - """ - # Todo: Clean this up once ready to release. - asset = 'TEST' - worker_value = self.calculate_worker_value(asset) - print('worker_value: {} as {}'.format(worker_value, asset)) - - asset = 'CODACOIN' - worker_value = self.calculate_worker_value(asset) - print('worker_value: {} as {}\n'.format(worker_value, asset)) - - print('BASE asset for this strategy is {}'.format(self.base_asset)) - print('QUOTE asset for this strategy is {}'.format(self.quote_asset)) - print('Market is (QUOTE/BASE) ({})'.format(self.worker.get('market'))) - - market_orders = self.get_market_orders(10) - - print('\nMarket buy orders') - for order in market_orders['bids']: - print(order) - - print('\nMarket sell orders') - for order in market_orders['asks']: - print(order) - - all_orders = self.get_own_orders - print('\nUser\'s orders') - for order in all_orders: - print(order) - - print('\nGet own BUY orders') - buy_orders = self.get_own_buy_orders() - for order in buy_orders: - print(order) - - print('\nHighest own buy order') - highest_own_buy_order = self.get_highest_own_buy(buy_orders) - print(highest_own_buy_order) - - print('\nGet own SELL orders') - sell_orders = self.get_own_sell_orders() - for order in sell_orders: - print(order) - - print('\nLowest own sell order') - lowest_own_sell_order = self.get_lowest_own_sell_order(sell_orders) - print(lowest_own_sell_order) - print('Testing get_market_sell_price()') - quote_amount = 200 - base_amount = 1200 - sell_price = self.get_market_sell_price(quote_amount=quote_amount) - print('Sell price for {} CODACOIN {}'.format(quote_amount, sell_price)) - - sell_price = self.get_market_sell_price(base_amount=base_amount) - print('Sell price for {} TEST {}\n'.format(base_amount, sell_price)) - - print('Testing get_market_buy_price()') - buy_price = self.get_market_buy_price(quote_amount=quote_amount) - print('Buy price for {} CODACOIN is {}'.format(quote_amount, buy_price)) - - buy_price = self.get_market_buy_price(base_amount=base_amount) - print('Buy price for {} TEST is {}'.format(base_amount, buy_price)) - - self.market_center_price = self.get_market_center_price() - - """ Placing an order to the market has been made simple. Placing a buy order for example requires two values: + Placing an order to the market has been made simple. Placing a buy order for example requires two values: Amount (as QUOTE asset) and price (which is BASE amount divided by QUOTE amount) - + "Placing to buy 100 ASSET_A with price of 10 ASSET_A / ASSET_B" would be place_market_buy_order(100, 10). This would then cost 1000 USD to fulfil. - - Further documentation can be found from the function's documentation. - """ - # Place BUY order to the market - # self.place_market_buy_order(highest_own_buy_order['quote']['amount'], highest_own_buy_order['price']) - # self.place_market_buy_order(100, 10) + Further documentation can be found from the function's documentation. - # Place SELL order to the market - # self.place_market_sell_order(lowest_own_sell_order['quote']['amount'], lowest_own_sell_order['price']) + """ + # Start writing strategy logic from here. + self.log.info("Starting {}".format(STRATEGY_NAME)) def check_orders(self, *args, **kwargs): """ """ @@ -219,7 +153,6 @@ def tick(self, d): def update_gui_slider(self): """ Updates GUI slider on the workers list """ - # Todo: Need's fixing? latest_price = self.ticker().get('latest', {}).get('price', None) if not latest_price: return From e089dcaf5f3728d68dc2d5a8d9bf3d0fa5e7fcfe Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 13:42:41 +0300 Subject: [PATCH 41/42] Change example fields to be more clear --- dexbot/strategies/strategy_template.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dexbot/strategies/strategy_template.py b/dexbot/strategies/strategy_template.py index 71ae28d29..211223e53 100644 --- a/dexbot/strategies/strategy_template.py +++ b/dexbot/strategies/strategy_template.py @@ -53,12 +53,12 @@ def configure(cls, return_base_config=True): Documentation of ConfigElements can be found from base.py. """ return StrategyBase.configure(return_base_config) + [ - ConfigElement('upper_bound', 'float', 1, 'Max buy price', - 'Maximum price to pay in BASE.', - (0, None, 8, '')), - ConfigElement('lower_bound', 'float', 1, 'Min buy price', - 'Minimum price to pay in BASE.', - (0, None, 8, '')) + ConfigElement('lower_bound', 'float', 1, 'Lower bound', + 'The bottom price in the range', + (0, 10000000, 8, '')), + ConfigElement('upper_bound', 'float', 10, 'Upper bound', + 'The top price in the range', + (0, 10000000, 8, '')), ] def __init__(self, *args, **kwargs): From a1928d93e1d50e3198f157afa802965a715d84b8 Mon Sep 17 00:00:00 2001 From: joelva Date: Fri, 14 Sep 2018 13:43:16 +0300 Subject: [PATCH 42/42] Add support for the old BaseStrategy --- dexbot/strategies/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dexbot/strategies/base.py b/dexbot/strategies/base.py index c9711eaff..e7e7abf02 100644 --- a/dexbot/strategies/base.py +++ b/dexbot/strategies/base.py @@ -5,6 +5,7 @@ import math import time +from dexbot.basestrategy import BaseStrategy # Todo: Once the old BaseStrategy deprecates, remove it. from dexbot.config import Config from dexbot.storage import Storage from dexbot.statemachine import StateMachine @@ -44,7 +45,7 @@ ConfigElement = collections.namedtuple('ConfigElement', 'key type default title description extra') -class StrategyBase(Storage, StateMachine, Events): +class StrategyBase(BaseStrategy, Storage, StateMachine, Events): """ A strategy based on this class is intended to work in one market. This class contains most common methods needed by the strategy.