-
Notifications
You must be signed in to change notification settings - Fork 130
Description
Issue type
- bug
- missing functionality
- performance
- feature request
Brief description
A sticky order is an order that is executed as a limit price at the "best" possible bid (or ask price for shorts). The idea is to update the bid price whenever it changes repeating this as long as we haven't been filled. This kind of execution is very useful for automatic trading strategies that are sensitive to fees.
Bellow is an implementation of the execution strategy I've just described. Unfortunately it does not work as one would want, mainly due to the following reasons
- Sometimes when an order is placed in the orderbook, there is no way to access order status by its order ID. More precisely get_active_orders does not find an entry for the respective order. This happens randomly so its hard to diagnose.
- The bid/ask price is often way out of sync with the orderbook. Seems like the bid/ask reported by the REST interface is not precise enough or plain wrong? Same issue occurs using the WS interface. I see no other way to get the proper bid/ask other than keeping track of the whole orderbook, which is overkill for this task.
- In general it seems like a hard task to find the last order that was posted/executed. See the method get_last_order_hist .
Following is the code and an a usage example. Also to note is that this implementation was working just fine for about a month and started failing recently due to issue 1. Was the REST interface changed in this time?
Any kind of suggestions and improvements as to how this is currently implemented are welcome. I think many would benefit from having such a execution function available for use.
Steps to reproduce
B = Sticky(API, KEY)
await B.exec_order_maker(0.1)
Current bid/ask: 39844/39972
Could not get fill information for order: 65795090109
import asyncio
import numpy as np
from bfxapi import Client, Order
class Sticky():
"""Implementation of a sticky order interface for Bitfinex."""
def __init__(self, API, KEY, symbol='tBTCUSD'):
self.bfx = Client(API, KEY)
self.symbol = symbol
self.aff_code = 'au39AKAK7'
async def market_fill(self, amount):
"""Execute order for amount at market price. Returns the price filled."""
ret = await self.bfx.rest.submit_order(market_type=Order.Type.MARKET,
symbol=self.symbol,
amount=amount,
price='',
aff_code=self.aff_code)
if ret.status != 'SUCCESS':
raise Exception('Unable to submit REST order.')
return await self.get_fill_price(ret.notify_info[0].id)
async def exec_order_maker(self, amount, fallback_action=None, sleep_time=5, max_try=30):
"""This function tries to execute an order of size `amount' as a maker."""
if fallback_action is None:
fallback_action = self.market_fill
order_id = await self.post_order(amount, max_try, sleep_time)
if order_id is None:
print('Error. Could not post order. Reverting to fallback action.')
return await fallback_action(amount)
while True:
await asyncio.sleep(sleep_time)
# FIXME. Is there a better way to determine if we were filled? Is this even correct?
orders = [e for e in await self.bfx.rest.get_active_orders(symbol=self.symbol)
if e.id == order_id]
if not orders:
break
order = orders[0]
bid, ask = await self.bidask()
if (amount > 0 and bid > order.price) or (amount < 0 and ask < order.price):
print('Changing order price to', bid if amount > 0 else ask)
price = bid if amount > 0 else ask
try:
ret = await self.bfx.rest.submit_update_order(orderId=order_id, price=price)
# FIXME which exception exactly?
except Exception as e:
print(f'Failed to changed order ({e}). Filled already?')
# We have to make sure that the price didn't move
# adversely, thus getting our post-only order canceled
last_order = await self.get_last_order_hist(start=ret.notify_info.mts_update,
order_id=order_id)
if last_order is not None and last_order.status == 'POSTONLY CANCELED':
print('Update cancled our order. Figuring out our new order ID.')
order_id = await self.post_order(amount, max_try, sleep_time)
if order_id is None:
print('Error. Could not re-post order. Reverting to fallback action.')
return fallback_action(amount)
max_try -= 1
if max_try < 0:
print('Maximal number of tries reached. Reverting to fallback action.')
return await fallback_action(amount)
return await self.get_fill_price(order_id)
async def post_order(self, amount, max_try=10, sleep_time=1):
"""This function places an order in the orderbook. It makes sure the order
actually lands in the order book and is not executed at the market price."""
while True:
bid, ask = await self.bidask()
print(f'Current bid/ask: {bid}/{ask}.')
ret = await self.bfx.rest.submit_order(market_type=Order.Type.LIMIT,
symbol=self.symbol,
amount=amount,
price=bid if amount > 0 else ask,
post_only=True,
aff_code=self.aff_code)
if ret.status != 'SUCCESS':
raise Exception('REST order submission failed.')
start, order_id = ret.notify_info[0].mts_create, ret.notify_info[0].id
last_order = await self.get_last_order_hist(start=start, order_id=order_id)
# If the order landed in the order book then last_order *should* be None
if last_order is None or last_order.status != 'POSTONLY CANCELED':
return order_id
max_try -= 1
if max_try < 0:
print('Maximal number of tries reached.')
return None
await asyncio.sleep(sleep_time)
# FIXME This function often fails by not finding the last order that was just
# posted/executed. IS there a better way to get the last order?
async def get_last_order_hist(self, start=None, order_id=None, n_tries=10):
"""Returns the history of the last order. If order_id is specified then we want
to find the history of that specific order."""
for _ in range(n_tries):
hist = await self.bfx.rest.get_order_history(self.symbol, limit=1, sort=-1,
start=start, end=None)
if len(hist) == 0:
# It seems that sometimes BFX might be slow in updating histories
# and that might be the reason why hist == []
await asyncio.sleep(0.5)
continue
if order_id is None:
return hist[0]
# We need to make sure the ID's match as the REST engine is sometimes returning
# spurious answers. See, https://github.com/bitfinexcom/bitfinex-api-py/issues/116
if hist[0].id == order_id:
return hist[0]
return None
# FIXME. This function often fails not finding information about the given order.
async def get_fill_price(self, order_id):
"""Given an order ID, return the price at which it was executed."""
ret = await self.bfx.rest.get_order_trades(symbol=self.symbol, order_id=order_id)
if len(ret) == 0:
print(f'Could not get fill information for order: {order_id}')
# We are assuming there could be partial fills for different
# amounts/prices and, therefore, # we need to compute the weighted
# average. Is that true?
return sum(o.price*o.amount for o in ret)/sum(o.amount for o in ret)
# FIXME. Returned bid/ask is way, way off what the orderbook says. Why?
async def bidask(self):
"""Returns the current BID/ASK price."""
tick_data = await self.bfx.rest.get_public_ticker(symbol=self.symbol)
return tick_data[0], tick_data[2]