mirror of
https://github.com/freqtrade/freqtrade.git
synced 2026-01-20 14:00:38 +00:00
fix first important tests in test_freqtradebot, update and fix on order related Trade class hybrid_properties
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user