Beispiel #1
0
def log_print_startup_message():
    #Logger.Write("**** COMMODORE 64 BASIC V2  64K RAM SYSTEM  38911 BASIC BYTES FREE ****", echo=False)
    try:
        spending_account = Account(tip_sender, blockchain_instance=blockchain)
        avail_balance = spending_account.balance(tip_asset_symbol)
    except AccountDoesNotExistsException:
        print("ERROR: Sending account does not exist on BitShares network.")
        exit()
    except AssetDoesNotExistsException:
        print("ERROR: Chosen asset does not exist on BitShares network.")
        exit()
    Logger.Write("Sending tips from BitShares acount: \"%s\" (%s)" %
                 (spending_account.name, spending_account.identifier))
    Logger.Write("Available balance: %0.5f %s" %
                 (avail_balance, tip_asset_display_symbol))
    Logger.Write("READY.", echo=False)
Beispiel #2
0
    def test_account(self):
        Account("init0")
        Account("1.2.3")
        account = Account("init0", full=True)
        self.assertEqual(account.name, "init0")
        self.assertEqual(account["name"], account.name)
        self.assertEqual(account["id"], "1.2.100")
        self.assertIsInstance(account.balance("1.3.0"), Amount)
        # self.assertIsInstance(account.balance({"symbol": symbol}), Amount)
        self.assertIsInstance(account.balances, list)
        for _ in account.history(limit=1):
            pass

        # BlockchainObjects method
        account.cached = False
        self.assertTrue(account.items())
        account.cached = False
        self.assertIn("id", account)
        account.cached = False
        self.assertEqual(account["id"], "1.2.100")
        self.assertTrue(str(account).startswith("<Account "))
        self.assertIsInstance(Account(account), Account)
Beispiel #3
0
    def test_account(self):
        Account("witness-account")
        Account("1.2.3")
        asset = Asset("1.3.0")
        symbol = asset["symbol"]
        account = Account("witness-account", full=True)
        self.assertEqual(account.name, "witness-account")
        self.assertEqual(account["name"], account.name)
        self.assertEqual(account["id"], "1.2.1")
        self.assertIsInstance(account.balance("1.3.0"), Amount)
        # self.assertIsInstance(account.balance({"symbol": symbol}), Amount)
        self.assertIsInstance(account.balances, list)
        for h in account.history(limit=1):
            pass

        # BlockchainObjects method
        account.cached = False
        self.assertTrue(account.items())
        account.cached = False
        self.assertIn("id", account)
        account.cached = False
        self.assertEqual(account["id"], "1.2.1")
        self.assertEqual(str(account), "<Account 1.2.1>")
        self.assertIsInstance(Account(account), Account)
Beispiel #4
0
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.

        All prices are passed and returned as BASE/QUOTE.
        (In the BREAD:USD market that would be USD/BREAD, 2.5 USD / 1 BREAD).
         - Buy orders reserve BASE
         - Sell orders reserve QUOTE

        Strategy inherits:
            * :class:`dexbot.storage.Storage` : Stores data to sqlite database
            * :class:`dexbot.statemachine.StateMachine`
            * ``Events``

        Available attributes:
            * ``worker.bitshares``: instance of ´`bitshares.BitShares()``
            * ``worker.add_state``: Add a specific state
            * ``worker.set_state``: Set finite state machine
            * ``worker.get_state``: Change state of state machine
            * ``worker.account``: The Account object of this worker
            * ``worker.market``: The market used by this worker
            * ``worker.orders``: List of open orders of the worker's account in the worker's market
            * ``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\.]+')
        ]

        if return_base_config:
            return base_config
        return []

    def __init__(self,
                 name,
                 config=None,
                 onAccount=None,
                 onOrderMatched=None,
                 onOrderPlaced=None,
                 onMarketUpdate=None,
                 onUpdateCallOrder=None,
                 ontick=None,
                 bitshares_instance=None,
                 *args,
                 **kwargs):

        # BitShares instance
        self.bitshares = bitshares_instance or shared_bitshares_instance()

        # Dex instance used to get different fees for the market
        self.dex = Dex(self.bitshares)

        # Storage
        Storage.__init__(self, name)

        # Statemachine
        StateMachine.__init__(self, name)

        # Events
        Events.__init__(self)

        if ontick:
            self.ontick += ontick
        if onMarketUpdate:
            self.onMarketUpdate += onMarketUpdate
        if onAccount:
            self.onAccount += onAccount
        if onOrderMatched:
            self.onOrderMatched += onOrderMatched
        if onOrderPlaced:
            self.onOrderPlaced += onOrderPlaced
        if onUpdateCallOrder:
            self.onUpdateCallOrder += onUpdateCallOrder

        # Redirect this event to also call order placed and order matched
        self.onMarketUpdate += self._callbackPlaceFillOrders

        if config:
            self.config = config
        else:
            self.config = config = Config.get_worker_config_file(name)

        # Get worker's parameters from the config
        self.worker = config["workers"][name]

        # Get Bitshares account and market for this worker
        self._account = Account(self.worker["account"],
                                full=True,
                                bitshares_instance=self.bitshares)
        self._market = Market(config["workers"][name]["market"],
                              bitshares_instance=self.bitshares)

        # Recheck flag - Tell the strategy to check for updated orders
        self.recheck_orders = False

        # Count of orders to be fetched from the API
        self.fetch_depth = 8

        # Set fee asset
        fee_asset_symbol = self.worker.get('fee_asset')

        if fee_asset_symbol:
            try:
                self.fee_asset = Asset(fee_asset_symbol)
            except bitshares.exceptions.AssetDoesNotExistsException:
                self.fee_asset = Asset('1.3.0')
        else:
            # If there is no fee asset, use BTS
            self.fee_asset = Asset('1.3.0')

        # Ticker
        self.ticker = self.market.ticker

        # Settings for bitshares instance
        self.bitshares.bundle = bool(self.worker.get("bundle", False))

        # Disabled flag - this flag can be flipped to True by a worker and will be reset to False after reset only
        self.disabled = False

        # Order expiration time in seconds
        self.expiration = 60 * 60 * 24 * 365 * 5

        # buy/sell actions will return order id by default
        self.returnOrderId = 'head'

        # A private logger that adds worker identify data to the LogRecord
        self.log = logging.LoggerAdapter(
            logging.getLogger('dexbot.per_worker'), {
                'worker_name': name,
                'account': self.worker['account'],
                'market': self.worker['market'],
                'is_disabled': lambda: self.disabled
            })

        self.orders_log = logging.LoggerAdapter(
            logging.getLogger('dexbot.orders_log'), {})

    def _callbackPlaceFillOrders(self, d):
        """ This method distinguishes notifications caused by Matched orders from those caused by placed orders
        """
        if isinstance(d, FilledOrder):
            self.onOrderMatched(d)
        elif isinstance(d, Order):
            self.onOrderPlaced(d)
        elif isinstance(d, UpdateCallOrder):
            self.onUpdateCallOrder(d)
        else:
            pass

    def _cancel_orders(self, orders):
        """

            :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=0):
        """ Return the balance of your worker's account in a specific asset.

            :param string | asset: In what asset the balance is wanted to be returned
            :param float | fee_reservation: How much is saved in reserve for the fees
            :return: Balance of specific asset
        """
        balance = self._account.balance(asset)

        if fee_reservation > 0:
            balance['amount'] = balance['amount'] - fee_reservation

        return balance

    def calculate_order_data(self, order, amount, price):
        """

            :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
        base_asset = Amount(amount * price, self.market['base']['symbol'])
        order['base'] = base_asset
        return order

    def calculate_worker_value(self, unit_of_measure):
        """ Returns the combined value of allocated and available BASE and QUOTE. Total value is
            measured in "unit_of_measure", which is either BASE or QUOTE symbol.

            :param string | unit_of_measure: Asset symbol
            :return: Value of the worker as float
        """
        base_total = 0
        quote_total = 0

        # Calculate total balances
        balances = self.balances
        for balance in balances:
            if balance['symbol'] == self.base_asset:
                base_total += balance['amount']
            elif balance['symbol'] == self.quote_asset:
                quote_total += balance['amount']

        # Calculate value of the orders in unit of measure
        orders = self.get_own_orders
        for order in orders:
            if order['base']['symbol'] == self.quote_asset:
                # Pick sell orders order's BASE amount, which is same as worker's QUOTE, to worker's BASE
                quote_total += order['base']['amount']
            else:
                base_total += order['base']['amount']

        # Finally convert asset to another and return the sum
        if unit_of_measure == self.base_asset:
            quote_total = self.convert_asset(quote_total, self.quote_asset,
                                             unit_of_measure)
        elif unit_of_measure == self.quote_asset:
            base_total = self.convert_asset(base_total, self.base_asset,
                                            unit_of_measure)

        # Fixme: Make sure that decimal precision is correct.
        return base_total + quote_total

    def cancel_all_orders(self):
        """ Cancel all orders of the worker's account
        """
        self.log.info('Canceling all orders')

        if self.all_own_orders:
            self.cancel_orders(self.all_own_orders)

        self.log.info("Orders canceled")

    def cancel_orders(self, orders, batch_only=False):
        """ Cancel specific order(s)

            :param list | orders: List of orders to cancel
            :param bool | batch_only: Try cancel orders only in batch mode without one-by-one fallback
            :return:
        """
        if not isinstance(orders, (list, set, tuple)):
            orders = [orders]

        orders = [order['id'] for order in orders if 'id' in order]

        success = self._cancel_orders(orders)
        if not success and batch_only:
            return False
        if not success and len(orders) > 1 and not batch_only:
            # One of the order cancels failed, cancel the orders one by one
            for order in orders:
                self._cancel_orders(order)
        return 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

            :param list | order_ids: list of order ids to be added to the balance
            :param bool | return_asset: true if returned values should be Amount instances
            :return: dict with keys quote and base
            Todo: When would we want the sum of a subset of orders? Why order_ids? Maybe just specify asset?
        """
        quote = 0
        base = 0
        quote_asset = self.market['quote']['id']
        base_asset = self.market['base']['id']

        # Total balance calculation
        for balance in self.balances:
            if balance.asset['id'] == quote_asset:
                quote += balance['amount']
            elif balance.asset['id'] == base_asset:
                base += balance['amount']

        if order_ids is None:
            # Get all orders from Blockchain
            order_ids = [order['id'] for order in self.get_own_orders]
        if order_ids:
            orders_balance = self.get_allocated_assets(order_ids)
            quote += orders_balance['quote']
            base += orders_balance['base']

        if return_asset:
            quote = Amount(quote, quote_asset)
            base = Amount(base, base_asset)

        return {'quote': quote, 'base': base}

    def get_allocated_assets(self, order_ids=None, return_asset=False):
        """ Returns the amount of QUOTE and BASE allocated in orders, and that do not show up in available balance

            :param list | order_ids:
            :param bool | return_asset:
            :return: Dictionary of QUOTE and BASE amounts
        """
        if not order_ids:
            order_ids = []
        elif isinstance(order_ids, str):
            order_ids = [order_ids]

        quote = 0
        base = 0
        quote_asset = self.market['quote']['id']
        base_asset = self.market['base']['id']

        for order_id in order_ids:
            order = self.get_updated_order(order_id)
            if not order:
                continue
            asset_id = order['base']['asset']['id']
            if asset_id == quote_asset:
                quote += order['base']['amount']
            elif asset_id == base_asset:
                base += order['base']['amount']

        # Return as Amount objects instead of only float values
        if return_asset:
            quote = Amount(quote, quote_asset)
            base = Amount(base, base_asset)

        return {'quote': quote, 'base': base}

    def get_market_fee(self):
        """ Returns the fee percentage for buying specified asset

            :return: Fee percentage in decimal form (0.025)
        """
        return self.fee_asset.market_fee_percent

    def get_market_buy_orders(self, depth=10):
        """ Fetches most reset data and returns list of buy orders.

            :param int | depth: Amount of buy orders returned, Default=10
            :return: List of market sell orders
        """
        return self.get_market_orders(depth=depth)['bids']

    def get_market_sell_orders(self, depth=10):
        """ Fetches most reset data and returns list of sell orders.

            :param int | depth: Amount of sell orders returned, Default=10
            :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.

            :param list | orders: Optional list of orders, if none given fetch newest from market
            :return: Highest market buy order or None
        """
        if not orders:
            orders = self.get_market_buy_orders(1)

        try:
            order = orders[0]
        except IndexError:
            self.log.info('Market has no buy orders.')
            return None

        return order

    def get_highest_own_buy(self, orders=None):
        """ Returns highest own buy order.

            :param list | orders:
            :return: Highest own buy order by price at the market or None
        """
        if not orders:
            orders = self.get_own_buy_orders()

        try:
            return orders[0]
        except IndexError:
            return None

    def get_lowest_market_sell_order(self, orders=None):
        """ Returns the lowest sell order that is not own, regardless of order size.

            :param list | orders: Optional list of orders, if none given fetch newest from market
            :return: Lowest market sell order or None
        """
        if not orders:
            orders = self.get_market_sell_orders(1)

        try:
            order = orders[0]
        except IndexError:
            self.log.info('Market has no sell orders.')
            return None

        return order

    def get_lowest_own_sell_order(self, orders=None):
        """ Returns lowest own sell order.

            :param list | orders:
            :return: Lowest own sell order by price at the market
        """
        if not orders:
            orders = self.get_own_sell_orders()

        try:
            return orders[0]
        except IndexError:
            return None

    def get_market_center_price(self,
                                base_amount=0,
                                quote_amount=0,
                                suppress_errors=False):
        """ Returns the center price of market including own orders.

            :param float | base_amount:
            :param float | quote_amount:
            :param bool | suppress_errors:
            :return: Market center price as float
        """

        buy_price = self.get_market_buy_price(quote_amount=quote_amount,
                                              base_amount=base_amount)
        sell_price = self.get_market_sell_price(quote_amount=quote_amount,
                                                base_amount=base_amount)

        if buy_price is None or buy_price == 0.0:
            if not suppress_errors:
                self.log.critical(
                    "Cannot estimate center price, there is no highest bid.")
                self.disabled = True
            return None

        if sell_price is None or sell_price == 0.0:
            if not suppress_errors:
                self.log.critical(
                    "Cannot estimate center price, there is no lowest ask.")
                self.disabled = True
            return None

        # Calculate and return market center price
        return buy_price * math.sqrt(sell_price / buy_price)

    def get_market_buy_price(self, quote_amount=0, base_amount=0):
        """ Returns the BASE/QUOTE price for which [depth] worth of QUOTE could be bought, enhanced with
            moving average or weighted moving average

            :param float | quote_amount:
            :param float | base_amount:
            :return:
        """
        # 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')

        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
        missing_amount = target_amount

        for order in market_buy_orders:
            if base:
                # BASE amount was given
                if order['base']['amount'] <= missing_amount:
                    quote_amount += order['quote']['amount']
                    base_amount += order['base']['amount']
                    missing_amount -= order['base']['amount']
                else:
                    base_amount += missing_amount
                    quote_amount += missing_amount / order['price']
                    break
            elif not base:
                # QUOTE amount was given
                if order['quote']['amount'] <= missing_amount:
                    quote_amount += order['quote']['amount']
                    base_amount += order['base']['amount']
                    missing_amount -= order['quote']['amount']
                else:
                    base_amount += missing_amount * order['price']
                    quote_amount += missing_amount
                    break

        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.

            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=00):
        """ Returns the BASE/QUOTE price for which [quote_amount] worth of QUOTE could be bought,
            enhanced with moving average or weighted moving average.

            [quote/base]_amount = 0 means lowest regardless of size

            :param float | quote_amount:
            :param float | base_amount:
            :return:
        """
        # 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')

        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()

        target_amount = asset_amount * (1 + market_fee)

        quote_amount = 0
        base_amount = 0
        missing_amount = target_amount

        for order in market_sell_orders:
            if quote:
                # QUOTE amount was given
                if order['quote']['amount'] <= missing_amount:
                    quote_amount += order['quote']['amount']
                    base_amount += order['base']['amount']
                    missing_amount -= order['quote']['amount']
                else:
                    base_amount += missing_amount * order['price']
                    quote_amount += missing_amount
                    break
            elif not quote:
                # BASE amount was given
                if order['base']['amount'] <= missing_amount:
                    quote_amount += order['quote']['amount']
                    base_amount += order['base']['amount']
                    missing_amount -= order['base']['amount']
                else:
                    base_amount += missing_amount
                    quote_amount += missing_amount / order['price']
                    break

        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 float | quote_amount:
            :param float | base_amount:
            :return: Market spread as float or None
        """
        ask = self.get_market_sell_price(quote_amount=quote_amount,
                                         base_amount=base_amount)
        bid = self.get_market_buy_price(quote_amount=quote_amount,
                                        base_amount=base_amount)

        # Calculate market spread
        if ask == 0 or bid == 0:
            return None

        return ask / bid - 1

    def get_order_cancellation_fee(self, fee_asset):
        """ Returns the order cancellation fee in the specified asset.

            :param string | fee_asset: Asset in which the fee is wanted
            :return: Cancellation fee as fee asset
        """
        # Get fee
        fees = self.dex.returnFees()
        limit_order_cancel = fees['limit_order_cancel']

        # Convert fee
        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

            :param fee_asset: QUOTE, BASE, BTS, or any other
            :return:
        """
        # Get fee
        fees = self.dex.returnFees()
        limit_order_create = fees['limit_order_create']

        # Convert fee
        return self.convert_asset(limit_order_create['fee'], 'BTS', fee_asset)

    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, or from a set of orders passed for this function.

            :return: List of buy orders
        """
        if not orders:
            # List of orders was not given so fetch everything from the market
            orders = self.get_own_orders

        return self.filter_buy_orders(orders)

    def get_own_sell_orders(self, orders=None):
        """ Get own sell orders from current market

            :return: List of sell orders
        """
        if not orders:
            # List of orders was not given so fetch everything from the market
            orders = self.get_own_orders

        return self.filter_sell_orders(orders)

    def get_own_spread(self):
        """ Returns the difference between own closest opposite orders.

            :return: float or None: Own spread
        """
        try:
            # Try fetching own orders
            highest_own_buy_price = self.get_highest_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
        return actual_spread

    def get_updated_order(self, order_id):
        # Todo: This needed?
        """ Tries to get the updated order from the API. Returns None if the order doesn't exist

            :param str|dict order_id: blockchain object id of the order can be an order dict with the id key in it
        """
        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 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(self):
        """ Pause the worker

            Note: By default pause cancels orders, but this can be overridden by strategy
        """
        # Cancel all orders from the market
        self.cancel_all_orders()

        # Removes worker's orders from local database
        self.clear_orders()

    def clear_all_worker_data(self):
        """ Clear all the worker data from the database and cancel all orders
        """
        # Removes worker's orders from local database
        self.clear_orders()

        # Cancel all orders from the market
        self.cancel_all_orders()

        # Finally clear all worker data from the database
        self.clear()

    def place_market_buy_order(self,
                               amount,
                               price,
                               return_none=False,
                               *args,
                               **kwargs):
        """ Places a buy order in the market

            :param float | amount: Order amount in QUOTE
            :param float | price: Order price in BASE
            :param bool | return_none:
            :param args:
            :param kwargs:
            :return:
        """
        symbol = self.market['base']['symbol']
        precision = self.market['base']['precision']
        base_amount = truncate(price * amount, precision)

        # Don't try to place an order of size 0
        if not base_amount:
            self.log.critical('Trying to buy 0')
            self.disabled = True
            return None

        # Make sure we have enough balance for the order
        if self.returnOrderId and self.balance(
                self.market['base']) < base_amount:
            self.log.critical("Insufficient buy balance, needed {} {}".format(
                base_amount, symbol))
            self.disabled = True
            return None

        self.log.info('Placing a buy order for {} {} @ {:.8f}'.format(
            base_amount, symbol, price))

        # 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=self.returnOrderId,
                                            fee_asset=self.fee_asset['id'],
                                            *args,
                                            **kwargs)

        self.log.debug('Placed buy order {}'.format(buy_transaction))
        if self.returnOrderId:
            buy_order = self.get_order(buy_transaction['orderid'],
                                       return_none=return_none)
            if buy_order and buy_order['deleted']:
                # The API doesn't return data on orders that don't exist
                # We need to calculate the data on our own
                buy_order = self.calculate_order_data(buy_order, amount, price)
                self.recheck_orders = True
            return buy_order
        else:
            return True

    def place_market_sell_order(self,
                                amount,
                                price,
                                return_none=False,
                                *args,
                                **kwargs):
        """ Places a sell order in the market

            :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.returnOrderId and self.balance(
                self.market['quote']) < quote_amount:
            self.log.critical("Insufficient sell balance, needed {} {}".format(
                amount, symbol))
            self.disabled = True
            return None

        self.log.info('Placing a sell order for {} {} @ {:.8f}'.format(
            quote_amount, symbol, price))

        # 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=self.returnOrderId,
                                             fee_asset=self.fee_asset['id'],
                                             *args,
                                             **kwargs)

        self.log.debug('Placed sell order {}'.format(sell_transaction))
        if self.returnOrderId:
            sell_order = self.get_order(sell_transaction['orderid'],
                                        return_none=return_none)
            if sell_order and sell_order['deleted']:
                # The API doesn't return data on orders that don't exist, we need to calculate the data on our own
                sell_order = self.calculate_order_data(sell_order, amount,
                                                       price)
                sell_order.invert()
                self.recheck_orders = True
            return sell_order
        else:
            return True

    def retry_action(self, action, *args, **kwargs):
        """ Perform an action, and if certain suspected-to-be-spurious grapheme bugs occur,
            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):
        """ Write order log to csv file

            :param string | worker_name: Name of the worker
            :param object | order: Order that was fulfilled
        """
        operation_type = 'TRADE'

        if order['base']['symbol'] == self.market['base']['symbol']:
            base_symbol = order['base']['symbol']
            base_amount = -order['base']['amount']
            quote_symbol = order['quote']['symbol']
            quote_amount = order['quote']['amount']
        else:
            base_symbol = order['quote']['symbol']
            base_amount = order['quote']['amount']
            quote_symbol = order['base']['symbol']
            quote_amount = -order['base']['amount']

        message = '{};{};{};{};{};{};{};{}'.format(
            worker_name, order['id'], operation_type, base_symbol, base_amount,
            quote_symbol, quote_amount,
            datetime.datetime.now().isoformat())

        self.orders_log.info(message)

    @property
    def account(self):
        """ Return the full account as :class:`bitshares.account.Account` object!
            Can be refreshed by using ``x.refresh()``

            :return: object | Account
        """
        return self._account

    @property
    def balances(self):
        """ Returns all the balances of the account assigned for the worker.

            :return: Balances in list where each asset is in their own Amount object
        """
        return self._account.balances

    @property
    def base_asset(self):
        return self.worker['market'].split('/')[1]

    @property
    def quote_asset(self):
        return self.worker['market'].split('/')[0]

    @property
    def all_own_orders(self, refresh=True):
        """ Return the worker's open orders in all markets

            :param bool | refresh: Use most resent data
            :return: List of Order objects
        """
        # Refresh account data
        if refresh:
            self.account.refresh()

        orders = []
        for order in self.account.openorders:
            orders.append(order)

        return orders

    @property
    def get_own_orders(self):
        """ Return the account's open orders in the current market

            :return: List of Order objects
        """
        orders = []

        # Refresh account data
        self.account.refresh()

        for order in self.account.openorders:
            if self.worker[
                    "market"] == order.market and self.account.openorders:
                orders.append(order)

        return orders

    @property
    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):
        """ Converts asset to another based on the latest market value

            :param float | from_value: Amount of the input asset
            :param string | from_asset: Symbol of the input asset
            :param string | to_asset: Symbol of the output asset
            :return: float Asset converted to another asset as float value
        """
        market = Market('{}/{}'.format(from_asset, to_asset))
        ticker = market.ticker()
        latest_price = ticker.get('latest', {}).get('price', None)
        precision = market['base']['precision']

        return truncate((from_value * latest_price), precision)

    @staticmethod
    def get_order(order_id, return_none=True):
        """ Get Order object with order_id

            :param str | dict order_id: blockchain object id of the order can be an order dict with the id key in it
            :param bool return_none: return None instead of an empty Order object when the order doesn't exist
            :return: Order object
        """
        if not order_id:
            return None
        if 'id' in order_id:
            order_id = order_id['id']
        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?
            Todo: If get_updated_order is removed, this can be removed as well.
        """
        order = copy.deepcopy(limit_order)
        price = float(order['sell_price']['base']['amount']) / float(
            order['sell_price']['quote']['amount'])
        base_amount = float(order['for_sale'])
        quote_amount = base_amount / price
        order['sell_price']['base']['amount'] = base_amount
        order['sell_price']['quote']['amount'] = quote_amount
        return order

    @staticmethod
    def purge_all_local_worker_data(worker_name):
        """ Removes worker's data and orders from local sqlite database

            :param worker_name: Name of the worker to be removed
        """
        Storage.clear_worker_data(worker_name)

    @staticmethod
    def sort_orders_by_price(orders, sort='DESC'):
        """ Return list of orders sorted ascending or descending by price

            :param list | orders: list of orders to be sorted
            :param string | sort: ASC or DESC. Default DESC
            :return list: Sorted list of orders
        """
        if sort.upper() == 'ASC':
            reverse = False
        elif sort.upper() == 'DESC':
            reverse = True
        else:
            return None

        # Sort orders by price
        return sorted(orders,
                      key=lambda order: order['price'],
                      reverse=reverse)
Beispiel #5
0
def tapbasic(referrer):

    # test is request has 'account' key
    if not request.json or 'account' not in request.json:
        abort(400)
    account = request.json.get('account', {})

    # make sure all keys are present
    if any([
            key not in account
            for key in ["active_key", "memo_key", "owner_key", "name"]
    ]):
        abort(400)

    # prevent massive account registration
    if request.headers.get('X-Real-IP'):
        ip = request.headers.get('X-Real-IP')
    else:
        ip = request.remote_addr
    log.info("Request from IP: " + ip)
    if ip != "127.0.0.1" and models.Accounts.exists(ip):
        return api_error("Only one account per IP")

    # Check if account name is cheap name
    if (not re.search(r"[0-9-]", account["name"])
            and re.search(r"[aeiouy]", account["name"])):
        return api_error("Only cheap names allowed!")

    # This is not really needed but added to keep API-compatibility with Rails Faucet
    account.update({"id": None})

    bitshares = BitShares(config.witness_url,
                          nobroadcast=config.nobroadcast,
                          keys=[config.wif])

    try:
        Account(account["name"], bitshares_instance=bitshares)
        return api_error("Account exists")
    except:
        pass

    # Registrar
    registrar = account.get("registrar", config.registrar) or config.registrar
    try:
        registrar = Account(registrar, bitshares_instance=bitshares)
    except:
        return api_error("Unknown registrar: %s" % registrar)

    # Referrer
    referrer = account.get("referrer",
                           config.default_referrer) or config.default_referrer
    try:
        referrer = Account(referrer, bitshares_instance=bitshares)
    except:
        return api_error("Unknown referrer: %s" % referrer)
    referrer_percent = account.get("referrer_percent", config.referrer_percent)

    # Proxy
    proxy_account = None
    allow_proxy = account.get("allow_proxy", True)
    if allow_proxy:
        proxy_account = config.get("proxy", None)

    # Create new account
    try:
        bitshares.create_account(
            account["name"],
            registrar=registrar["id"],
            referrer=referrer["id"],
            referrer_percent=referrer_percent,
            owner_key=account["owner_key"],
            active_key=account["active_key"],
            memo_key=account["memo_key"],
            proxy_account=proxy_account,
            additional_owner_accounts=config.get("additional_owner_accounts",
                                                 []),
            additional_active_accounts=config.get("additional_active_accounts",
                                                  []),
            additional_owner_keys=config.get("additional_owner_keys", []),
            additional_active_keys=config.get("additional_active_keys", []),
        )
    except Exception as e:
        log.error(traceback.format_exc())
        return api_error(str(e))

    models.Accounts(account["name"], ip)

    balance = registrar.balance(config.core_asset)
    if balance and balance.amount < config.balance_mailthreshold:
        log.critical(
            "The faucet's balances is below {}".format(
                config.balance_mailthreshold), )

    return jsonify({
        "account": {
            "name": account["name"],
            "owner_key": account["owner_key"],
            "active_key": account["active_key"],
            "memo_key": account["memo_key"],
            "referrer": referrer["name"]
        }
    })
Beispiel #6
0
class BaseStrategy(Storage, StateMachine, Events):
    """ Base Strategy and methods available in all Sub Classes that
        inherit this BaseStrategy.

        BaseStrategy inherits:

        * :class:`dexbot.storage.Storage`
        * :class:`dexbot.statemachine.StateMachine`
        * ``Events``

        Available attributes:

         * ``basestrategy.bitshares``: instance of ´`bitshares.BitShares()``
         * ``basestrategy.add_state``: Add a specific state
         * ``basestrategy.set_state``: Set finite state machine
         * ``basestrategy.get_state``: Change state of state machine
         * ``basestrategy.account``: The Account object of this bot
         * ``basestrategy.market``: The market used by this bot
         * ``basestrategy.orders``: List of open orders of the bot's account in the bot's market
         * ``basestrategy.balance``: List of assets and amounts available in the bot's account
         * ``basestrategy.log``: a per-bot logger (actually LoggerAdapter) adds bot-specific context: botname & account
           (Because some UIs might want to display per-bot logs)

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

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

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

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

    __events__ = [
        'ontick',
        'onMarketUpdate',
        'onAccount',
        'error_ontick',
        'error_onMarketUpdate',
        'error_onAccount',
        'onOrderMatched',
        'onOrderPlaced',
        'onUpdateCallOrder',
    ]

    @classmethod
    def configure(kls):
        """
        Return a list of ConfigElement objects defining the configuration values for
        this class
        User interfaces should then generate widgets based on this values, gather
        data and save back to the config dictionary for the bot.

        NOTE: when overriding you almost certainly will want to call the ancestor
        and then add your config values to the list.
        """
        # these configs are common to all bots
        return [
            ConfigElement(
                "account", "string", "",
                "BitShares account name for the bot to operate with", ""),
            ConfigElement(
                "market", "string", "USD:BTS",
                "BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"",
                "[A-Z]+:[A-Z]+")
        ]

    def __init__(self,
                 config,
                 name,
                 onAccount=None,
                 onOrderMatched=None,
                 onOrderPlaced=None,
                 onMarketUpdate=None,
                 onUpdateCallOrder=None,
                 ontick=None,
                 bitshares_instance=None,
                 *args,
                 **kwargs):
        # BitShares instance
        self.bitshares = bitshares_instance or shared_bitshares_instance()

        # Storage
        Storage.__init__(self, name)

        # Statemachine
        StateMachine.__init__(self, name)

        # Events
        Events.__init__(self)

        if ontick:
            self.ontick += ontick
        if onMarketUpdate:
            self.onMarketUpdate += onMarketUpdate
        if onAccount:
            self.onAccount += onAccount
        if onOrderMatched:
            self.onOrderMatched += onOrderMatched
        if onOrderPlaced:
            self.onOrderPlaced += onOrderPlaced
        if onUpdateCallOrder:
            self.onUpdateCallOrder += onUpdateCallOrder

        # Redirect this event to also call order placed and order matched
        self.onMarketUpdate += self._callbackPlaceFillOrders

        self.config = config
        self.bot = config["bots"][name]
        self._account = Account(self.bot["account"],
                                full=True,
                                bitshares_instance=self.bitshares)
        self._market = Market(config["bots"][name]["market"],
                              bitshares_instance=self.bitshares)

        # Settings for bitshares instance
        self.bitshares.bundle = bool(self.bot.get("bundle", False))

        # disabled flag - this flag can be flipped to True by a bot and
        # will be reset to False after reset only
        self.disabled = False

        # a private logger that adds bot identify data to the LogRecord
        self.log = logging.LoggerAdapter(
            logging.getLogger('dexbot.per_bot'), {
                'botname': name,
                'account': self.bot['account'],
                'market': self.bot['market'],
                'is_disabled': lambda: self.disabled
            })

    @property
    def orders(self):
        """ Return the bot's open accounts in the current market
        """
        self.account.refresh()
        return [
            o for o in self.account.openorders
            if self.bot["market"] == o.market and self.account.openorders
        ]

    def get_order(self, order_id):
        for order in self.orders:
            if order['id'] == order_id:
                return order
        return False

    def get_updated_order(self, order):
        if not order:
            return False
        for updated_order in self.updated_open_orders:
            if updated_order['id'] == order['id']:
                return updated_order
        return False

    @property
    def updated_open_orders(self):
        """
        Returns updated open Orders.
        account.openorders doesn't return updated values for the order so we calculate the values manually
        """
        self.account.refresh()
        self.account.ensure_full()

        limit_orders = self.account['limit_orders'][:]
        for o in limit_orders:
            base_amount = o['for_sale']
            price = o['sell_price']['base']['amount'] / \
                o['sell_price']['quote']['amount']
            quote_amount = base_amount / price
            o['sell_price']['base']['amount'] = base_amount
            o['sell_price']['quote']['amount'] = quote_amount

        orders = [
            Order(o, bitshares_instance=self.bitshares) for o in limit_orders
        ]

        return [o for o in orders if self.bot["market"] == o.market]

    @property
    def market(self):
        """ Return the market object as :class:`bitshares.market.Market`
        """
        return self._market

    @property
    def account(self):
        """ Return the full account as :class:`bitshares.account.Account` object!

            Can be refreshed by using ``x.refresh()``
        """
        return self._account

    def balance(self, asset):
        """ Return the balance of your bot's account for a specific asset
        """
        return self._account.balance(asset)

    def get_converted_asset_amount(self, asset):
        """
        Returns asset amount converted to base asset amount
        """
        base_asset = self.market['base']
        quote_asset = Asset(asset['symbol'], bitshares_instance=self.bitshares)
        if base_asset['symbol'] == quote_asset['symbol']:
            return asset['amount']
        else:
            market = Market(base=base_asset,
                            quote=quote_asset,
                            bitshares_instance=self.bitshares)
            return market.ticker()['latest']['price'] * asset['amount']

    @property
    def test_mode(self):
        return self.config['node'] == "wss://node.testnet.bitshares.eu"

    @property
    def balances(self):
        """ Return the balances of your bot's account
        """
        return self._account.balances

    def _callbackPlaceFillOrders(self, d):
        """ This method distringuishes notifications caused by Matched orders
            from those caused by placed orders
        """
        if isinstance(d, FilledOrder):
            self.onOrderMatched(d)
        elif isinstance(d, Order):
            self.onOrderPlaced(d)
        elif isinstance(d, UpdateCallOrder):
            self.onUpdateCallOrder(d)
        else:
            pass

    def execute(self):
        """ Execute a bundle of operations
        """
        self.bitshares.blocking = "head"
        r = self.bitshares.txbuffer.broadcast()
        self.bitshares.blocking = False
        return r

    def cancel(self, orders):
        """ Cancel specific orders
        """
        if not isinstance(orders, list):
            orders = [orders]
        return self.bitshares.cancel([o["id"] for o in orders if "id" in o],
                                     account=self.account)

    def cancel_all(self):
        """ Cancel all orders of this bot
        """
        if self.orders:
            return self.bitshares.cancel([o["id"] for o in self.orders],
                                         account=self.account)

    def record_balances(self, baseprice):
        self.save_journal([('price', baseprice),
                           (self.market['quote']['symbol'],
                            self.balance(self.market['quote'])),
                           (self.market['base']['symbol'],
                            self.balance(self.market['base']))])

    def purge(self):
        """
        Clear all the bot data from the database and cancel all orders
        """
        self.cancel_all()
        self.clear()

    def graph(self, start, end_=None):
        """Draw a graph over the specified time period (both datetime)
        Uses routine suitable for one-market trading bots
        More complex bots (arbitrage, etc) may need to override to provide
        meaningful graphs
        """
        data = graph.query_to_dicts(self.query_journal(start, end_))
        if len(data) < 2:
            # not enough data to graph
            return None
        data = graph.rebase_data(data, self.market['quote']['symbol'],
                                 self.market['base']['symbol'])
        return graph.do_graph(data)
Beispiel #7
0
class BaseStrategy(Storage, StateMachine, Events):
    """ Base Strategy and methods available in all Sub Classes that
        inherit this BaseStrategy.

        BaseStrategy inherits:

        * :class:`dexbot.storage.Storage`
        * :class:`dexbot.statemachine.StateMachine`
        * ``Events``

        Available attributes:

         * ``basestrategy.bitshares``: instance of ´`bitshares.BitShares()``
         * ``basestrategy.add_state``: Add a specific state
         * ``basestrategy.set_state``: Set finite state machine
         * ``basestrategy.get_state``: Change state of state machine
         * ``basestrategy.account``: The Account object of this worker
         * ``basestrategy.market``: The market used by this worker
         * ``basestrategy.orders``: List of open orders of the worker's account in the worker's market
         * ``basestrategy.balance``: List of assets and amounts available in the worker's account
         * ``basestrategy.log``: a per-worker logger (actually LoggerAdapter) adds worker-specific context:
            worker name & account (Because some UIs might want to display per-worker logs)

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

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

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

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

    __events__ = [
        'ontick',
        'onMarketUpdate',
        'onAccount',
        'error_ontick',
        'error_onMarketUpdate',
        'error_onAccount',
        'onOrderMatched',
        'onOrderPlaced',
        'onUpdateCallOrder',
    ]

    @classmethod
    def configure(cls, return_base_config=True):
        """
        Return a list of ConfigElement objects defining the configuration values for 
        this class
        User interfaces should then generate widgets based on this values, gather
        data and save back to the config dictionary for the worker.

        NOTE: when overriding you almost certainly will want to call the ancestor
        and then add your config values to the list.
        """
        # These configs are common to all bots
        base_config = [
            ConfigElement(
                "account", "string", "", "Account",
                "BitShares account name for the bot to operate with", ""),
            ConfigElement(
                "market", "string", "USD:BTS", "Market",
                "BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"",
                r"[A-Z\.]+[:\/][A-Z\.]+"),
            ConfigElement('fee_asset', 'string', 'BTS', 'Fee asset',
                          'Asset to be used to pay transaction fees',
                          r'[A-Z\.]+')
        ]
        if return_base_config:
            return base_config
        return []

    def __init__(self,
                 name,
                 config=None,
                 onAccount=None,
                 onOrderMatched=None,
                 onOrderPlaced=None,
                 onMarketUpdate=None,
                 onUpdateCallOrder=None,
                 ontick=None,
                 bitshares_instance=None,
                 *args,
                 **kwargs):
        # BitShares instance
        self.bitshares = bitshares_instance or shared_bitshares_instance()

        # Storage
        Storage.__init__(self, name)

        # Statemachine
        StateMachine.__init__(self, name)

        # Events
        Events.__init__(self)

        if ontick:
            self.ontick += ontick
        if onMarketUpdate:
            self.onMarketUpdate += onMarketUpdate
        if onAccount:
            self.onAccount += onAccount
        if onOrderMatched:
            self.onOrderMatched += onOrderMatched
        if onOrderPlaced:
            self.onOrderPlaced += onOrderPlaced
        if onUpdateCallOrder:
            self.onUpdateCallOrder += onUpdateCallOrder

        # Redirect this event to also call order placed and order matched
        self.onMarketUpdate += self._callbackPlaceFillOrders

        if config:
            self.config = config
        else:
            self.config = config = Config.get_worker_config_file(name)

        self.worker = config["workers"][name]
        self._account = Account(self.worker["account"],
                                full=True,
                                bitshares_instance=self.bitshares)
        self._market = Market(config["workers"][name]["market"],
                              bitshares_instance=self.bitshares)

        # Recheck flag - Tell the strategy to check for updated orders
        self.recheck_orders = False

        # Set fee asset
        fee_asset_symbol = self.worker.get('fee_asset')
        if fee_asset_symbol:
            try:
                self.fee_asset = Asset(fee_asset_symbol)
            except bitshares.exceptions.AssetDoesNotExistsException:
                self.fee_asset = Asset('1.3.0')
        else:
            self.fee_asset = Asset('1.3.0')

        # Settings for bitshares instance
        self.bitshares.bundle = bool(self.worker.get("bundle", False))

        # Disabled flag - this flag can be flipped to True by a worker and
        # will be reset to False after reset only
        self.disabled = False

        # Order expiration time in seconds
        self.expiration = 60 * 60 * 24 * 365 * 5

        # buy/sell actions will return order id by default
        self.returnOrderId = 'head'

        # CER cache
        self.core_exchange_rate = None

        # A private logger that adds worker identify data to the LogRecord
        self.log = logging.LoggerAdapter(
            logging.getLogger('dexbot.per_worker'), {
                'worker_name': name,
                'account': self.worker['account'],
                'market': self.worker['market'],
                'is_disabled': lambda: self.disabled
            })

        self.orders_log = logging.LoggerAdapter(
            logging.getLogger('dexbot.orders_log'), {})

    def _calculate_center_price(self, suppress_errors=False):
        ticker = self.market.ticker()
        highest_bid = ticker.get("highestBid")
        lowest_ask = ticker.get("lowestAsk")
        if highest_bid is None or highest_bid == 0.0:
            if not suppress_errors:
                self.log.critical(
                    "Cannot estimate center price, there is no highest bid.")
                self.disabled = True
            return None
        elif lowest_ask is None or lowest_ask == 0.0:
            if not suppress_errors:
                self.log.critical(
                    "Cannot estimate center price, there is no lowest ask.")
                self.disabled = True
            return None

        center_price = highest_bid['price'] * math.sqrt(
            lowest_ask['price'] / highest_bid['price'])
        return center_price

    def calculate_center_price(self,
                               center_price=None,
                               asset_offset=False,
                               spread=None,
                               order_ids=None,
                               manual_offset=0,
                               suppress_errors=False):
        """ Calculate center price which shifts based on available funds
        """
        if center_price is None:
            # No center price was given so we simply calculate the center price
            calculated_center_price = self._calculate_center_price(
                suppress_errors)
        else:
            # Center price was given so we only use the calculated center price
            # for quote to base asset conversion
            calculated_center_price = self._calculate_center_price(True)
            if not calculated_center_price:
                calculated_center_price = center_price

        if center_price:
            calculated_center_price = center_price

        if asset_offset:
            total_balance = self.total_balance(order_ids)
            total = (total_balance['quote'] *
                     calculated_center_price) + total_balance['base']

            if not total:  # Prevent division by zero
                balance = 0
            else:
                # Returns a value between -1 and 1
                balance = (total_balance['base'] / total) * 2 - 1

            if balance < 0:
                # With less of base asset center price should be offset downward
                calculated_center_price = calculated_center_price / math.sqrt(
                    1 + spread * (balance * -1))
            elif balance > 0:
                # With more of base asset center price will be offset upwards
                calculated_center_price = calculated_center_price * math.sqrt(
                    1 + spread * balance)
            else:
                calculated_center_price = calculated_center_price

        # Calculate final_offset_price if manual center price offset is given
        if manual_offset:
            calculated_center_price = calculated_center_price + (
                calculated_center_price * manual_offset)

        return calculated_center_price

    @property
    def orders(self):
        """ Return the account's open orders in the current market
        """
        self.account.refresh()
        return [
            o for o in self.account.openorders
            if self.worker["market"] == o.market and self.account.openorders
        ]

    @property
    def all_orders(self):
        """ Return the accounts's open orders in all markets
        """
        self.account.refresh()
        return [o for o in self.account.openorders]

    def get_buy_orders(self, sort=None, orders=None):
        """ Return buy orders
            :param str sort: DESC or ASC will sort the orders accordingly, default None.
            :param list orders: List of orders. If None given get all orders from Blockchain.
            :return list buy_orders: List of buy orders only.
        """
        buy_orders = []

        if not orders:
            orders = self.orders

        # Find buy orders
        for order in orders:
            if self.is_buy_order(order):
                buy_orders.append(order)
        if sort:
            buy_orders = self.sort_orders(buy_orders, sort)

        return buy_orders

    def get_sell_orders(self, sort=None, orders=None):
        """ Return sell orders
            :param str sort: DESC or ASC will sort the orders accordingly, default None.
            :param list orders: List of orders. If None given get all orders from Blockchain.
            :return list sell_orders: List of sell orders only.
        """
        sell_orders = []

        if not orders:
            orders = self.orders

        # Find sell orders
        for order in orders:
            if self.is_sell_order(order):
                sell_orders.append(order)

        if sort:
            sell_orders = self.sort_orders(sell_orders, sort)

        return sell_orders

    def is_buy_order(self, order):
        """ Checks if the order is Buy order
            :param order: Buy / Sell order
            :return: bool: True = Buy order
        """
        if order['base']['symbol'] == self.market['base']['symbol']:
            return True
        return False

    def is_sell_order(self, order):
        """ Checks if the order is Sell order
            :param order: Buy / Sell order
            :return: bool: True = Sell order
        """
        if order['base']['symbol'] != self.market['base']['symbol']:
            return True
        return False

    @staticmethod
    def sort_orders(orders, sort='DESC'):
        """ Return list of orders sorted ascending or descending
            :param list orders: list of orders to be sorted
            :param str sort: ASC or DESC. Default DESC
            :return list: Sorted list of orders.
        """
        if sort == 'ASC':
            reverse = False
        elif sort == 'DESC':
            reverse = True
        else:
            return None

        # Sort orders by price
        return sorted(orders,
                      key=lambda order: order['price'],
                      reverse=reverse)

    def get_order_cancellation_fee(self, fee_asset):
        """ Returns the order cancellation fee in the specified asset.

            :param string | fee_asset: Asset in which the fee is wanted
            :return: Cancellation fee as fee asset
        """
        # Get fee
        fees = self.dex.returnFees()
        limit_order_cancel = fees['limit_order_cancel']
        return self.convert_fee(limit_order_cancel['fee'], fee_asset)

    def get_order_creation_fee(self, fee_asset):
        """ Returns the cost of creating an order in the asset specified

            :param fee_asset: QUOTE, BASE, BTS, or any other
            :return:
        """
        # Get fee
        fees = self.dex.returnFees()
        limit_order_create = fees['limit_order_create']
        return self.convert_fee(limit_order_create['fee'], fee_asset)

    @staticmethod
    def get_order(order_id, return_none=True):
        """ Returns the Order object for the order_id

            :param str|dict order_id: blockchain object id of the order
                can be an order dict with the id key in it
            :param bool return_none: return None instead of an empty
                Order object when the order doesn't exist
        """
        if not order_id:
            return None
        if 'id' in order_id:
            order_id = order_id['id']
        order = Order(order_id)
        if return_none and order['deleted']:
            return None
        return order

    def get_updated_order(self, order_id):
        """ Tries to get the updated order from the API
            returns None if the order doesn't exist

            :param str|dict order_id: blockchain object id of the order
                can be an order dict with the id key in it
        """
        if isinstance(order_id, dict):
            order_id = order_id['id']

        # Get the limited order by id
        order = None
        for limit_order in self.account['limit_orders']:
            if order_id == limit_order['id']:
                order = limit_order
                break
        else:
            return order

        # Do not try to continue whether there is no order in the blockchain
        if not order:
            return None

        order = self.get_updated_limit_order(order)
        return Order(order, bitshares_instance=self.bitshares)

    @property
    def updated_orders(self):
        """ Returns all open orders as updated orders
        """
        self.account.refresh()

        limited_orders = []
        for order in self.account['limit_orders']:
            base_asset_id = order['sell_price']['base']['asset_id']
            quote_asset_id = order['sell_price']['quote']['asset_id']
            # Check if the order is in the current market
            if not self.is_current_market(base_asset_id, quote_asset_id):
                continue

            limited_orders.append(self.get_updated_limit_order(order))

        return [
            Order(o, bitshares_instance=self.bitshares) for o in limited_orders
        ]

    @staticmethod
    def get_updated_limit_order(limit_order):
        """ Returns a modified limit_order so that when passed to Order class,
            will return an Order object with updated amount values
            :param limit_order: an item of Account['limit_orders']
            :return: dict
        """
        o = copy.deepcopy(limit_order)
        price = float(o['sell_price']['base']['amount']) / float(
            o['sell_price']['quote']['amount'])
        base_amount = float(o['for_sale'])
        quote_amount = base_amount / price
        o['sell_price']['base']['amount'] = base_amount
        o['sell_price']['quote']['amount'] = quote_amount
        return o

    @property
    def market(self):
        """ Return the market object as :class:`bitshares.market.Market`
        """
        return self._market

    @property
    def account(self):
        """ Return the full account as :class:`bitshares.account.Account` object!

            Can be refreshed by using ``x.refresh()``
        """
        return self._account

    def balance(self, asset):
        """ Return the balance of your worker's account for a specific asset
        """
        return self._account.balance(asset)

    @property
    def balances(self):
        """ Return the balances of your worker's account
        """
        return self._account.balances

    def _callbackPlaceFillOrders(self, d):
        """ This method distinguishes notifications caused by Matched orders
            from those caused by placed orders
        """
        if isinstance(d, FilledOrder):
            self.onOrderMatched(d)
        elif isinstance(d, Order):
            self.onOrderPlaced(d)
        elif isinstance(d, UpdateCallOrder):
            self.onUpdateCallOrder(d)
        else:
            pass

    def execute(self):
        """ Execute a bundle of operations
        """
        self.bitshares.blocking = "head"
        r = self.bitshares.txbuffer.broadcast()
        self.bitshares.blocking = False
        return r

    def _cancel(self, orders):
        try:
            self.retry_action(self.bitshares.cancel,
                              orders,
                              account=self.account,
                              fee_asset=self.fee_asset['id'])
        except bitsharesapi.exceptions.UnhandledRPCError as e:
            if str(e).startswith(
                    'Assert Exception: maybe_found != nullptr: Unable to find Object'
            ):
                # The order(s) we tried to cancel doesn't exist
                self.bitshares.txbuffer.clear()
                return False
            else:
                self.log.exception("Unable to cancel order")
        except bitshares.exceptions.MissingKeyError:
            self.log.exception(
                'Unable to cancel order(s), private key missing.')

        return True

    def cancel(self, orders, batch_only=False):
        """ Cancel specific order(s)
            :param list orders: list of orders to cancel
            :param bool batch_only: try cancel orders only in batch mode without one-by-one fallback
        """
        if not isinstance(orders, (list, set, tuple)):
            orders = [orders]

        orders = [order['id'] for order in orders if 'id' in order]

        success = self._cancel(orders)
        if not success and batch_only:
            return False
        if not success and len(orders) > 1 and not batch_only:
            # One of the order cancels failed, cancel the orders one by one
            for order in orders:
                self._cancel(order)
        return True

    def cancel_all(self):
        """ Cancel all orders of the worker's account
        """
        self.log.info('Canceling all orders')
        if self.orders:
            self.cancel(self.orders)
        self.log.info("Orders canceled")

    def pause(self):
        """ Pause the worker
        """
        # By default, just call cancel_all(); strategies may override this method
        self.cancel_all()
        self.clear_orders()

    def market_buy(self,
                   quote_amount,
                   price,
                   return_none=False,
                   *args,
                   **kwargs):
        symbol = self.market['base']['symbol']
        precision = self.market['base']['precision']
        base_amount = truncate(price * quote_amount, precision)

        # Don't try to place an order of size 0
        if not base_amount:
            self.log.critical('Trying to buy 0')
            self.disabled = True
            return None

        # Make sure we have enough balance for the order
        if self.returnOrderId and self.balance(
                self.market['base']) < base_amount:
            self.log.critical("Insufficient buy balance, needed {} {}".format(
                base_amount, symbol))
            self.disabled = True
            return None

        self.log.info('Placing a buy order for {:.{prec}} {} @ {:.8f}'.format(
            base_amount, symbol, price, prec=precision))

        # Place the order
        buy_transaction = self.retry_action(self.market.buy,
                                            price,
                                            Amount(amount=quote_amount,
                                                   asset=self.market["quote"]),
                                            account=self.account.name,
                                            expiration=self.expiration,
                                            returnOrderId=self.returnOrderId,
                                            fee_asset=self.fee_asset['id'],
                                            *args,
                                            **kwargs)

        self.log.debug('Placed buy order {}'.format(buy_transaction))

        if self.returnOrderId:
            buy_order = self.get_order(buy_transaction['orderid'],
                                       return_none=return_none)
            if buy_order and buy_order['deleted']:
                # The API doesn't return data on orders that don't exist
                # We need to calculate the data on our own
                buy_order = self.calculate_order_data(buy_order, quote_amount,
                                                      price)
                self.recheck_orders = True
            return buy_order
        else:
            return True

    def market_sell(self,
                    quote_amount,
                    price,
                    return_none=False,
                    *args,
                    **kwargs):
        symbol = self.market['quote']['symbol']
        precision = self.market['quote']['precision']
        quote_amount = truncate(quote_amount, precision)

        # Don't try to place an order of size 0
        if not quote_amount:
            self.log.critical('Trying to sell 0')
            self.disabled = True
            return None

        # Make sure we have enough balance for the order
        if self.returnOrderId and self.balance(
                self.market['quote']) < quote_amount:
            self.log.critical("Insufficient sell balance, needed {} {}".format(
                quote_amount, symbol))
            self.disabled = True
            return None

        self.log.info(
            'Placing a sell order for {:.{prec}f} {} @ {:.8f}'.format(
                quote_amount, symbol, price, prec=precision))

        # Place the order
        sell_transaction = self.retry_action(self.market.sell,
                                             price,
                                             Amount(
                                                 amount=quote_amount,
                                                 asset=self.market["quote"]),
                                             account=self.account.name,
                                             expiration=self.expiration,
                                             returnOrderId=self.returnOrderId,
                                             fee_asset=self.fee_asset['id'],
                                             *args,
                                             **kwargs)

        self.log.debug('Placed sell order {}'.format(sell_transaction))
        if self.returnOrderId:
            sell_order = self.get_order(sell_transaction['orderid'],
                                        return_none=return_none)
            if sell_order and sell_order['deleted']:
                # The API doesn't return data on orders that don't exist
                # We need to calculate the data on our own
                sell_order = self.calculate_order_data(sell_order,
                                                       quote_amount, price)
                sell_order.invert()
                self.recheck_orders = True
            return sell_order
        else:
            return True

    def calculate_order_data(self, order, amount, price):
        quote_asset = Amount(amount, self.market['quote']['symbol'])
        order['quote'] = quote_asset
        order['price'] = price
        base_asset = Amount(amount * price, self.market['base']['symbol'])
        order['base'] = base_asset
        return order

    def is_current_market(self, base_asset_id, quote_asset_id):
        """ Returns True if given asset id's are of the current market
        """
        if quote_asset_id == self.market['quote']['id']:
            if base_asset_id == self.market['base']['id']:
                return True
            return False
        if quote_asset_id == self.market['base']['id']:
            if base_asset_id == self.market['quote']['id']:
                return True
            return False
        return False

    def purge(self):
        """ Clear all the worker data from the database and cancel all orders
        """
        self.clear_orders()
        self.cancel_all()
        self.clear()

    @staticmethod
    def purge_worker_data(worker_name):
        """ Remove worker data from database only """
        Storage.clear_worker_data(worker_name)

    def total_balance(self, order_ids=None, return_asset=False):
        """ Returns the combined balance of the given order ids and the account balance
            The amounts are returned in quote and base assets of the market

            :param order_ids: list of order ids to be added to the balance
            :param return_asset: true if returned values should be Amount instances
            :return: dict with keys quote and base
        """
        quote = 0
        base = 0
        quote_asset = self.market['quote']['id']
        base_asset = self.market['base']['id']

        # Total balance calculation
        for balance in self.balances:
            if balance.asset['id'] == quote_asset:
                quote += balance['amount']
            elif balance.asset['id'] == base_asset:
                base += balance['amount']

        if order_ids is None:
            # Get all orders from Blockchain
            order_ids = [order['id'] for order in self.orders]
        if order_ids:
            orders_balance = self.orders_balance(order_ids)
            quote += orders_balance['quote']
            base += orders_balance['base']

        if return_asset:
            quote = Amount(quote, quote_asset)
            base = Amount(base, base_asset)

        return {'quote': quote, 'base': base}

    def account_total_value(self, return_asset):
        """ Returns the total value of the account in given asset
            :param str return_asset: Asset which is wanted as return
            :return: float: Value of the account in one asset
        """
        total_value = 0

        # Total balance calculation
        for balance in self.balances:
            if balance['symbol'] != return_asset:
                # Convert to asset if different
                total_value += self.convert_asset(balance['amount'],
                                                  balance['symbol'],
                                                  return_asset)
            else:
                total_value += balance['amount']

        # Orders balance calculation
        for order in self.all_orders:
            updated_order = self.get_updated_order(order['id'])

            if not order:
                continue
            if updated_order['base']['symbol'] == return_asset:
                total_value += updated_order['base']['amount']
            else:
                total_value += self.convert_asset(
                    updated_order['base']['amount'],
                    updated_order['base']['symbol'], return_asset)

        return total_value

    @staticmethod
    def convert_asset(from_value, from_asset, to_asset):
        """Converts asset to another based on the latest market value
            :param from_value: Amount of the input asset
            :param from_asset: Symbol of the input asset
            :param to_asset: Symbol of the output asset
            :return: Asset converted to another asset as float value
        """
        market = Market('{}/{}'.format(from_asset, to_asset))
        ticker = market.ticker()
        latest_price = ticker.get('latest', {}).get('price', None)
        return from_value * latest_price

    def convert_fee(self, fee_amount, fee_asset):
        """ Convert fee amount in BTS to fee in fee_asset

            :param float | fee_amount: fee amount paid in BTS
            :param Asset | fee_asset: fee asset to pay fee in
            :return: float | amount of fee_asset to pay fee
        """
        if isinstance(fee_asset, str):
            fee_asset = Asset(fee_asset)

        if fee_asset['id'] == '1.3.0':
            # Fee asset is BTS, so no further calculations are needed
            return fee_amount
        else:
            if not self.core_exchange_rate:
                # Determine how many fee_asset is needed for core-exchange
                temp_market = Market(base=fee_asset, quote=Asset('1.3.0'))
                self.core_exchange_rate = temp_market.ticker(
                )['core_exchange_rate']
            return fee_amount * self.core_exchange_rate['base']['amount']

    def orders_balance(self, order_ids, return_asset=False):
        if not order_ids:
            order_ids = []
        elif isinstance(order_ids, str):
            order_ids = [order_ids]

        quote = 0
        base = 0
        quote_asset = self.market['quote']['id']
        base_asset = self.market['base']['id']

        for order_id in order_ids:
            order = self.get_updated_order(order_id)
            if not order:
                continue
            asset_id = order['base']['asset']['id']
            if asset_id == quote_asset:
                quote += order['base']['amount']
            elif asset_id == base_asset:
                base += order['base']['amount']

        if return_asset:
            quote = Amount(quote, quote_asset)
            base = Amount(base, base_asset)

        return {'quote': quote, 'base': base}

    def retry_action(self, action, *args, **kwargs):
        """
        Perform an action, and if certain suspected-to-be-spurious graphene bugs occur,
        instead of bubbling the exception, it is quietly logged (level WARN), and try again
        tries a fixed number of times (MAX_TRIES) before failing
        """
        tries = 0
        while True:
            try:
                return action(*args, **kwargs)
            except bitsharesapi.exceptions.UnhandledRPCError as e:
                if "Assert Exception: amount_to_sell.amount > 0" in str(e):
                    if tries > MAX_TRIES:
                        raise
                    else:
                        tries += 1
                        self.log.warning("Ignoring: '{}'".format(str(e)))
                        self.bitshares.txbuffer.clear()
                        self.account.refresh()
                        time.sleep(2)
                elif "now <= trx.expiration" in str(
                        e):  # Usually loss of sync to blockchain
                    if tries > MAX_TRIES:
                        raise
                    else:
                        tries += 1
                        self.log.warning("retrying on '{}'".format(str(e)))
                        self.bitshares.txbuffer.clear()
                        time.sleep(6)  # Wait at least a BitShares block
                else:
                    raise

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

        if order['base']['symbol'] == self.market['base']['symbol']:
            base_symbol = order['base']['symbol']
            base_amount = -order['base']['amount']
            quote_symbol = order['quote']['symbol']
            quote_amount = order['quote']['amount']
        else:
            base_symbol = order['quote']['symbol']
            base_amount = order['quote']['amount']
            quote_symbol = order['base']['symbol']
            quote_amount = -order['base']['amount']

        message = '{};{};{};{};{};{};{};{}'.format(
            worker_name, order['id'], operation_type, base_symbol, base_amount,
            quote_symbol, quote_amount,
            datetime.datetime.now().isoformat())

        self.orders_log.info(message)
Beispiel #8
0
class BaseStrategy(Storage, StateMachine, Events):
    """ Base Strategy and methods available in all Sub Classes that
        inherit this BaseStrategy.

        BaseStrategy inherits:

        * :class:`stakemachine.storage.Storage`
        * :class:`stakemachine.statemachine.StateMachine`
        * ``Events``

        Available attributes:

         * ``basestrategy.bitshares``: instance of ´`bitshares.BitShares()``
         * ``basestrategy.add_state``: Add a specific state
         * ``basestrategy.set_state``: Set finite state machine
         * ``basestrategy.get_state``: Change state of state machine
         * ``basestrategy.account``: The Account object of this bot
         * ``basestrategy.market``: The market used by this bot
         * ``basestrategy.orders``: List of open orders of the bot's account in the bot's market
         * ``basestrategy.balance``: List of assets and amounts available in the bot's account

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

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

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

    __events__ = [
        'ontick',
        'onMarketUpdate',
        'onAccount',
        'error_ontick',
        'error_onMarketUpdate',
        'error_onAccount',
        'onOrderMatched',
        'onOrderPlaced',
        'onUpdateCallOrder',
    ]

    def __init__(self,
                 config,
                 name,
                 onAccount=None,
                 onOrderMatched=None,
                 onOrderPlaced=None,
                 onMarketUpdate=None,
                 onUpdateCallOrder=None,
                 ontick=None,
                 bitshares_instance=None,
                 *args,
                 **kwargs):
        # BitShares instance
        self.bitshares = bitshares_instance or shared_bitshares_instance()

        # Storage
        Storage.__init__(self, name)

        # Statemachine
        StateMachine.__init__(self, name)

        # Events
        Events.__init__(self)

        if ontick:
            self.ontick += ontick
        if onMarketUpdate:
            self.onMarketUpdate += onMarketUpdate
        if onAccount:
            self.onAccount += onAccount
        if onOrderMatched:
            self.onOrderMatched += onOrderMatched
        if onOrderPlaced:
            self.onOrderPlaced += onOrderPlaced
        if onUpdateCallOrder:
            self.onUpdateCallOrder += onUpdateCallOrder

        # Redirect this event to also call order placed and order matched
        self.onMarketUpdate += self._callbackPlaceFillOrders

        self.config = config
        self.bot = config["bots"][name]
        self._account = Account(self.bot["account"],
                                full=True,
                                bitshares_instance=self.bitshares)
        self._market = Market(config["bots"][name]["market"],
                              bitshares_instance=self.bitshares)

        # Settings for bitshares instance
        self.bitshares.bundle = bool(self.bot.get("bundle", False))

        # disabled flag - this flag can be flipped to True by a bot and
        # will be reset to False after reset only
        self.disabled = False

    @property
    def orders(self):
        """ Return the bot's open accounts in the current market
        """
        self.account.refresh()
        return [
            o for o in self.account.openorders
            if self.bot["market"] == o.market and self.account.openorders
        ]

    @property
    def market(self):
        """ Return the market object as :class:`bitshares.market.Market`
        """
        return self._market

    @property
    def account(self):
        """ Return the full account as :class:`bitshares.account.Account` object!

            Can be refreshed by using ``x.refresh()``
        """
        return self._account

    def balance(self, asset):
        """ Return the balance of your bot's account for a specific asset
        """
        return self._account.balance(asset)

    @property
    def balances(self):
        """ Return the balances of your bot's account
        """
        return self._account.balances

    def _callbackPlaceFillOrders(self, d):
        """ This method distringuishes notifications caused by Matched orders
            from those caused by placed orders
        """
        if isinstance(d, FilledOrder):
            self.onOrderMatched(d)
        elif isinstance(d, Order):
            self.onOrderPlaced(d)
        elif isinstance(d, UpdateCallOrder):
            self.onUpdateCallOrder(d)
        else:
            pass

    def execute(self):
        """ Execute a bundle of operations
        """
        self.bitshares.blocking = "head"
        r = self.bitshares.txbuffer.broadcast()
        self.bitshares.blocking = False
        return r

    def cancelall(self):
        """ Cancel all orders of this bot
        """
        if self.orders:
            return self.bitshares.cancel([o["id"] for o in self.orders],
                                         account=self.account)
Beispiel #9
0
def auto_trans():
    bitshares = BitShares()
    pwd = get_bitshare_pwd()
    private_key = get_bitshare_private_key()
    # create usd:cny market obj
    usd_cny_market = Market("USD:CNY")
    # create fox wallet obj
    fox_wallet = Wallet()
    if not fox_wallet.created():
        fox_wallet.newWallet(pwd)
    # add private key, TODO:keep private key and pwd in safe place
    usd_cny_market.bitshares.wallet.unlock(pwd)
    try:
        usd_cny_market.bitshares.wallet.addPrivateKey(private_key)
    except ValueError as ve:
        logger.info('wif is already set')

    logger.info('start auto trans usd:cny')
    lb = 6.40
    ub = 6.50
    fox = Account("zfpx-fdjl")
    while True:
        start_time = time.time()
        logger.info("my open orders:%s", fox.openorders)
        my_usd = fox.balance("USD")
        my_cny = fox.balance("CNY")

        if start_time % 60 < 10:
            logger.info("my balance:%s", fox.balances)
            logger.info("my USD:%s my CNY:%s", my_usd, my_cny)

        # get avg price
        avg_price = update_market(usd_cny_market)[0]
        if avg_price < 6 or avg_price > 7:
            logger.error("!!!!!!!!!!!!!!!!!!!!!!!!:avg price out of range:",
                         avg_price)
            continue

        # set upper bound and lower bound
        lb = avg_price * (1 - 0.005)
        ub = avg_price * (1 + 0.005)

        # get top orders
        top_orders = usd_cny_market.orderbook()
        # unlock fox wallet for usd:cny maket
        usd_cny_market.bitshares.wallet.unlock(pwd)
        # cancel all of the orders
        orders = usd_cny_market.accountopenorders(fox)
        for order in orders:
            logger.info("try cancel %s : %s", order["id"], order)
            usd_cny_market.cancel(order["id"], fox)
            time.sleep(1)

        # sell all
        for bid in top_orders["bids"]:
            price = bid["price"]
            quote = bid["quote"]
            print("price:%s ub:%s quote:%s", price, ub, quote)
            if price >= ub:
                print("price:%s >= ub:%s quote:%s", price, ub, quote)
                if my_usd > 0:
                    # sell_usd = min(my_usd, quote)
                    if my_usd["amount"] < quote["amount"]:
                        sell_usd = my_usd
                    else:
                        sell_usd = quote
                    left_usd = my_usd["amount"] - sell_usd["amount"]
                    print("sell_usd:%s left_usd:%s price:%s bid:%s", sell_usd,
                          left_usd, price, bid)
                    logger.info("sell_usd:%s left_usd:%s price:%s bid:%s",
                                sell_usd, left_usd, price, bid)
                    try:
                        usd_cny_market.sell(price, sell_usd, 86400, False, fox)
                    except Exception as e:
                        logger.error("on except:%s", e)
                        log_bt()
                    my_usd["amount"] = left_usd
            else:
                break
            #    print("price:", price, " < ub:", ub)

        # buy all
        for ask in top_orders["asks"]:
            price = ask["price"]
            base = ask["base"]
            print("price:%s lb:%s base:%s", price, lb, base)
            if price <= lb:
                print("price:%s <= lb:%s base:%s", price, lb, base)
                if my_cny > 0:
                    if base["amount"] < my_cny["amount"]:
                        buy_cny = base["amount"]
                    else:
                        buy_cny = my_cny["amount"]
                    buy_usd = buy_cny / price
                    left_cny = my_cny["amount"] - buy_cny
                    print("buy_usd:%s left_cny:%s price:%s ask:%s", buy_usd,
                          left_cny, price, ask)
                    logger.info("buy_usd:%s left_cny:%s price:%s ask:%s",
                                buy_usd, left_cny, price, ask)
                    try:
                        # usd_cny_market.buy(price, buy_usd, 5, False, fox)
                        usd_cny_market.buy(price, 1, 86400, False, fox)
                    except Exception as e:
                        logger.error("on except:%s", e)
                        log_bt()
                    my_cny["amount"] = left_cny
            else:
                break
            #    print("price:", price, " > lb:", lb)

        usd_cny_market.bitshares.wallet.lock()
        delta_t = time.time() - start_time
        time.sleep(max(1, 30 - delta_t))
Beispiel #10
0
class BaseStrategy(Storage, StateMachine, Events):
    """ Base Strategy and methods available in all Sub Classes that
        inherit this BaseStrategy.

        BaseStrategy inherits:

        * :class:`dexbot.storage.Storage`
        * :class:`dexbot.statemachine.StateMachine`
        * ``Events``

        Available attributes:

         * ``basestrategy.bitshares``: instance of ´`bitshares.BitShares()``
         * ``basestrategy.add_state``: Add a specific state
         * ``basestrategy.set_state``: Set finite state machine
         * ``basestrategy.get_state``: Change state of state machine
         * ``basestrategy.account``: The Account object of this bot
         * ``basestrategy.market``: The market used by this bot
         * ``basestrategy.orders``: List of open orders of the bot's account in the bot's market
         * ``basestrategy.balance``: List of assets and amounts available in the bot's account
         * ``basestrategy.log``: a per-bot logger (actually LoggerAdapter) adds bot-specific context: botname & account
           (Because some UIs might want to display per-bot logs)

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

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

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

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

    __events__ = [
        'ontick',
        'onMarketUpdate',
        'onAccount',
        'error_ontick',
        'error_onMarketUpdate',
        'error_onAccount',
        'onOrderMatched',
        'onOrderPlaced',
        'onUpdateCallOrder',
    ]

    @classmethod
    def configure(kls):
        """
        Return a list of ConfigElement objects defining the configuration values for 
        this class
        User interfaces should then generate widgets based on this values, gather
        data and save back to the config dictionary for the bot.

        NOTE: when overriding you almost certainly will want to call the ancestor
        and then add your config values to the list.
        """
        # these configs are common to all bots
        return [
            ConfigElement(
                "account", "string", "",
                "BitShares account name for the bot to operate with", ""),
            ConfigElement(
                "market", "string", "USD:BTS",
                "BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"",
                "[A-Z]+:[A-Z]+")
        ]

    def __init__(self,
                 config,
                 name,
                 onAccount=None,
                 onOrderMatched=None,
                 onOrderPlaced=None,
                 onMarketUpdate=None,
                 onUpdateCallOrder=None,
                 ontick=None,
                 bitshares_instance=None,
                 *args,
                 **kwargs):
        # BitShares instance
        self.bitshares = bitshares_instance or shared_bitshares_instance()

        # Storage
        Storage.__init__(self, name)

        # Statemachine
        StateMachine.__init__(self, name)

        # Events
        Events.__init__(self)

        if ontick:
            self.ontick += ontick
        if onMarketUpdate:
            self.onMarketUpdate += onMarketUpdate
        if onAccount:
            self.onAccount += onAccount
        if onOrderMatched:
            self.onOrderMatched += onOrderMatched
        if onOrderPlaced:
            self.onOrderPlaced += onOrderPlaced
        if onUpdateCallOrder:
            self.onUpdateCallOrder += onUpdateCallOrder

        # Redirect this event to also call order placed and order matched
        self.onMarketUpdate += self._callbackPlaceFillOrders

        self.config = config
        self.bot = config["bots"][name]
        self._account = Account(self.bot["account"],
                                full=True,
                                bitshares_instance=self.bitshares)
        self._market = Market(config["bots"][name]["market"],
                              bitshares_instance=self.bitshares)

        # Settings for bitshares instance
        self.bitshares.bundle = bool(self.bot.get("bundle", False))

        # disabled flag - this flag can be flipped to True by a bot and
        # will be reset to False after reset only
        self.disabled = False

        # a private logger that adds bot identify data to the LogRecord
        self.log = logging.LoggerAdapter(
            logging.getLogger('dexbot.per_bot'), {
                'botname': name,
                'account': self.bot['account'],
                'market': self.bot['market'],
                'is_disabled': (lambda: self.disabled)
            })

    @property
    def orders(self):
        """ Return the bot's open accounts in the current market
        """
        self.account.refresh()
        return [
            o for o in self.account.openorders
            if self.bot["market"] == o.market and self.account.openorders
        ]

    @property
    def market(self):
        """ Return the market object as :class:`bitshares.market.Market`
        """
        return self._market

    @property
    def account(self):
        """ Return the full account as :class:`bitshares.account.Account` object!

            Can be refreshed by using ``x.refresh()``
        """
        return self._account

    def balance(self, asset):
        """ Return the balance of your bot's account for a specific asset
        """
        return self._account.balance(asset)

    @property
    def balances(self):
        """ Return the balances of your bot's account
        """
        return self._account.balances

    def _callbackPlaceFillOrders(self, d):
        """ This method distringuishes notifications caused by Matched orders
            from those caused by placed orders
        """
        if isinstance(d, FilledOrder):
            self.onOrderMatched(d)
        elif isinstance(d, Order):
            self.onOrderPlaced(d)
        elif isinstance(d, UpdateCallOrder):
            self.onUpdateCallOrder(d)
        else:
            pass

    def execute(self):
        """ Execute a bundle of operations
        """
        self.bitshares.blocking = "head"
        r = self.bitshares.txbuffer.broadcast()
        self.bitshares.blocking = False
        return r

    def cancelall(self):
        """ Cancel all orders of this bot
        """
        if self.orders:
            return self.bitshares.cancel([o["id"] for o in self.orders],
                                         account=self.account)
Beispiel #11
0
        #if len(open_orders) == 1:
        #    for h in account.history(1,1,10):
        #        print(h)
        #        o_num = h.get('result')
        #        first = o_num[0]
        #        print(first)
        #        order_num = o_num[1]
        #        print(order_num)
        #        if first == 1:
        #            #market.cancel(order_num, account)
        #            break  
        # удаляем старый займ
    elif current_ratio <= margin_ratio: 
        # если поймали моржа
        bbitasset = account.balance(base)
        print(bitasset)

        # подготавливаем переменную вида "1 CNY"
        amount = Amount(bitasset, base)
        print(amount)

        # ставим ордер на откуп нашего МК
        market.sell(0.00001, amount, 8640000, False, our_account) 

else:
    # займов нет, надо брать :)
    # расчитываем сколько юаней можем напечатать

    # получаем цену погашения
    ticker = market.ticker()
Beispiel #12
0
from bitshares.account import Account
#print('python shell test');
account = Account("jeaimetu-free")
print(account.balance("BTS"))
print(account.balance("BEANS"))
Beispiel #13
0
def dex_sell():

    # update wallet unlock to low latency node
    zprint('SELL')
    nds = race_read('nodes.txt')
    if isinstance(nds, list):
        nodes = nds
    account = Account(USERNAME,
                      bitshares_instance=BitShares(nodes, num_retries=0))
    market = Market(BitPAIR,
                    bitshares_instance=BitShares(nodes, num_retries=0),
                    mode='head')
    try:
        market.bitshares.wallet.unlock(PASS_PHRASE)
    except:
        pass
    # attempt to sell 10X or until satisfied
    def sell(price, amount):
        confirm.destroy()
        zprint('CONFIRMED')
        attempt = 1
        while attempt:
            try:
                details = market.sell(price, amount)
                print(details)
                attempt = 0
            except:
                zprint(("sell attempt %s failed" % attempt))
                attempt += 1
                if attempt > 10:
                    zprint('sell aborted')
                    return
                pass

    # interact with tkinter
    confirm = Tk()
    if market.bitshares.wallet.unlocked():
        price = sell_price.get()
        amount = sell_amount.get()
        if price == '':
            price = 0.5 * float(market.ticker()['latest'])
            sprice = 'market RATE'
        if amount == '':
            amount = ANTISAT
        try:
            price = float(price)
            amount = float(amount)
            if price != SATOSHI:
                sprice = '%.16f' % price
            assets = float(account.balance(BitASSET))
            if amount > (0.998 * assets):
                amount = 0.998 * assets
            samount = str(amount)
            sorder = str('CONFIRM SELL ' + samount + ' ' + BitASSET + ' @ ' +
                         sprice)

            if amount > 0:
                confirm.title(sorder)
                Button(confirm,
                       text='CONFIRM SELL',
                       command=lambda: sell(price, amount)).grid(row=1,
                                                                 column=0,
                                                                 pady=8)
                Button(confirm, text='INVALIDATE',
                       command=confirm.destroy).grid(row=2, column=0, pady=8)
            else:
                confirm.title('NO ASSETS TO SELL')
                Button(confirm, text='OK',
                       command=confirm.destroy).grid(row=2, column=0, pady=8)
        except:
            confirm.title('INVALID SELL ORDER')
            Button(confirm, text='OK', command=confirm.destroy).grid(row=2,
                                                                     column=0,
                                                                     pady=8)
    else:
        confirm.title('YOUR WALLET IS LOCKED')
        Button(confirm, text='OK', command=confirm.destroy).grid(row=2,
                                                                 column=0,
                                                                 pady=8)
    confirm.geometry('500x100+800+175')
    confirm.lift()
    confirm.call('wm', 'attributes', '.', '-topmost', True)
Beispiel #14
0
def book(nodes, a=None, b=None):  #updates orderbook details

    # create fresh websocket connections for this child instance
    account = Account(USERNAME,
                      bitshares_instance=BitShares(nodes, num_retries=0))
    market = Market(BitPAIR,
                    bitshares_instance=BitShares(nodes, num_retries=0),
                    mode='head')
    node = nodes[0]
    begin = time.time()
    while time.time() < (begin + TIMEOUT):
        time.sleep(random())
        try:
            # add unix time to trades dictionary
            trades = market.trades(limit=100)
            for t in range(len(trades)):
                ts = time.strptime(str(trades[t]['time']), '%Y-%m-%d %H:%M:%S')
                trades[t]['unix'] = int(time.mktime(ts))
                fprice = '%.16f' % float(trades[t]['price'])
                trades[t]['fprice'] = fprice[:10] + ',' + fprice[10:]
            # last price
            # last = market.ticker()['latest']
            last = float(trades[0]['price'])
            slast = '%.16f' % last
            # complete account balances
            call = decimal(time.time())
            raw = list(account.balances)
            elapsed = float(decimal(time.time()) - call)
            if elapsed > 1:
                continue
            elapsed = '%.17f' % elapsed
            cbalances = {}
            for i in range(len(raw)):
                cbalances[raw[i]['symbol']] = float(raw[i]['amount'])
            # orderbook
            raw = market.orderbook(limit=20)
            bids = raw['bids']
            asks = raw['asks']
            sbidp = [('%.16f' % bids[i]['price']) for i in range(len(bids))]
            saskp = [('%.16f' % asks[i]['price']) for i in range(len(asks))]
            sbidv = [('%.2f' % float(bids[i]['quote'])).rjust(12, ' ')
                     for i in range(len(bids))]
            saskv = [('%.2f' % float(asks[i]['quote'])).rjust(12, ' ')
                     for i in range(len(asks))]
            bidv = [float(bids[i]['quote']) for i in range(len(bids))]
            askv = [float(asks[i]['quote']) for i in range(len(asks))]
            cbidv = list(np.cumsum(bidv))
            caskv = list(np.cumsum(askv))
            cbidv = [('%.2f' % i).rjust(12, ' ') for i in cbidv]
            caskv = [('%.2f' % i).rjust(12, ' ') for i in caskv]
            # dictionary of currency and assets in this market
            currency = float(account.balance(BitCURRENCY))
            assets = float(account.balance(BitASSET))
            balances = {BitCURRENCY: currency, BitASSET: assets}
            # dictionary of open orders in traditional format:
            # orderNumber, orderType, market, amount, price
            orders = []
            for order in market.accountopenorders():
                orderNumber = order['id']
                asset = order['base']['symbol']
                currency = order['quote']['symbol']
                amount = float(order['base'])
                price = float(order['price'])
                orderType = 'buy'
                if asset == BitASSET:
                    orderType = 'sell'
                    price = 1 / price
                else:
                    amount = amount / price
                orders.append({
                    'orderNumber': orderNumber,
                    'orderType': orderType,
                    'market': BitPAIR,
                    'amount': amount,
                    'price': ('%.16f' % price)
                })
            trades = trades[:10]
            stale = int(time.time() - float(trades[0]['unix']))
            # display orderbooks
            print("\033c")
            print(time.ctime(), '            ', int(time.time()), '   ', a, b)
            print('                        PING', (elapsed), '   ', node)
            print('')
            print('                        LAST',
                  slast[:10] + ',' + slast[10:], '   ', BitPAIR)
            print('')
            print('            ', sbidv[0], '  ',
                  (sbidp[0])[:10] + ',' + (sbidp[0])[10:], '   ',
                  (saskp[0])[:10] + ',' + (saskp[0])[10:], (saskv[0]))
            print('                                           ', 'BIDS', '   ',
                  'ASKS')
            for i in range(1, len(sbidp)):
                print(cbidv[i], sbidv[i], '  ',
                      (sbidp[i])[:10] + ',' + (sbidp[i])[10:], '   ',
                      (saskp[i])[:10] + ',' + (saskp[i])[10:], saskv[i],
                      caskv[i])
            print('')
            for o in orders:
                print(o)
            if len(orders) == 0:
                print('                                  NO OPEN ORDERS')
            print('')
            print('%s BALANCE:' % BitPAIR)
            print(balances)
            print('')
            print('MARKET HISTORY:', stale, 'since last trade')
            for t in trades:
                # print(t.items())
                print(t['unix'],
                      str(t['time'])[11:19], t['fprice'],
                      ('%.4f' % float(t['quote']['amount'])).rjust(12, ' '))
            print('')
            print('ctrl+shift+\ will EXIT to terminal')
            print('')
            print('COMPLETE HOLDINGS:')
            print(cbalances)
        except:
            pass
class BaseStrategy(Storage, StateMachine, Events):
    """ Base Strategy and methods available in all Sub Classes that
        inherit this BaseStrategy.

        BaseStrategy inherits:

        * :class:`dexbot.storage.Storage`
        * :class:`dexbot.statemachine.StateMachine`
        * ``Events``

        Available attributes:

         * ``basestrategy.bitshares``: instance of ´`bitshares.BitShares()``
         * ``basestrategy.add_state``: Add a specific state
         * ``basestrategy.set_state``: Set finite state machine
         * ``basestrategy.get_state``: Change state of state machine
         * ``basestrategy.account``: The Account object of this worker
         * ``basestrategy.market``: The market used by this worker
         * ``basestrategy.orders``: List of open orders of the worker's account in the worker's market
         * ``basestrategy.balance``: List of assets and amounts available in the worker's account
         * ``basestrategy.log``: a per-worker logger (actually LoggerAdapter) adds worker-specific context:
            worker name & account (Because some UIs might want to display per-worker logs)

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

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

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

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

    __events__ = [
        'ontick',
        'onMarketUpdate',
        'onAccount',
        'error_ontick',
        'error_onMarketUpdate',
        'error_onAccount',
        'onOrderMatched',
        'onOrderPlaced',
        'onUpdateCallOrder',
    ]

    def __init__(self,
                 config,
                 name,
                 onAccount=None,
                 onOrderMatched=None,
                 onOrderPlaced=None,
                 onMarketUpdate=None,
                 onUpdateCallOrder=None,
                 ontick=None,
                 bitshares_instance=None,
                 *args,
                 **kwargs):
        # BitShares instance
        self.bitshares = bitshares_instance or shared_bitshares_instance()

        # Storage
        Storage.__init__(self, name)

        # Statemachine
        StateMachine.__init__(self, name)

        # Events
        Events.__init__(self)

        if ontick:
            self.ontick += ontick
        if onMarketUpdate:
            self.onMarketUpdate += onMarketUpdate
        if onAccount:
            self.onAccount += onAccount
        if onOrderMatched:
            self.onOrderMatched += onOrderMatched
        if onOrderPlaced:
            self.onOrderPlaced += onOrderPlaced
        if onUpdateCallOrder:
            self.onUpdateCallOrder += onUpdateCallOrder

        # Redirect this event to also call order placed and order matched
        self.onMarketUpdate += self._callbackPlaceFillOrders

        self.config = config
        self.worker = config["workers"][name]
        self._account = Account(self.worker["account"],
                                full=True,
                                bitshares_instance=self.bitshares)
        self._market = Market(config["workers"][name]["market"],
                              bitshares_instance=self.bitshares)

        # 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

        # A private logger that adds worker identify data to the LogRecord
        self.log = logging.LoggerAdapter(
            logging.getLogger('dexbot.per_worker'), {
                'worker_name': name,
                'account': self.worker['account'],
                'market': self.worker['market'],
                'is_disabled': lambda: self.disabled
            })

    @property
    def calculate_center_price(self):
        ticker = self.market.ticker()
        highest_bid = ticker.get("highestBid")
        lowest_ask = ticker.get("lowestAsk")
        if highest_bid is None or highest_bid == 0.0:
            self.log.critical(
                "Cannot estimate center price, there is no highest bid.")
            self.disabled = True
        elif lowest_ask is None or lowest_ask == 0.0:
            self.log.critical(
                "Cannot estimate center price, there is no lowest ask.")
            self.disabled = True
        else:
            center_price = (highest_bid['price'] + lowest_ask['price']) / 2
            return center_price

    @property
    def orders(self):
        """ Return the worker's open accounts in the current market
        """
        self.account.refresh()
        return [
            o for o in self.account.openorders
            if self.worker["market"] == o.market and self.account.openorders
        ]

    def get_order(self, order_id):
        for order in self.orders:
            if order['id'] == order_id:
                return order
        return False

    def get_updated_order(self, order):
        """ Tries to get the updated order from the API
            returns None if the order doesn't exist
        """
        if not order:
            return None
        if isinstance(order, str):
            order = {'id': order}
        for updated_order in self.updated_open_orders:
            if updated_order['id'] == order['id']:
                return updated_order
        return None

    @property
    def updated_open_orders(self):
        """
        Returns updated open Orders.
        account.openorders doesn't return updated values for the order so we calculate the values manually
        """
        self.account.refresh()
        self.account.ensure_full()

        limit_orders = self.account['limit_orders'][:]
        for o in limit_orders:
            base_amount = o['for_sale']
            price = o['sell_price']['base']['amount'] / o['sell_price'][
                'quote']['amount']
            quote_amount = base_amount / price
            o['sell_price']['base']['amount'] = base_amount
            o['sell_price']['quote']['amount'] = quote_amount

        orders = [
            Order(o, bitshares_instance=self.bitshares) for o in limit_orders
        ]

        return [o for o in orders if self.worker["market"] == o.market]

    @property
    def market(self):
        """ Return the market object as :class:`bitshares.market.Market`
        """
        return self._market

    @property
    def account(self):
        """ Return the full account as :class:`bitshares.account.Account` object!

            Can be refreshed by using ``x.refresh()``
        """
        return self._account

    def balance(self, asset):
        """ Return the balance of your worker's account for a specific asset
        """
        return self._account.balance(asset)

    @property
    def test_mode(self):
        return self.config['node'] == "wss://node.testnet.bitshares.eu"

    @property
    def balances(self):
        """ Return the balances of your worker's account
        """
        return self._account.balances

    def _callbackPlaceFillOrders(self, d):
        """ This method distinguishes notifications caused by Matched orders
            from those caused by placed orders
        """
        if isinstance(d, FilledOrder):
            self.onOrderMatched(d)
        elif isinstance(d, Order):
            self.onOrderPlaced(d)
        elif isinstance(d, UpdateCallOrder):
            self.onUpdateCallOrder(d)
        else:
            pass

    def execute(self):
        """ Execute a bundle of operations
        """
        self.bitshares.blocking = "head"
        r = self.bitshares.txbuffer.broadcast()
        self.bitshares.blocking = False
        return r

    def _cancel(self, orders):
        try:
            self.bitshares.cancel(orders, account=self.account)
        except bitsharesapi.exceptions.UnhandledRPCError as e:
            if str(
                    e
            ) == 'Assert Exception: maybe_found != nullptr: Unable to find Object':
                # The order(s) we tried to cancel doesn't exist
                print('nope')
                return False
            else:
                raise
        return True

    def cancel(self, orders):
        """ Cancel specific order(s)
        """
        if not isinstance(orders, (list, set, tuple)):
            orders = [orders]

        orders = [order['id'] for order in orders if 'id' in order]

        success = self._cancel(orders)
        if not success and len(orders) > 1:
            for order in orders:
                self._cancel(order)

    def cancel_all(self):
        """ Cancel all orders of the worker's account
        """
        if self.orders:
            self.log.info('Canceling all orders')
            self.cancel(self.orders)

    def purge(self):
        """ Clear all the worker data from the database and cancel all orders
        """
        self.cancel_all()
        self.clear()

    @staticmethod
    def get_order_amount(order, asset_type):
        try:
            order_amount = order[asset_type]['amount']
        except (KeyError, TypeError):
            order_amount = 0
        return order_amount

    def total_balance(self, order_ids=None, return_asset=False):
        """ Returns the combined balance of the given order ids and the account balance
            The amounts are returned in quote and base assets of the market

            :param order_ids: list of order ids to be added to the balance
            :param return_asset: true if returned values should be Amount instances
            :return: dict with keys quote and base
        """
        quote = 0
        base = 0
        quote_asset = self.market['quote']['id']
        base_asset = self.market['base']['id']

        for balance in self.balances:
            if balance.asset['id'] == quote_asset:
                quote += balance['amount']
            elif balance.asset['id'] == base_asset:
                base += balance['amount']

        orders_balance = self.orders_balance(order_ids)
        quote += orders_balance['quote']
        base += orders_balance['base']

        if return_asset:
            quote = Amount(quote, quote_asset)
            base = Amount(base, base_asset)

        return {'quote': quote, 'base': base}

    def orders_balance(self, order_ids, return_asset=False):
        if not order_ids:
            order_ids = []
        elif isinstance(order_ids, str):
            order_ids = [order_ids]

        quote = 0
        base = 0
        quote_asset = self.market['quote']['id']
        base_asset = self.market['base']['id']

        for order_id in order_ids:
            order = self.get_updated_order(order_id)
            if not order:
                continue
            asset_id = order['base']['asset']['id']
            if asset_id == quote_asset:
                quote += order['base']['amount']
            elif asset_id == base_asset:
                base += order['base']['amount']

        if return_asset:
            quote = Amount(quote, quote_asset)
            base = Amount(base, base_asset)

        return {'quote': quote, 'base': base}
def test_worker_balance(bitshares, accounts):
    a = Account('worker2', bitshares_instance=bitshares)
    assert a.balance('MYBASE') == 20000
    assert a.balance('MYQUOTE') == 5000
    assert a.balance('TEST') == 10000
Beispiel #17
0
class BaseStrategy(Storage, StateMachine, Events):
    """ Base Strategy and methods available in all Sub Classes that
        inherit this BaseStrategy.

        BaseStrategy inherits:

        * :class:`dexbot.storage.Storage`
        * :class:`dexbot.statemachine.StateMachine`
        * ``Events``

        Available attributes:

         * ``basestrategy.bitshares``: instance of ´`bitshares.BitShares()``
         * ``basestrategy.add_state``: Add a specific state
         * ``basestrategy.set_state``: Set finite state machine
         * ``basestrategy.get_state``: Change state of state machine
         * ``basestrategy.account``: The Account object of this worker
         * ``basestrategy.market``: The market used by this worker
         * ``basestrategy.orders``: List of open orders of the worker's account in the worker's market
         * ``basestrategy.balance``: List of assets and amounts available in the worker's account
         * ``basestrategy.log``: a per-worker logger (actually LoggerAdapter) adds worker-specific context:
            worker name & account (Because some UIs might want to display per-worker logs)

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

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

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

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

    __events__ = [
        'ontick',
        'onMarketUpdate',
        'onAccount',
        'error_ontick',
        'error_onMarketUpdate',
        'error_onAccount',
        'onOrderMatched',
        'onOrderPlaced',
        'onUpdateCallOrder',
    ]

    @classmethod
    def configure(cls):
        """
        Return a list of ConfigElement objects defining the configuration values for 
        this class
        User interfaces should then generate widgets based on this values, gather
        data and save back to the config dictionary for the worker.

        NOTE: when overriding you almost certainly will want to call the ancestor
        and then add your config values to the list.
        """
        # these configs are common to all bots
        return [
            ConfigElement(
                "account", "string", "",
                "BitShares account name for the bot to operate with", ""),
            ConfigElement(
                "market", "string", "USD:BTS",
                "BitShares market to operate on, in the format ASSET:OTHERASSET, for example \"USD:BTS\"",
                r"[A-Z\.]+[:\/][A-Z\.]+")
        ]

    def __init__(self,
                 name,
                 config=None,
                 onAccount=None,
                 onOrderMatched=None,
                 onOrderPlaced=None,
                 onMarketUpdate=None,
                 onUpdateCallOrder=None,
                 ontick=None,
                 bitshares_instance=None,
                 *args,
                 **kwargs):
        # BitShares instance
        self.bitshares = bitshares_instance or shared_bitshares_instance()

        # Storage
        Storage.__init__(self, name)

        # Statemachine
        StateMachine.__init__(self, name)

        # Events
        Events.__init__(self)

        if ontick:
            self.ontick += ontick
        if onMarketUpdate:
            self.onMarketUpdate += onMarketUpdate
        if onAccount:
            self.onAccount += onAccount
        if onOrderMatched:
            self.onOrderMatched += onOrderMatched
        if onOrderPlaced:
            self.onOrderPlaced += onOrderPlaced
        if onUpdateCallOrder:
            self.onUpdateCallOrder += onUpdateCallOrder

        # Redirect this event to also call order placed and order matched
        self.onMarketUpdate += self._callbackPlaceFillOrders

        if config:
            self.config = config
        else:
            self.config = config = Config.get_worker_config_file(name)

        self.worker = config["workers"][name]
        self._account = Account(self.worker["account"],
                                full=True,
                                bitshares_instance=self.bitshares)
        self._market = Market(config["workers"][name]["market"],
                              bitshares_instance=self.bitshares)

        # Recheck flag - Tell the strategy to check for updated orders
        self.recheck_orders = False

        # 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

        # A private logger that adds worker identify data to the LogRecord
        self.log = logging.LoggerAdapter(
            logging.getLogger('dexbot.per_worker'), {
                'worker_name': name,
                'account': self.worker['account'],
                'market': self.worker['market'],
                'is_disabled': lambda: self.disabled
            })

        self.orders_log = logging.LoggerAdapter(
            logging.getLogger('dexbot.orders_log'), {})

    def _calculate_center_price(self, suppress_errors=False):
        ticker = self.market.ticker()
        highest_bid = ticker.get("highestBid")
        lowest_ask = ticker.get("lowestAsk")
        if not float(highest_bid):
            if not suppress_errors:
                self.log.critical(
                    "Cannot estimate center price, there is no highest bid.")
                self.disabled = True
            return None
        elif lowest_ask is None or lowest_ask == 0.0:
            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):
        """ 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()
        else:
            # Center price was given so we only use the calculated center price
            # for quote to base asset conversion
            calculated_center_price = self._calculate_center_price(True)
            if not calculated_center_price:
                calculated_center_price = center_price

        if center_price:
            calculated_center_price = center_price

        if asset_offset:
            total_balance = self.total_balance(order_ids)
            total = (total_balance['quote'] *
                     calculated_center_price) + total_balance['base']

            if not total:  # Prevent division by zero
                balance = 0
            else:
                # Returns a value between -1 and 1
                balance = (total_balance['base'] / total) * 2 - 1

            if balance < 0:
                # With less of base asset center price should be offset downward
                calculated_center_price = calculated_center_price / math.sqrt(
                    1 + spread * (balance * -1))
            elif balance > 0:
                # With more of base asset center price will be offset upwards
                calculated_center_price = calculated_center_price * math.sqrt(
                    1 + spread * balance)
            else:
                calculated_center_price = calculated_center_price

        # Calculate final_offset_price if manual center price offset is given
        if manual_offset:
            calculated_center_price = calculated_center_price + (
                calculated_center_price * manual_offset)

        return calculated_center_price

    @property
    def orders(self):
        """ Return the worker's open accounts in the current market
        """
        self.account.refresh()
        return [
            o for o in self.account.openorders
            if self.worker["market"] == o.market and self.account.openorders
        ]

    @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 a dict with the id key in it
            :param bool return_none: return None instead of an empty
                Order object when the order doesn't exist
        """
        if not order_id:
            return None
        if 'id' in order_id:
            order_id = order_id['id']
        order = Order(order_id)
        if return_none and order['deleted']:
            return None
        return order

    def get_updated_order(self, order):
        """ Tries to get the updated order from the API
            returns None if the order doesn't exist
        """
        if not order:
            return None
        if isinstance(order, str):
            order = {'id': order}
        for updated_order in self.updated_open_orders:
            if updated_order['id'] == order['id']:
                return updated_order
        return None

    @property
    def updated_open_orders(self):
        """
        Returns updated open Orders.
        account.openorders doesn't return updated values for the order so we calculate the values manually
        """
        self.account.refresh()
        self.account.ensure_full()

        limit_orders = self.account['limit_orders'][:]
        for o in limit_orders:
            base_amount = float(o['for_sale'])
            price = float(o['sell_price']['base']['amount']) / float(
                o['sell_price']['quote']['amount'])
            quote_amount = base_amount / price
            o['sell_price']['base']['amount'] = base_amount
            o['sell_price']['quote']['amount'] = quote_amount

        orders = [
            Order(o, bitshares_instance=self.bitshares) for o in limit_orders
        ]

        return [o for o in orders if self.worker["market"] == o.market]

    @property
    def market(self):
        """ Return the market object as :class:`bitshares.market.Market`
        """
        return self._market

    @property
    def account(self):
        """ Return the full account as :class:`bitshares.account.Account` object!

            Can be refreshed by using ``x.refresh()``
        """
        return self._account

    def balance(self, asset):
        """ Return the balance of your worker's account for a specific asset
        """
        return self._account.balance(asset)

    @property
    def test_mode(self):
        return self.config['node'] == "wss://node.testnet.bitshares.eu"

    @property
    def balances(self):
        """ Return the balances of your worker's account
        """
        return self._account.balances

    def _callbackPlaceFillOrders(self, d):
        """ This method distinguishes notifications caused by Matched orders
            from those caused by placed orders
        """
        if isinstance(d, FilledOrder):
            self.onOrderMatched(d)
        elif isinstance(d, Order):
            self.onOrderPlaced(d)
        elif isinstance(d, UpdateCallOrder):
            self.onUpdateCallOrder(d)
        else:
            pass

    def execute(self):
        """ Execute a bundle of operations
        """
        self.bitshares.blocking = "head"
        r = self.bitshares.txbuffer.broadcast()
        self.bitshares.blocking = False
        return r

    def _cancel(self, orders):
        try:
            self.retry_action(self.bitshares.cancel,
                              orders,
                              account=self.account)
        except bitsharesapi.exceptions.UnhandledRPCError as e:
            if str(
                    e
            ) == 'Assert Exception: maybe_found != nullptr: Unable to find Object':
                # The order(s) we tried to cancel doesn't exist
                self.bitshares.txbuffer.clear()
                return False
            else:
                self.log.exception("Unable to cancel order")
        except bitshares.exceptions.MissingKeyError:
            self.log.exception(
                'Unable to cancel order(s), private key missing.')

        return True

    def cancel(self, orders):
        """ Cancel specific order(s)
        """
        if not isinstance(orders, (list, set, tuple)):
            orders = [orders]

        orders = [order['id'] for order in orders if 'id' in order]

        success = self._cancel(orders)
        if not success and len(orders) > 1:
            # One of the order cancels failed, cancel the orders one by one
            for order in orders:
                self._cancel(order)

    def cancel_all(self):
        """ Cancel all orders of the worker's account
        """
        self.log.info('Canceling all orders')
        if self.orders:
            self.cancel(self.orders)
        self.log.info("Orders canceled")

    def pause(self):
        """ Pause the worker
        """
        # By default, just call cancel_all(); strategies may override this method
        self.cancel_all()
        self.clear_orders()

    def market_buy(self, amount, price, return_none=False, *args, **kwargs):
        symbol = self.market['base']['symbol']
        precision = self.market['base']['precision']
        base_amount = self.truncate(price * amount, precision)

        # Make sure we have enough balance for the order
        if self.balance(self.market['base']) < 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,
                                            returnOrderId="head",
                                            *args,
                                            **kwargs)
        self.log.debug('Placed buy order {}'.format(buy_transaction))
        buy_order = self.get_order(buy_transaction['orderid'],
                                   return_none=return_none)
        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 market_sell(self, amount, price, return_none=False, *args, **kwargs):
        symbol = self.market['quote']['symbol']
        precision = self.market['quote']['precision']
        quote_amount = self.truncate(amount, precision)

        # 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,
                                             returnOrderId="head",
                                             *args,
                                             **kwargs)
        self.log.debug('Placed sell order {}'.format(sell_transaction))
        sell_order = self.get_order(sell_transaction['orderid'],
                                    return_none=return_none)
        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 calculate_order_data(self, order, amount, price):
        quote_asset = Amount(amount, self.market['quote']['symbol'])
        order['quote'] = quote_asset
        order['price'] = price
        base_asset = Amount(amount * price, self.market['base']['symbol'])
        order['base'] = base_asset
        return order

    def purge(self):
        """ Clear all the worker data from the database and cancel all orders
        """
        self.clear_orders()
        self.cancel_all()
        self.clear()

    @staticmethod
    def purge_worker_data(worker_name):
        Storage.clear_worker_data(worker_name)

    @staticmethod
    def get_order_amount(order, asset_type):
        try:
            order_amount = order[asset_type]['amount']
        except (KeyError, TypeError):
            order_amount = 0
        return order_amount

    def total_balance(self, order_ids=None, return_asset=False):
        """ Returns the combined balance of the given order ids and the account balance
            The amounts are returned in quote and base assets of the market

            :param order_ids: list of order ids to be added to the balance
            :param return_asset: true if returned values should be Amount instances
            :return: dict with keys quote and base
        """
        quote = 0
        base = 0
        quote_asset = self.market['quote']['id']
        base_asset = self.market['base']['id']

        for balance in self.balances:
            if balance.asset['id'] == quote_asset:
                quote += balance['amount']
            elif balance.asset['id'] == base_asset:
                base += balance['amount']

        orders_balance = self.orders_balance(order_ids)
        quote += orders_balance['quote']
        base += orders_balance['base']

        if return_asset:
            quote = Amount(quote, quote_asset)
            base = Amount(base, base_asset)

        return {'quote': quote, 'base': base}

    def orders_balance(self, order_ids, return_asset=False):
        if not order_ids:
            order_ids = []
        elif isinstance(order_ids, str):
            order_ids = [order_ids]

        quote = 0
        base = 0
        quote_asset = self.market['quote']['id']
        base_asset = self.market['base']['id']

        for order_id in order_ids:
            order = self.get_updated_order(order_id)
            if not order:
                continue
            asset_id = order['base']['asset']['id']
            if asset_id == quote_asset:
                quote += order['base']['amount']
            elif asset_id == base_asset:
                base += order['base']['amount']

        if return_asset:
            quote = Amount(quote, quote_asset)
            base = Amount(base, base_asset)

        return {'quote': quote, 'base': base}

    def retry_action(self, action, *args, **kwargs):
        """
        Perform an action, and if certain suspected-to-be-spurious graphene bugs occur,
        instead of bubbling the exception, it is quietly logged (level WARN), and try again
        tries a fixed number of times (MAX_TRIES) before failing
        """
        tries = 0
        while True:
            try:
                return action(*args, **kwargs)
            except bitsharesapi.exceptions.UnhandledRPCError as e:
                if "Assert Exception: amount_to_sell.amount > 0" in str(e):
                    if tries > MAX_TRIES:
                        raise
                    else:
                        tries += 1
                        self.log.warning("Ignoring: '{}'".format(str(e)))
                        self.bitshares.txbuffer.clear()
                        self.account.refresh()
                        time.sleep(2)
                elif "now <= trx.expiration" in str(
                        e):  # Usually loss of sync to blockchain
                    if tries > MAX_TRIES:
                        raise
                    else:
                        tries += 1
                        self.log.warning("retrying on '{}'".format(str(e)))
                        self.bitshares.txbuffer.clear()
                        time.sleep(6)  # Wait at least a BitShares block
                else:
                    raise

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

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

        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)
Beispiel #18
0
class BaseStrategy(Storage, StateMachine, Events):
    """ Base Strategy and methods available in all Sub Classes that
        inherit this BaseStrategy.

        BaseStrategy inherits:

        * :class:`stakemachine.storage.Storage`
        * :class:`stakemachine.statemachine.StateMachine`
        * ``Events``

        Available attributes:

         * ``basestrategy.bitshares``: instance of ´`bitshares.BitShares()``
         * ``basestrategy.add_state``: Add a specific state
         * ``basestrategy.set_state``: Set finite state machine
         * ``basestrategy.get_state``: Change state of state machine
         * ``basestrategy.account``: The Account object of this bot
         * ``basestrategy.market``: The market used by this bot
         * ``basestrategy.orders``: List of open orders of the bot's account in the bot's market
         * ``basestrategy.balance``: List of assets and amounts available in the bot's account

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

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

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

    __events__ = [
        'ontick',
        'onMarketUpdate',
        'onAccount',
        'error_ontick',
        'error_onMarketUpdate',
        'error_onAccount',
        'onOrderMatched',
        'onOrderPlaced',
        'onUpdateCallOrder',
    ]

    def __init__(
        self,
        config,
        name,
        onAccount=None,
        onOrderMatched=None,
        onOrderPlaced=None,
        onMarketUpdate=None,
        onUpdateCallOrder=None,
        ontick=None,
        bitshares_instance=None,
        *args,
        **kwargs
    ):
        # BitShares instance
        self.bitshares = bitshares_instance or shared_bitshares_instance()

        # Storage
        Storage.__init__(self, name)

        # Statemachine
        StateMachine.__init__(self, name)

        # Events
        Events.__init__(self)

        if ontick:
            self.ontick += ontick
        if onMarketUpdate:
            self.onMarketUpdate += onMarketUpdate
        if onAccount:
            self.onAccount += onAccount
        if onOrderMatched:
            self.onOrderMatched += onOrderMatched
        if onOrderPlaced:
            self.onOrderPlaced += onOrderPlaced
        if onUpdateCallOrder:
            self.onUpdateCallOrder += onUpdateCallOrder

        # Redirect this event to also call order placed and order matched
        self.onMarketUpdate += self._callbackPlaceFillOrders

        self.config = config
        self.bot = config["bots"][name]
        self._account = Account(
            self.bot["account"],
            full=True,
            bitshares_instance=self.bitshares
        )
        self._market = Market(
            config["bots"][name]["market"],
            bitshares_instance=self.bitshares
        )

        # Settings for bitshares instance
        self.bitshares.bundle = bool(self.bot.get("bundle", False))

        # disabled flag - this flag can be flipped to True by a bot and
        # will be reset to False after reset only
        self.disabled = False

    @property
    def orders(self):
        """ Return the bot's open accounts in the current market
        """
        self.account.refresh()
        return [o for o in self.account.openorders if self.bot["market"] == o.market and self.account.openorders]

    @property
    def market(self):
        """ Return the market object as :class:`bitshares.market.Market`
        """
        return self._market

    @property
    def account(self):
        """ Return the full account as :class:`bitshares.account.Account` object!

            Can be refreshed by using ``x.refresh()``
        """
        return self._account

    def balance(self, asset):
        """ Return the balance of your bot's account for a specific asset
        """
        return self._account.balance(asset)

    @property
    def balances(self):
        """ Return the balances of your bot's account
        """
        return self._account.balances

    def _callbackPlaceFillOrders(self, d):
        """ This method distringuishes notifications caused by Matched orders
            from those caused by placed orders
        """
        if isinstance(d, FilledOrder):
            self.onOrderMatched(d)
        elif isinstance(d, Order):
            self.onOrderPlaced(d)
        elif isinstance(d, UpdateCallOrder):
            self.onUpdateCallOrder(d)
        else:
            pass

    def execute(self):
        """ Execute a bundle of operations
        """
        self.bitshares.blocking = "head"
        r = self.bitshares.txbuffer.broadcast()
        self.bitshares.blocking = False
        return r

    def cancelall(self):
        """ Cancel all orders of this bot
        """
        if self.orders:
            return self.bitshares.cancel(
                [o["id"] for o in self.orders],
                account=self.account
            )