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)
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)
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)
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)
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"] } })
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)
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)
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)
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))
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)
#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()
from bitshares.account import Account #print('python shell test'); account = Account("jeaimetu-free") print(account.balance("BTS")) print(account.balance("BEANS"))
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)
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
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)
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 )