Update dry-run order handling to use realistic fill prices
closes #3389
This commit is contained in:
parent
a0893b291a
commit
1e988c97ad
@ -561,7 +561,7 @@ class Exchange:
|
||||
rate: float, params: Dict = {}) -> Dict[str, Any]:
|
||||
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
|
||||
_amount = self.amount_to_precision(pair, amount)
|
||||
dry_order = {
|
||||
dry_order: Dict[str, Any] = {
|
||||
'id': order_id,
|
||||
'symbol': pair,
|
||||
'price': rate,
|
||||
@ -577,26 +577,73 @@ class Exchange:
|
||||
'fee': None,
|
||||
'info': {}
|
||||
}
|
||||
self._store_dry_order(dry_order, pair)
|
||||
if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]:
|
||||
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
||||
|
||||
if dry_order["type"] == "market":
|
||||
# Update market order pricing
|
||||
average = self.get_dry_market_fill_price(pair, side, amount, rate)
|
||||
dry_order.update({
|
||||
'average': average,
|
||||
'cost': dry_order['amount'] * average,
|
||||
})
|
||||
self.add_dry_order_fee(pair, dry_order)
|
||||
|
||||
self._dry_run_open_orders[dry_order["id"]] = dry_order
|
||||
# Copy order and close it - so the returned order is open unless it's a market order
|
||||
return dry_order
|
||||
|
||||
def _store_dry_order(self, dry_order: Dict, pair: str) -> None:
|
||||
closed_order = dry_order.copy()
|
||||
if closed_order['type'] in ["market", "limit"]:
|
||||
closed_order.update({
|
||||
'status': 'closed',
|
||||
'filled': closed_order['amount'],
|
||||
'remaining': 0,
|
||||
'fee': {
|
||||
'currency': self.get_pair_quote_currency(pair),
|
||||
'cost': dry_order['cost'] * self.get_fee(pair),
|
||||
'rate': self.get_fee(pair)
|
||||
}
|
||||
})
|
||||
if closed_order["type"] in ["stop_loss_limit", "stop-loss-limit"]:
|
||||
closed_order["info"].update({"stopPrice": closed_order["price"]})
|
||||
self._dry_run_open_orders[closed_order["id"]] = closed_order
|
||||
def add_dry_order_fee(self, pair: str, dry_order: Dict[str, Any]):
|
||||
dry_order.update({
|
||||
'fee': {
|
||||
'currency': self.get_pair_quote_currency(pair),
|
||||
'cost': dry_order['cost'] * self.get_fee(pair),
|
||||
'rate': self.get_fee(pair)
|
||||
}
|
||||
})
|
||||
|
||||
def get_dry_market_fill_price(self, pair: str, side: str, amount: float, rate: float) -> float:
|
||||
"""
|
||||
Get the market order fill price based on orderbook interpolation
|
||||
"""
|
||||
if self.exchange_has('fetchL2OrderBook'):
|
||||
ob = self.fetch_l2_order_book(pair, 20)
|
||||
book_entry_type = 'asks' if side == 'buy' else 'bids'
|
||||
|
||||
remaining_amount = amount
|
||||
filled_amount = 0
|
||||
for book_entry in ob[book_entry_type]:
|
||||
book_entry_price = book_entry[0]
|
||||
book_entry_coin_volume = book_entry[1]
|
||||
book_entry_ref_currency_volume = book_entry_price * book_entry_coin_volume
|
||||
if remaining_amount > 0:
|
||||
if remaining_amount < book_entry_ref_currency_volume:
|
||||
filled_amount += remaining_amount * book_entry_price
|
||||
else:
|
||||
filled_amount += book_entry_ref_currency_volume * book_entry_price
|
||||
remaining_amount -= book_entry_ref_currency_volume
|
||||
else:
|
||||
break
|
||||
forecast_avg_filled_price = filled_amount / amount
|
||||
return self.price_to_precision(pair, forecast_avg_filled_price)
|
||||
|
||||
return rate
|
||||
|
||||
def dry_limit_order_filled(self, pair: str, side: str, limit: float) -> bool:
|
||||
if not self.exchange_has('fetchL2OrderBook'):
|
||||
return True
|
||||
ob = self.fetch_l2_order_book(pair, 1)
|
||||
if side == 'buy':
|
||||
price = ob['asks'][0][0]
|
||||
logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}")
|
||||
if limit >= price:
|
||||
return True
|
||||
else:
|
||||
price = ob['bids'][0][0]
|
||||
logger.debug(f"{pair} checking dry sell-order: price={price}, limit={limit}")
|
||||
if limit <= price:
|
||||
return True
|
||||
return False
|
||||
|
||||
def fetch_dry_run_order(self, order_id) -> Dict[str, Any]:
|
||||
"""
|
||||
@ -605,6 +652,16 @@ class Exchange:
|
||||
"""
|
||||
try:
|
||||
order = self._dry_run_open_orders[order_id]
|
||||
pair = order['symbol']
|
||||
if order['status'] != "closed" and order['type'] in ["limit"]:
|
||||
if self.dry_limit_order_filled(pair, order['side'], order['price']):
|
||||
order.update({
|
||||
'status': 'closed',
|
||||
'filled': order['amount'],
|
||||
'remaining': 0,
|
||||
})
|
||||
self.add_dry_order_fee(pair, order)
|
||||
|
||||
return order
|
||||
except KeyError as e:
|
||||
# Gracefully handle errors with dry-run orders.
|
||||
|
@ -2117,6 +2117,7 @@ def test_get_historic_trades_notsupported(default_conf, mocker, caplog, exchange
|
||||
def test_cancel_order_dry_run(default_conf, mocker, exchange_name):
|
||||
default_conf['dry_run'] = True
|
||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||
mocker.patch('freqtrade.exchange.Exchange.dry_limit_order_filled', return_value=True)
|
||||
assert exchange.cancel_order(order_id='123', pair='TKN/BTC') == {}
|
||||
assert exchange.cancel_stoploss_order(order_id='123', pair='TKN/BTC') == {}
|
||||
|
||||
|
@ -996,7 +996,8 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets):
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
markets=PropertyMock(return_value=markets),
|
||||
dry_limit_order_filled=MagicMock(return_value=True),
|
||||
)
|
||||
patch_get_signal(ftbot, (True, False))
|
||||
|
||||
|
@ -225,6 +225,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
dry_limit_order_filled=MagicMock(return_value=True),
|
||||
)
|
||||
status_table = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
@ -671,6 +672,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
dry_limit_order_filled=MagicMock(return_value=True),
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
@ -729,6 +731,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
dry_limit_order_filled=MagicMock(return_value=True),
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
@ -789,6 +792,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
get_fee=fee,
|
||||
dry_limit_order_filled=MagicMock(return_value=True),
|
||||
)
|
||||
default_conf['max_open_trades'] = 4
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
|
@ -2750,6 +2750,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
|
||||
price_to_precision=lambda s, x, y: y,
|
||||
stoploss=stoploss,
|
||||
cancel_stoploss_order=cancel_order,
|
||||
dry_limit_order_filled=MagicMock(return_value=True),
|
||||
)
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
@ -2792,6 +2793,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
|
||||
get_fee=fee,
|
||||
amount_to_precision=lambda s, x, y: y,
|
||||
price_to_precision=lambda s, x, y: y,
|
||||
dry_limit_order_filled=MagicMock(return_value=True),
|
||||
)
|
||||
|
||||
stoploss = MagicMock(return_value={
|
||||
|
Loading…
Reference in New Issue
Block a user