fix first important tests in test_freqtradebot, update and fix on order related Trade class hybrid_properties
This commit is contained in:
+43
-34
@@ -373,7 +373,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
"Order is older than 5 days. Assuming order was fully cancelled.")
|
"Order is older than 5 days. Assuming order was fully cancelled.")
|
||||||
fo = order.to_ccxt_object()
|
fo = order.to_ccxt_object()
|
||||||
fo['status'] = 'canceled'
|
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:
|
except ExchangeError as e:
|
||||||
|
|
||||||
@@ -1318,26 +1321,33 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
for trade in Trade.get_open_order_trades():
|
for trade in Trade.get_open_order_trades():
|
||||||
try:
|
for open_order in trade.open_orders:
|
||||||
if not trade.open_order_id:
|
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
|
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)
|
fully_cancelled = self.update_trade_state(trade, open_order.order_id, order)
|
||||||
not_closed = order['status'] == 'open' or fully_cancelled
|
not_closed = order['status'] == 'open' or fully_cancelled
|
||||||
order_obj = trade.select_order_by_order_id(trade.open_order_id)
|
order_obj = trade.select_order_by_order_id(open_order.order_id)
|
||||||
|
|
||||||
if not_closed:
|
if not_closed:
|
||||||
if fully_cancelled or (order_obj and self.strategy.ft_check_timed_out(
|
if fully_cancelled or (
|
||||||
trade, order_obj, datetime.now(timezone.utc))):
|
order_obj and self.strategy.ft_check_timed_out(
|
||||||
self.handle_cancel_order(order, trade, constants.CANCEL_REASON['TIMEOUT'])
|
trade, order_obj, datetime.now(timezone.utc)
|
||||||
else:
|
)
|
||||||
self.replace_order(order, order_obj, trade)
|
):
|
||||||
|
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.
|
Check if current analyzed order timed out and cancel if necessary.
|
||||||
:param order: Order dict grabbed with exchange.fetch_order()
|
:param order: Order dict grabbed with exchange.fetch_order()
|
||||||
@@ -1345,7 +1355,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
if order['side'] == trade.entry_side:
|
if order['side'] == trade.entry_side:
|
||||||
self.handle_cancel_enter(trade, order, reason)
|
self.handle_cancel_enter(trade, order, order_id, reason)
|
||||||
else:
|
else:
|
||||||
canceled = self.handle_cancel_exit(trade, order, reason)
|
canceled = self.handle_cancel_exit(trade, order, reason)
|
||||||
canceled_count = trade.get_exit_order_count()
|
canceled_count = trade.get_exit_order_count()
|
||||||
@@ -1399,7 +1409,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
cancel_reason = constants.CANCEL_REASON['USER_CANCEL']
|
cancel_reason = constants.CANCEL_REASON['USER_CANCEL']
|
||||||
if order_obj.price != adjusted_entry_price:
|
if order_obj.price != adjusted_entry_price:
|
||||||
# cancel existing order if new price is supplied or None
|
# 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)
|
replacing=replacing)
|
||||||
if adjusted_entry_price:
|
if adjusted_entry_price:
|
||||||
# place new order only if new price is supplied
|
# place new order only if new price is supplied
|
||||||
@@ -1436,8 +1446,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
Trade.commit()
|
Trade.commit()
|
||||||
|
|
||||||
def handle_cancel_enter(
|
def handle_cancel_enter(
|
||||||
self, trade: Trade, order: Dict, reason: str,
|
self, trade: Trade, order: Dict, order_id: str,
|
||||||
replacing: Optional[bool] = False
|
reason: str, replacing: Optional[bool] = False
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
entry cancel - cancel order
|
entry cancel - cancel order
|
||||||
@@ -1446,7 +1456,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
"""
|
"""
|
||||||
was_trade_fully_canceled = False
|
was_trade_fully_canceled = False
|
||||||
side = trade.entry_side.capitalize()
|
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}.")
|
logger.warning(f"No open order for {trade}.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -1459,16 +1469,16 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
if filled_val > 0 and minstake and filled_stake < minstake:
|
if filled_val > 0 and minstake and filled_stake < minstake:
|
||||||
logger.warning(
|
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.")
|
f"as the filled amount of {filled_val} would result in an unexitable trade.")
|
||||||
return False
|
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)
|
trade.amount)
|
||||||
# Avoid race condition where the order could not be cancelled coz its already filled.
|
# 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
|
# Simply bailing here is the only safe way - as this order will then be
|
||||||
# handled in the next iteration.
|
# handled in the next iteration.
|
||||||
if corder.get('status') not in constants.NON_OPEN_EXCHANGE_STATES:
|
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
|
return False
|
||||||
else:
|
else:
|
||||||
# Order was cancelled already, so we can reuse the existing dict
|
# Order was cancelled already, so we can reuse the existing dict
|
||||||
@@ -1488,14 +1498,12 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
was_trade_fully_canceled = True
|
was_trade_fully_canceled = True
|
||||||
reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}"
|
reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}"
|
||||||
else:
|
else:
|
||||||
self.update_trade_state(trade, trade.open_order_id, corder)
|
self.update_trade_state(trade, order_id, corder)
|
||||||
trade.open_order_id = None
|
|
||||||
logger.info(f'{side} Order timeout for {trade}.')
|
logger.info(f'{side} Order timeout for {trade}.')
|
||||||
else:
|
else:
|
||||||
# update_trade_state (and subsequently recalc_trade_from_orders) will handle updates
|
# update_trade_state (and subsequently recalc_trade_from_orders) will handle updates
|
||||||
# to the trade object
|
# to the trade object
|
||||||
self.update_trade_state(trade, trade.open_order_id, corder)
|
self.update_trade_state(trade, order_id, corder)
|
||||||
trade.open_order_id = None
|
|
||||||
|
|
||||||
logger.info(f'Partial {trade.entry_side} order timeout for {trade}.')
|
logger.info(f'Partial {trade.entry_side} order timeout for {trade}.')
|
||||||
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
|
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
|
||||||
@@ -1505,7 +1513,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
reason=reason)
|
reason=reason)
|
||||||
return was_trade_fully_canceled
|
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
|
exit order cancel - cancel order and update trade
|
||||||
:return: True if exit order was cancelled, false otherwise
|
:return: True if exit order was cancelled, false otherwise
|
||||||
@@ -1522,7 +1533,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
reason = constants.CANCEL_REASON['PARTIALLY_FILLED']
|
reason = constants.CANCEL_REASON['PARTIALLY_FILLED']
|
||||||
if minstake and filled_rem_stake < minstake:
|
if minstake and filled_rem_stake < minstake:
|
||||||
logger.warning(
|
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.")
|
f"the filled amount of {filled_val} would result in an unexitable trade.")
|
||||||
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||||
|
|
||||||
@@ -1539,7 +1550,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
order['id'], trade.pair, trade.amount)
|
order['id'], trade.pair, trade.amount)
|
||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.exception(
|
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
|
return False
|
||||||
|
|
||||||
# Set exit_reason for fill message
|
# Set exit_reason for fill message
|
||||||
@@ -1548,14 +1559,12 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Order might be filled above in odd timing issues.
|
# Order might be filled above in odd timing issues.
|
||||||
if order.get('status') in ('canceled', 'cancelled'):
|
if order.get('status') in ('canceled', 'cancelled'):
|
||||||
trade.exit_reason = None
|
trade.exit_reason = None
|
||||||
trade.open_order_id = None
|
|
||||||
else:
|
else:
|
||||||
trade.exit_reason = exit_reason_prev
|
trade.exit_reason = exit_reason_prev
|
||||||
cancelled = True
|
cancelled = True
|
||||||
else:
|
else:
|
||||||
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
||||||
trade.exit_reason = None
|
trade.exit_reason = None
|
||||||
trade.open_order_id = None
|
|
||||||
|
|
||||||
self.update_trade_state(trade, order['id'], order)
|
self.update_trade_state(trade, order['id'], order)
|
||||||
|
|
||||||
|
|||||||
@@ -1329,14 +1329,44 @@ class Trade(ModelBase, LocalTrade):
|
|||||||
def open_orders(self):
|
def open_orders(self):
|
||||||
return [order for order in self.orders if order.ft_is_open]
|
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
|
@hybrid_property
|
||||||
def open_orders_count(self) -> int:
|
def open_orders_count(self) -> int:
|
||||||
return len(self.open_orders)
|
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
|
@hybrid_property
|
||||||
def open_orders_ids(self) -> list:
|
def open_orders_ids(self) -> list:
|
||||||
return [open_order.order_id for open_order in self.open_orders]
|
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):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.realized_profit = 0
|
self.realized_profit = 0
|
||||||
@@ -1440,7 +1470,7 @@ class Trade(ModelBase, LocalTrade):
|
|||||||
Returns all open trades
|
Returns all open trades
|
||||||
NOTE: Not supported in Backtesting.
|
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
|
@staticmethod
|
||||||
def get_open_trades_without_assigned_fees():
|
def get_open_trades_without_assigned_fees():
|
||||||
|
|||||||
+1
-2
@@ -2592,7 +2592,6 @@ def open_trade():
|
|||||||
pair='ETH/BTC',
|
pair='ETH/BTC',
|
||||||
open_rate=0.00001099,
|
open_rate=0.00001099,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
open_order_id='123456789',
|
|
||||||
amount=90.99181073,
|
amount=90.99181073,
|
||||||
fee_open=0.0,
|
fee_open=0.0,
|
||||||
fee_close=0.0,
|
fee_close=0.0,
|
||||||
@@ -2604,7 +2603,7 @@ def open_trade():
|
|||||||
Order(
|
Order(
|
||||||
ft_order_side='buy',
|
ft_order_side='buy',
|
||||||
ft_pair=trade.pair,
|
ft_pair=trade.pair,
|
||||||
ft_is_open=False,
|
ft_is_open=True,
|
||||||
ft_amount=trade.amount,
|
ft_amount=trade.amount,
|
||||||
ft_price=trade.open_rate,
|
ft_price=trade.open_rate,
|
||||||
order_id='123456789',
|
order_id='123456789',
|
||||||
|
|||||||
@@ -3143,7 +3143,8 @@ def test_manage_open_orders_partial(
|
|||||||
open_trade.is_short = is_short
|
open_trade.is_short = is_short
|
||||||
open_trade.leverage = leverage
|
open_trade.leverage = leverage
|
||||||
open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy'
|
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_order_old_partial['side'] = 'sell' if is_short else 'buy'
|
||||||
limit_buy_canceled = deepcopy(limit_buy_order_old_partial)
|
limit_buy_canceled = deepcopy(limit_buy_order_old_partial)
|
||||||
limit_buy_canceled['status'] = 'canceled'
|
limit_buy_canceled['status'] = 'canceled'
|
||||||
@@ -3167,7 +3168,8 @@ def test_manage_open_orders_partial(
|
|||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
assert rpc_mock.call_count == 3
|
assert rpc_mock.call_count == 3
|
||||||
trades = Trade.session.scalars(
|
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 len(trades) == 1
|
||||||
assert trades[0].amount == 23.0
|
assert trades[0].amount == 23.0
|
||||||
assert trades[0].stake_amount == open_trade.open_rate * trades[0].amount / leverage
|
assert trades[0].stake_amount == open_trade.open_rate * trades[0].amount / leverage
|
||||||
|
|||||||
Reference in New Issue
Block a user