Skip to content

Commit

Permalink
Add check for zero spread
Browse files Browse the repository at this point in the history
KOTH worker must avoid filling of opposite orders in case spread is too
small. This commit adds a check for this.

Closes: #668
  • Loading branch information
bitphage committed Sep 3, 2019
1 parent b70308d commit 1d9153c
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 10 deletions.
30 changes: 30 additions & 0 deletions dexbot/strategies/king_of_the_hill.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ def __init__(self, *args, **kwargs):
self.last_check = datetime(2000, 1, 1)
self.min_check_interval = self.min_order_lifetime
self.partial_fill_threshold = 0.8
# Stubs
self.highest_bid = 0
self.lowest_ask = 0

if self.view:
self.update_gui_slider()
Expand Down Expand Up @@ -192,6 +195,15 @@ def get_top_prices(self):
self.log.debug('Top buy price to be higher: {:.8f}'.format(self.top_buy_price))
break

# Fill top prices from orderbook because we need to keep in mind own orders too
# FYI: getting price from self.ticker() doesn't work in local testnet
orderbook = self.get_orderbook_orders(depth=1)
try:
self.highest_bid = orderbook['bids'][0]['price']
self.lowest_ask = orderbook['asks'][0]['price']
except IndexError:
self.log.info('Market has empty orderbook')

def is_too_small_amounts(self, amount_quote, amount_base):
""" Check whether amounts are within asset precision limits
:param Decimal amount_quote: QUOTE asset amount
Expand Down Expand Up @@ -242,6 +254,15 @@ def place_order(self, order_type):
self.log.error('Amount for {} order is too small'.format(order_type))
return

# Check crossing with opposite orders
if price >= self.lowest_ask:
self.log.warning(
'Cannot place top {} order because it will cross the opposite side; '
'increase your order size to lower price step; my top price: {:.8f}, lowest ast: '
'{:.8f}'.format(order_type, price, self.lowest_ask)
)
return

new_order = self.place_market_buy_order(float(amount_quote), float(price))
self.beaten_buy_order = self.buy_order_to_beat
elif order_type == 'sell':
Expand Down Expand Up @@ -273,6 +294,15 @@ def place_order(self, order_type):
self.log.error('Amount for {} order is too small'.format(order_type))
return

# Check crossing with opposite orders
if price <= self.highest_bid:
self.log.warning(
'Cannot place top {} order because it will cross the opposite side; '
'increase your order size to lower price step; my top price: {:.8f}, highest bid: '
'{:.8f}'.format(order_type, price, self.highest_bid)
)
return

new_order = self.place_market_sell_order(float(amount_quote), float(price))
self.beaten_sell_order = self.sell_order_to_beat

Expand Down
45 changes: 37 additions & 8 deletions tests/strategies/king_of_the_hill/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def account(base_account):
return base_account()


@pytest.fixture(scope='module', params=[('QUOTEA', 'BASEA'), ('QUOTEB', 'BASEB')])
@pytest.fixture(params=[('QUOTEA', 'BASEA'), ('QUOTEB', 'BASEB')])
def config(request, bitshares, account, kh_worker_name):
""" Define worker's config with variable assets
Expand All @@ -68,7 +68,7 @@ def config(request, bitshares, account, kh_worker_name):
'mode': 'both',
'module': 'dexbot.strategies.king_of_the_hill',
'relative_order_size': False,
'sell_order_amount': 2.0,
'sell_order_amount': 1.0,
'sell_order_size_threshold': 0.0,
'upper_bound': 1.2,
}
Expand All @@ -87,7 +87,7 @@ def config_variable_modes(request, config, kh_worker_name):
return config


@pytest.fixture(scope='module')
@pytest.fixture
def config_other_account(config, base_account, kh_worker_name):
""" Config for other account which simulates foreign trader
"""
Expand Down Expand Up @@ -145,18 +145,24 @@ def orders1(worker):


@pytest.fixture
def other_orders(bitshares, kh_worker_name, config_other_account):
def other_worker(bitshares, kh_worker_name, config_other_account):
worker = StrategyBase(name=kh_worker_name, config=config_other_account, bitshares_instance=bitshares)
yield worker
worker.cancel_all_orders()
time.sleep(1.1)


@pytest.fixture
def other_orders(other_worker):
""" Place some orders from second account to simulate foreign trader
"""
worker = StrategyBase(name=kh_worker_name, config=config_other_account, bitshares_instance=bitshares)
worker = other_worker
worker.place_market_buy_order(10, 0.9)
worker.place_market_buy_order(10, 1)
worker.place_market_sell_order(10, 2.1)
worker.place_market_sell_order(10, 2)

yield worker
worker.cancel_all_orders()
time.sleep(1.1)
return worker


@pytest.fixture
Expand All @@ -166,3 +172,26 @@ def other_orders_out_of_bounds(other_orders):
worker = other_orders
worker.place_market_buy_order(10, worker.worker['upper_bound'] * 1.2)
worker.place_market_sell_order(10, worker.worker['lower_bound'] / 1.2)


@pytest.fixture
def other_orders_zero_spread(other_worker):
""" Place orders from another account simulating smallest possible spread
Note: main worker should use same order amount, otherwise this spread will not be "smallest possible"!!!
"""
worker = other_worker
orderid = worker.place_market_buy_order(2, 1, returnOrderId=True)
# When order amount is 1, we can use 1 precision step to adjust the price
# To understand, assume precision=2 and calc 1.01/1 and 10.01/10
precision = worker.market['base']['precision']
sell_price = 1 + 2 * 10 ** -precision
worker.place_market_sell_order(1, sell_price)
log.debug('Other orders before match: {}'.format(worker.own_orders))
buy_order = worker.get_order(orderid)
while buy_order['base']['amount'] == buy_order['for_sale']['amount']:
log.debug('Placing lower sell order')
sell_price -= 10 ** -precision
worker.place_market_sell_order(1, sell_price)
buy_order = worker.get_order(orderid)
log.debug('Other orders after match: {}'.format(worker.own_orders))
return worker
33 changes: 31 additions & 2 deletions tests/strategies/king_of_the_hill/test_king_of_the_hill.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
def test_amount_quote(worker):
""" Test quote amount calculation
"""
# config: 'sell_order_amount': 2.0,
assert worker.amount_quote == 2
# config: 'sell_order_amount': 1.0,
assert worker.amount_quote == 1

worker.is_relative_order_size = True
quote_balance = float(worker.balance(worker.market['quote']))
Expand Down Expand Up @@ -267,3 +267,32 @@ def test_maintain_strategy(worker, other_orders):
"""
worker.maintain_strategy()
assert len(worker.own_orders) == 2


def test_zero_spread(worker, other_orders_zero_spread):
""" Make sure the strategy doesn't crossing opposite side orders when market spread is too close
"""
other_worker = other_orders_zero_spread
other_orders_before = other_worker.own_orders

worker.upper_bound = 2
worker.lower_bound = 0.5
# Bounds should allow us to cross the spread
worker.buy_order_size_threshold = 0.00001
worker.sell_order_size_threshold = 0.00001

worker.get_top_prices()
worker.place_order('buy')
num_orders_1 = len(worker.own_orders)
worker.place_order('sell')
num_orders_2 = len(worker.own_orders)
# If the strategy placed both orders, they should not cross each other
assert num_orders_2 >= num_orders_1

other_orders_after = other_worker.own_orders
# Foreign orders left untouched
assert other_orders_after == other_orders_before

# Own orders not partially filled
for order in worker.own_orders:
assert order['base']['amount'] == order['for_sale']['amount']

0 comments on commit 1d9153c

Please sign in to comment.