Пример #1
0
def calls(ctx, obj):
    """ List call/short positions of an account or an asset
    """
    if obj.upper() == obj:
        # Asset
        from bitshares.asset import Asset
        asset = Asset(obj, full=True)
        calls = asset.get_call_orders(10)
        t = PrettyTable(
            ["acount", "debt", "collateral", "call price", "ratio"])
        t.align = 'r'
        for call in calls:
            t.add_row([
                str(call["account"]["name"]),
                str(call["debt"]),
                str(call["collateral"]),
                str(call["call_price"]),
                "%.2f" % (call["ratio"])
            ])
        click.echo(str(t))
    else:
        # Account
        from bitshares.dex import Dex
        dex = Dex(bitshares_instance=ctx.bitshares)
        calls = dex.list_debt_positions(account=obj)
        t = PrettyTable(["debt", "collateral", "call price", "ratio"])
        t.align = 'r'
        for symbol in calls:
            t.add_row([
                str(calls[symbol]["debt"]),
                str(calls[symbol]["collateral"]),
                str(calls[symbol]["call_price"]),
                "%.2f" % (calls[symbol]["ratio"])
            ])
        click.echo(str(t))
Пример #2
0
    def __init__(self,
                 name,
                 config=None,
                 _account=None,
                 _market=None,
                 fee_asset_symbol=None,
                 bitshares_instance=None,
                 bitshares_bundle=None,
                 *args,
                 **kwargs):

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

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

        # Storage
        Storage.__init__(self, name)

        # Events
        Events.__init__(self)

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

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

        self._market = _market
        self._account = _account

        # 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

        self.fee_asset = fee_asset_symbol

        # CER cache
        self.core_exchange_rate = None

        # Ticker
        self.ticker = self._market.ticker

        # Settings for bitshares instance
        self.bitshares.bundle = bitshares_bundle

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

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

        # buy/sell actions will return order id by default
        self.returnOrderId = 'head'
Пример #3
0
def borrow(ctx, amount, symbol, ratio, account):
    """ Borrow a bitasset/market-pegged asset
    """
    from bitshares.dex import Dex
    dex = Dex(bitshares_instance=ctx.bitshares)
    print_tx(
        dex.borrow(Amount(amount, symbol),
                   collateral_ratio=ratio,
                   account=account))
Пример #4
0
def calls(ctx, account):
    """ List call/short positions of an account
    """
    from bitshares.dex import Dex
    dex = Dex(bitshares_instance=ctx.bitshares)
    t = PrettyTable(["debt", "collateral", "call price", "ratio"])
    t.align = 'r'
    calls = dex.list_debt_positions(account=account)
    for symbol in calls:
        t.add_row([
            str(calls[symbol]["debt"]),
            str(calls[symbol]["collateral"]),
            str(calls[symbol]["call_price"]),
            "%.2f" % (calls[symbol]["ratio"])
        ])
    click.echo(str(t))
Пример #5
0
def bitasset_local(bitshares, base_bitasset, default_account):
    asset = base_bitasset()
    dex = Dex(blockchain_instance=bitshares)

    # Set initial price feed
    price = Price(1.5, base=asset, quote=Asset("TEST"))
    bitshares.publish_price_feed(asset.symbol, price, account=default_account)

    # Borrow some amount
    to_borrow = Amount(100, asset)
    dex.borrow(to_borrow, collateral_ratio=2.1, account=default_account)

    # Drop pricefeed to cause margin call
    price = Price(1.0, base=asset, quote=Asset("TEST"))
    bitshares.publish_price_feed(asset.symbol, price, account=default_account)

    return asset
Пример #6
0
def calls(ctx, obj, limit):
    """ List call/short positions of an account or an asset
    """
    if obj.upper() == obj:
        # Asset
        from bitshares.asset import Asset

        asset = Asset(obj, full=True)
        calls = asset.get_call_orders(limit)
        t = [["acount", "debt", "collateral", "call price", "ratio"]]
        for call in calls:
            t.append(
                [
                    str(call["account"]["name"]),
                    str(call["debt"]),
                    str(call["collateral"]),
                    str(call["call_price"]),
                    "%.2f" % (call["ratio"]),
                ]
            )
        print_table(t)
    else:
        # Account
        from bitshares.dex import Dex

        dex = Dex(bitshares_instance=ctx.bitshares)
        calls = dex.list_debt_positions(account=obj)
        t = [["debt", "collateral", "call price", "ratio"]]
        for symbol in calls:
            t.append(
                [
                    str(calls[symbol]["debt"]),
                    str(calls[symbol]["collateral"]),
                    str(calls[symbol]["call_price"]),
                    "%.2f" % (calls[symbol]["ratio"]),
                ]
            )
        print_table(t)
Пример #7
0
def list_fees(api_key: hug.types.text, request, hug_timer=5):
	"""Output the current Bitshares network fees in JSON."""
	if (check_api_token(api_key) == True): # Check the api key
		# API KEY VALID
		google_analytics(request, 'list_fees')

		network_fees = Dex().returnFees()
		extracted_fees = extract_object(network_fees)

		return {'network_fees': extracted_fees,
				'valid_key': True,
				'took': float(hug_timer)}
	else:
	# API KEY INVALID!
		return {'valid_key': False,
				'took': float(hug_timer)}
Пример #8
0
def updateratio(ctx, symbol, ratio, account):
    """ Update the collateral ratio of a call positions
    """
    from bitshares.dex import Dex
    dex = Dex(bitshares_instance=ctx.bitshares)
    print_tx(dex.adjust_collateral_ratio(symbol, ratio, account=account))
Пример #9
0
class BitsharesOrderEngine(Storage, Events):
    """
       All prices are passed and returned as BASE/QUOTE.
        (In the BREAD:USD market that would be USD/BREAD, 2.5 USD / 1 BREAD).
         - Buy orders reserve BASE
         - Sell orders reserve QUOTE

        OrderEngine inherits:
            * :class:`dexbot.storage.Storage` : Stores data to sqlite database
            * ``Events`` :The websocket endpoint of BitShares has notifications that are subscribed to
                            and dispatched by dexbot. This uses python's native Events

    """
    def __init__(self,
                 name,
                 config=None,
                 account=None,
                 market=None,
                 worker_market=None,
                 fee_asset_symbol=None,
                 bitshares_instance=None,
                 bitshares_bundle=None,
                 *args,
                 **kwargs):

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

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

        # Storage
        Storage.__init__(self, name)

        # Events
        Events.__init__(self)

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

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

        # Get Bitshares account and market for this worker
        self.account = account

        self.market = market

        self.worker_market = worker_market

        # 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 = fee_asset_symbol

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

        # CER cache
        self.core_exchange_rate = None

        # Ticker
        self.ticker = self.market.ticker

        # Settings for bitshares instance
        self.bitshares.bundle = bitshares_bundle

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

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

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

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

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

        return True

    def account_total_value(self, return_asset):
        """ Returns the total value of the account in given asset

            :param string | return_asset: Balance is returned as this asset
            :return: float: Value of the account in one asset
        """
        total_value = 0

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

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

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

        return total_value

    def balance(self, asset, fee_reservation=0):
        """ Return the balance of your worker's account in a specific asset.

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

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

        return balance

    def calculate_order_data(self, order, amount, price):
        quote_asset = Amount(amount,
                             self.market['quote']['symbol'],
                             bitshares_instance=self.bitshares)
        order['quote'] = quote_asset
        order['price'] = price
        base_asset = Amount(amount * price,
                            self.market['base']['symbol'],
                            bitshares_instance=self.bitshares)
        order['base'] = base_asset
        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.own_orders
        for order in orders:
            if order['base']['symbol'] == self.quote_asset:
                # Pick sell orders order's BASE amount, which is same as worker's QUOTE, to worker's BASE
                quote_total += order['base']['amount']
            else:
                base_total += order['base']['amount']

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

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

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

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

        self.log.info("Orders canceled")

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

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

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

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

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

            :param list | order_ids: list of order ids to be added to the balance
            :param bool | return_asset: true if returned values should be Amount instances
            :return: dict with keys quote and base

            Todo: When would we want the sum of a subset of orders? Why order_ids? Maybe just specify asset?
        """
        quote = 0
        base = 0
        quote_asset = self.market['quote']['id']
        base_asset = self.market['base']['id']

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

        if order_ids is None:
            # Get all orders from Blockchain
            order_ids = [order['id'] for order in self.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,
                           bitshares_instance=self.bitshares)
            base = Amount(base, base_asset, bitshares_instance=self.bitshares)

        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,
                           bitshares_instance=self.bitshares)
            base = Amount(base, base_asset, bitshares_instance=self.bitshares)

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

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

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

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

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

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

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

    def get_market_orders(self, depth=1, updated=True):
        """ Returns orders from the current market. Orders are sorted by price.

            get_limit_orders() call does not have any depth limit.

            :param int | depth: Amount of orders per side will be fetched, default=1
            :param bool | updated: Return updated orders. "Updated" means partially filled orders will represent
                                   remainders and not just initial amounts
            :return: Returns a list of orders or None
        """
        orders = self.bitshares.rpc.get_limit_orders(
            self.market['base']['id'], self.market['quote']['id'], depth)
        if updated:
            orders = [self.get_updated_limit_order(o) for o in orders]
        orders = [Order(o, bitshares_instance=self.bitshares) for o in orders]
        return orders

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

            :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, LLC, or any other
            :return:
        """
        # Get fee
        fees = self.dex.returnFees()
        limit_order_create = fees['limit_order_create']
        return self.convert_fee(limit_order_create['fee'], fee_asset)

    def get_own_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.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.own_orders

        return self.filter_sell_orders(orders)

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

            :return: float or None: Own spread
        """
        try:
            # Try fetching own orders
            highest_own_buy_price = self.get_highest_own_buy_order().get(
                'price')
            lowest_own_sell_price = self.get_lowest_own_sell_order().get(
                'price')
        except AttributeError:
            return None

        # Calculate actual spread
        actual_spread = lowest_own_sell_price / highest_own_buy_price - 1
        return actual_spread

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

            :param str|dict order_id: blockchain Order object or id of the order
        """
        if isinstance(order_id, dict):
            order_id = order_id['id']

        # At first, try to look up own orders. This prevents RPC calls whether requested order is own order
        order = None
        for limit_order in self.account['limit_orders']:
            if order_id == limit_order['id']:
                order = limit_order
                break
        else:
            # We are using direct rpc call here because passing an Order object to self.get_updated_limit_order() give
            # us weird error "Object of type 'BitShares' is not JSON serializable"
            order = self.bitshares.rpc.get_objects([order_id])[0]

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

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

    def execute(self):
        """ Execute a bundle of operations

            :return: dict: transaction
        """
        self.bitshares.blocking = "head"
        r = self.bitshares.txbuffer.broadcast()
        self.bitshares.blocking = False
        return r

    def is_buy_order(self, order):
        """ Check whether an order is buy order

            :param dict | order: dict or Order object
            :return bool
        """
        # Check if the order is buy order, by comparing asset symbol of the order and the market
        if order['base']['symbol'] == self.market['base']['symbol']:
            return True
        else:
            return False

    def is_current_market(self, base_asset_id, quote_asset_id):
        """ Returns True if given asset id's are of the current market

            :return: bool: True = Current market, False = Not current market
        """
        if quote_asset_id == self.market['quote']['id']:
            if base_asset_id == self.market['base']['id']:
                return True
            return False

        # Todo: Should we return true if market is opposite?
        if quote_asset_id == self.market['base']['id']:
            if base_asset_id == self.market['quote']['id']:
                return True
            return False

        return False

    def is_sell_order(self, order):
        """ Check whether an order is sell order

            :param dict | order: dict or Order object
            :return bool
        """
        # Check if the order is sell order, by comparing asset symbol of the order and the market
        if order['base']['symbol'] == self.market['quote']['symbol']:
            return True
        else:
            return False

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

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

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

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

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

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

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

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

            :param float | amount: Order amount in QUOTE
            :param float | price: Order price in BASE
            :param bool | return_none:
            :param bool | invert: True = return inverted sell order
            :param args:
            :param kwargs:
            :return:
        """
        symbol = self.market['quote']['symbol']
        precision = self.market['quote']['precision']
        quote_amount = truncate(amount, precision)
        return_order_id = kwargs.pop('returnOrderId', self.returnOrderId)

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

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

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

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

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

    def retry_action(self, action, *args, **kwargs):
        """ Perform an action, and if certain suspected-to-be-spurious grapheme bugs occur,
            instead of bubbling the exception, it is quietly logged (level WARN), and try again
            tries a fixed number of times (MAX_TRIES) before failing

            :param action:
            :return:
        """
        tries = 0
        while True:
            try:
                return action(*args, **kwargs)
            except bitsharesapi.exceptions.UnhandledRPCError as exception:
                if "Assert Exception: amount_to_sell.amount > 0" in str(
                        exception):
                    if tries > MAX_TRIES:
                        raise
                    else:
                        tries += 1
                        self.log.warning("Ignoring: '{}'".format(
                            str(exception)))
                        self.bitshares.txbuffer.clear()
                        self.account.refresh()
                        time.sleep(2)
                elif "now <= trx.expiration" in str(
                        exception):  # Usually loss of sync to blockchain
                    if tries > MAX_TRIES:
                        raise
                    else:
                        tries += 1
                        self.log.warning("retrying on '{}'".format(
                            str(exception)))
                        self.bitshares.txbuffer.clear()
                        time.sleep(6)  # Wait at least a BitShares block
                elif "trx.expiration <= now + chain_parameters.maximum_time_until_expiration" in str(
                        exception):
                    if tries > MAX_TRIES:
                        info = self.bitshares.info()
                        raise Exception(
                            'Too much difference between node block time and trx expiration, please change '
                            'the node. Block time: {}, local time: {}'.format(
                                info['time'],
                                formatTime(datetime.datetime.utcnow())))
                    else:
                        tries += 1
                        self.log.warning(
                            'Too much difference between node block time and trx expiration, switching '
                            'node')
                        self.bitshares.txbuffer.clear()
                        self.bitshares.rpc.next()
                elif "Assert Exception: delta.amount > 0: Insufficient Balance" in str(
                        exception):
                    self.log.critical('Insufficient balance of fee asset')
                    raise
                else:
                    raise

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

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

    def get_own_orders(self, refresh=True):
        """ Return the account's open orders in the current market

            :param bool refresh: Use most recent data
            :return: List of Order objects
        """
        orders = []

        # Refresh account data
        if refresh:
            self.account.refresh()

        for order in self.account.openorders:
            if self.worker_market == order.market and self.account.openorders:
                orders.append(order)

        return orders

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

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

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

        return orders

    @property
    def all_own_orders(self):
        """ Return the worker's open orders in all markets
        """
        return self.get_all_own_orders()

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

    @staticmethod
    def get_updated_limit_order(limit_order):
        """ Returns a modified limit_order so that when passed to Order class,
            will return an Order object with updated amount values

            :param limit_order: an item of Account['limit_orders'] or bitshares.rpc.get_limit_orders()
            :return: Order
        """
        order = copy.deepcopy(limit_order)
        price = float(order['sell_price']['base']['amount']) / float(
            order['sell_price']['quote']['amount'])
        base_amount = float(order['for_sale'])
        quote_amount = base_amount / price
        order['sell_price']['base']['amount'] = base_amount
        order['sell_price']['quote']['amount'] = quote_amount
        return order

    @staticmethod
    def convert_asset(from_value, from_asset, to_asset):
        """ Converts asset to another based on the latest market value

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

        return truncate((from_value * latest_price), precision)

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

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

        if fee_asset['id'] == '1.3.0':
            # Fee asset is LLC, 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',
                                         bitshares_instance=self.bitshares))
                self.core_exchange_rate = temp_market.ticker(
                )['core_exchange_rate']
            return fee_amount * self.core_exchange_rate['base']['amount']

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

            :param str | dict order_id: blockchain object id of the order can be an order dict with the id key in it
            :param bool return_none: return None instead of an empty Order object when the order doesn't exist
            :return: Order object
        """
        if not order_id:
            return None
        if 'id' in order_id:
            order_id = order_id['id']
        try:
            order = Order(order_id, bitshares_instance=self.bitshares)
        except Exception:
            logging.getLogger(__name__).error(
                'Got an exception getting order id {}'.format(order_id))
            raise
        if return_none and order['deleted']:
            return None
        return order

    def is_partially_filled(self, order, threshold=0.3):
        """ Checks whether order was partially filled

            :param dict order: Order instance
            :param float fill_threshold: Order fill threshold, relative
            :return: bool True = Order is filled more than threshold
                          False = Order is not partially filled
        """
        if self.is_buy_order(order):
            order_type = 'buy'
            price = order['price']
        else:
            order_type = 'sell'
            price = order['price']**-1

        if order['for_sale']['amount'] != order['base']['amount']:
            diff_abs = order['base']['amount'] - order['for_sale']['amount']
            diff_rel = diff_abs / order['base']['amount']
            if diff_rel > threshold:
                self.log.debug(
                    'Partially filled {} order: {} {} @ {:.8f}, filled: {:.2%}'
                    .format(order_type, order['base']['amount'],
                            order['base']['symbol'], price, diff_rel))
                return True
        return False
Пример #10
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)
Пример #11
0
    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'), {})
Пример #12
0
    def __init__(
        self,
        name,
        config=None,
        _account=None,
        _market=None,
        fee_asset_symbol=None,
        bitshares_instance=None,
        *args,
        **kwargs
    ):

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

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

        # Storage
        Storage.__init__(self, name)

        # Events
        Events.__init__(self)

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

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

        # Get worker's parameters from the config
        self.worker = config["workers"][name]
        self._market = _market or Market(self.worker["market"], bitshares_instance=self.bitshares)
        self._account = _account or Account(self.worker["account"], full=True, bitshares_instance=self.bitshares)

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

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

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

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

        # CER cache
        self.core_exchange_rate = None

        # 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'

        self.log = logging.LoggerAdapter(logging.getLogger('dexbot.orderengine'), {})
Пример #13
0
from bitshares import BitShares
from bitshares.account import Account
from bitshares.asset import Asset
from bitshares.market import Market
from bitshares.amount import Amount
from bitshares.dex import Dex

# коннектимся к рудексу
bitshares = BitShares("wss://node.market.rudex.org")
dex = Dex(bitshares)

# открываем свой локальный кошелек
dex.bitshares.wallet.unlock("secret_wallet_password")
# открываем свой локальный кошелек
#bitshares.wallet.unlock("secret_wallet_password")

# наш коэф. перекрытия
init_ratio = 2.00
max_ratio = 2.10
margin_ratio = 1.75

# наш аккаунт
our_account = "id-b0t"
our_symbol = "CNY"
current_ratio = 0

# наша пара
base = Asset(our_symbol)
quote = Asset("BTS")

# выбираем рынок