fix first important tests in test_freqtradebot, update and fix on order related Trade class hybrid_properties

This commit is contained in:
axel
2023-06-15 01:55:13 -04:00
parent 450fc5763f
commit 9cdff0b0a5
4 changed files with 79 additions and 39 deletions

View File

@@ -373,7 +373,10 @@ class FreqtradeBot(LoggingMixin):
"Order is older than 5 days. Assuming order was fully cancelled.")
fo = order.to_ccxt_object()
fo['status'] = 'canceled'
self.handle_cancel_order(fo, order.trade, constants.CANCEL_REASON['TIMEOUT'])
self.handle_cancel_order(
fo, order.order_id, order.trade,
constants.CANCEL_REASON['TIMEOUT']
)
except ExchangeError as e:
@@ -1318,26 +1321,33 @@ class FreqtradeBot(LoggingMixin):
:return: None
"""
for trade in Trade.get_open_order_trades():
try:
if not trade.open_order_id:
for open_order in trade.open_orders:
try:
order = self.exchange.fetch_order(open_order.order_id, trade.pair)
except (ExchangeError):
logger.info(
'Cannot query order for %s due to %s', trade, traceback.format_exc()
)
continue
order = self.exchange.fetch_order(trade.open_order_id, trade.pair)
except (ExchangeError):
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
continue
fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
not_closed = order['status'] == 'open' or fully_cancelled
order_obj = trade.select_order_by_order_id(trade.open_order_id)
fully_cancelled = self.update_trade_state(trade, open_order.order_id, order)
not_closed = order['status'] == 'open' or fully_cancelled
order_obj = trade.select_order_by_order_id(open_order.order_id)
if not_closed:
if fully_cancelled or (order_obj and self.strategy.ft_check_timed_out(
trade, order_obj, datetime.now(timezone.utc))):
self.handle_cancel_order(order, trade, constants.CANCEL_REASON['TIMEOUT'])
else:
self.replace_order(order, order_obj, trade)
if not_closed:
if fully_cancelled or (
order_obj and self.strategy.ft_check_timed_out(
trade, order_obj, datetime.now(timezone.utc)
)
):
self.handle_cancel_order(
order, open_order.order_id, trade, constants.CANCEL_REASON['TIMEOUT']
)
else:
self.replace_order(order, order_obj, trade)
def handle_cancel_order(self, order: Dict, trade: Trade, reason: str) -> None:
def handle_cancel_order(self, order: Dict, order_id: str, trade: Trade, reason: str) -> None:
"""
Check if current analyzed order timed out and cancel if necessary.
:param order: Order dict grabbed with exchange.fetch_order()
@@ -1345,7 +1355,7 @@ class FreqtradeBot(LoggingMixin):
:return: None
"""
if order['side'] == trade.entry_side:
self.handle_cancel_enter(trade, order, reason)
self.handle_cancel_enter(trade, order, order_id, reason)
else:
canceled = self.handle_cancel_exit(trade, order, reason)
canceled_count = trade.get_exit_order_count()
@@ -1399,7 +1409,7 @@ class FreqtradeBot(LoggingMixin):
cancel_reason = constants.CANCEL_REASON['USER_CANCEL']
if order_obj.price != adjusted_entry_price:
# cancel existing order if new price is supplied or None
self.handle_cancel_enter(trade, order, cancel_reason,
self.handle_cancel_enter(trade, order, order_obj.order_id, cancel_reason,
replacing=replacing)
if adjusted_entry_price:
# place new order only if new price is supplied
@@ -1436,8 +1446,8 @@ class FreqtradeBot(LoggingMixin):
Trade.commit()
def handle_cancel_enter(
self, trade: Trade, order: Dict, reason: str,
replacing: Optional[bool] = False
self, trade: Trade, order: Dict, order_id: str,
reason: str, replacing: Optional[bool] = False
) -> bool:
"""
entry cancel - cancel order
@@ -1446,7 +1456,7 @@ class FreqtradeBot(LoggingMixin):
"""
was_trade_fully_canceled = False
side = trade.entry_side.capitalize()
if not trade.open_order_id:
if trade.open_orders_count == 0:
logger.warning(f"No open order for {trade}.")
return False
@@ -1459,16 +1469,16 @@ class FreqtradeBot(LoggingMixin):
if filled_val > 0 and minstake and filled_stake < minstake:
logger.warning(
f"Order {trade.open_order_id} for {trade.pair} not cancelled, "
f"Order {order_id} for {trade.pair} not cancelled, "
f"as the filled amount of {filled_val} would result in an unexitable trade.")
return False
corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
corder = self.exchange.cancel_order_with_result(order_id, trade.pair,
trade.amount)
# Avoid race condition where the order could not be cancelled coz its already filled.
# Simply bailing here is the only safe way - as this order will then be
# handled in the next iteration.
if corder.get('status') not in constants.NON_OPEN_EXCHANGE_STATES:
logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.")
logger.warning(f"Order {order_id} for {trade.pair} not cancelled.")
return False
else:
# Order was cancelled already, so we can reuse the existing dict
@@ -1488,14 +1498,12 @@ class FreqtradeBot(LoggingMixin):
was_trade_fully_canceled = True
reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}"
else:
self.update_trade_state(trade, trade.open_order_id, corder)
trade.open_order_id = None
self.update_trade_state(trade, order_id, corder)
logger.info(f'{side} Order timeout for {trade}.')
else:
# update_trade_state (and subsequently recalc_trade_from_orders) will handle updates
# to the trade object
self.update_trade_state(trade, trade.open_order_id, corder)
trade.open_order_id = None
self.update_trade_state(trade, order_id, corder)
logger.info(f'Partial {trade.entry_side} order timeout for {trade}.')
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
@@ -1505,7 +1513,10 @@ class FreqtradeBot(LoggingMixin):
reason=reason)
return was_trade_fully_canceled
def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> bool:
def handle_cancel_exit(
self, trade: Trade, order: Dict, order_id: str,
reason: str
) -> bool:
"""
exit order cancel - cancel order and update trade
:return: True if exit order was cancelled, false otherwise
@@ -1522,7 +1533,7 @@ class FreqtradeBot(LoggingMixin):
reason = constants.CANCEL_REASON['PARTIALLY_FILLED']
if minstake and filled_rem_stake < minstake:
logger.warning(
f"Order {trade.open_order_id} for {trade.pair} not cancelled, as "
f"Order {order_id} for {trade.pair} not cancelled, as "
f"the filled amount of {filled_val} would result in an unexitable trade.")
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
@@ -1539,7 +1550,7 @@ class FreqtradeBot(LoggingMixin):
order['id'], trade.pair, trade.amount)
except InvalidOrderException:
logger.exception(
f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
f"Could not cancel {trade.exit_side} order {order_id}")
return False
# Set exit_reason for fill message
@@ -1548,14 +1559,12 @@ class FreqtradeBot(LoggingMixin):
# Order might be filled above in odd timing issues.
if order.get('status') in ('canceled', 'cancelled'):
trade.exit_reason = None
trade.open_order_id = None
else:
trade.exit_reason = exit_reason_prev
cancelled = True
else:
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
trade.exit_reason = None
trade.open_order_id = None
self.update_trade_state(trade, order['id'], order)

View File

@@ -1329,14 +1329,44 @@ class Trade(ModelBase, LocalTrade):
def open_orders(self):
return [order for order in self.orders if order.ft_is_open]
@open_orders.expression
def open_orders(cls):
return (
select(Order).where(Order.ft_is_open is True)
.where(
Order.order_id.in_(
select(Order.order_id)
.where(Order.ft_trade_id == cls.id)
)
)
)
@hybrid_property
def open_orders_count(self) -> int:
return len(self.open_orders)
@open_orders_count.expression
def open_orders_count(cls):
return (
select(func.count(Order.order_id))
.where(Order.ft_is_open is True)
.where(Order.ft_trade_id == cls.id)
.subquery()
)
@hybrid_property
def open_orders_ids(self) -> list:
return [open_order.order_id for open_order in self.open_orders]
@open_orders_ids.expression
def open_orders_ids(cls):
return (
select(Order.order_id)
.where(Order.ft_is_open is True)
.where(Order.ft_trade_id == cls.id)
.subquery()
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.realized_profit = 0
@@ -1440,7 +1470,7 @@ class Trade(ModelBase, LocalTrade):
Returns all open trades
NOTE: Not supported in Backtesting.
"""
return cast(List[Trade], Trade.get_trades(Trade.open_orders_count.isnot(0)).all())
return cast(List[Trade], Trade.get_trades([Trade.open_orders_count != 0]).all())
@staticmethod
def get_open_trades_without_assigned_fees():

View File

@@ -2592,7 +2592,6 @@ def open_trade():
pair='ETH/BTC',
open_rate=0.00001099,
exchange='binance',
open_order_id='123456789',
amount=90.99181073,
fee_open=0.0,
fee_close=0.0,
@@ -2604,7 +2603,7 @@ def open_trade():
Order(
ft_order_side='buy',
ft_pair=trade.pair,
ft_is_open=False,
ft_is_open=True,
ft_amount=trade.amount,
ft_price=trade.open_rate,
order_id='123456789',

View File

@@ -3143,7 +3143,8 @@ def test_manage_open_orders_partial(
open_trade.is_short = is_short
open_trade.leverage = leverage
open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy'
limit_buy_order_old_partial['id'] = open_trade.open_order_id
# limit_buy_order_old_partial['id'] = open_trade.open_order_id
limit_buy_order_old_partial['id'] = open_trade.orders[0].order_id
limit_buy_order_old_partial['side'] = 'sell' if is_short else 'buy'
limit_buy_canceled = deepcopy(limit_buy_order_old_partial)
limit_buy_canceled['status'] = 'canceled'
@@ -3167,7 +3168,8 @@ def test_manage_open_orders_partial(
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 3
trades = Trade.session.scalars(
select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
select(Trade).filter(Trade.open_orders_count != 0)
).all()
assert len(trades) == 1
assert trades[0].amount == 23.0
assert trades[0].stake_amount == open_trade.open_rate * trades[0].amount / leverage