mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-11-29 08:33:07 +00:00
Merge pull request #8779 from Axel-CH/feature/multiple_open_orders
Feature: Multiple open orders
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:
|
||||
|
||||
@@ -440,13 +443,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
if fo and fo['status'] == 'open':
|
||||
# Assume this as the open stoploss order
|
||||
trade.stoploss_order_id = order.order_id
|
||||
elif order.ft_order_side == trade.exit_side:
|
||||
if fo and fo['status'] == 'open':
|
||||
# Assume this as the open order
|
||||
trade.open_order_id = order.order_id
|
||||
elif order.ft_order_side == trade.entry_side:
|
||||
if fo and fo['status'] == 'open':
|
||||
trade.open_order_id = order.order_id
|
||||
if fo:
|
||||
logger.info(f"Found {order} for trade {trade}.")
|
||||
self.update_trade_state(trade, order.order_id, fo,
|
||||
@@ -612,7 +608,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
# Walk through each pair and check if it needs changes
|
||||
for trade in Trade.get_open_trades():
|
||||
# If there is any open orders, wait for them to finish.
|
||||
if trade.open_order_id is None:
|
||||
# TODO Remove to allow mul open orders
|
||||
if not trade.has_open_orders:
|
||||
# Do a wallets update (will be ratelimited to once per hour)
|
||||
self.wallets.update(False)
|
||||
try:
|
||||
@@ -846,7 +843,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
open_rate_requested=enter_limit_requested,
|
||||
open_date=open_date,
|
||||
exchange=self.exchange.id,
|
||||
open_order_id=order_id,
|
||||
strategy=self.strategy.get_strategy_name(),
|
||||
enter_tag=enter_tag,
|
||||
timeframe=timeframe_to_minutes(self.config['timeframe']),
|
||||
@@ -867,7 +863,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade.is_open = True
|
||||
trade.fee_open_currency = None
|
||||
trade.open_rate_requested = enter_limit_requested
|
||||
trade.open_order_id = order_id
|
||||
|
||||
trade.orders.append(order_obj)
|
||||
trade.recalc_trade_from_orders()
|
||||
@@ -1077,7 +1072,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
trades_closed = 0
|
||||
for trade in trades:
|
||||
|
||||
if trade.open_order_id is None and not self.wallets.check_exit_amount(trade):
|
||||
if not trade.has_open_orders and not self.wallets.check_exit_amount(trade):
|
||||
logger.warning(
|
||||
f'Not enough {trade.safe_base_currency} in wallet to exit {trade}. '
|
||||
'Trying to recover.')
|
||||
@@ -1095,7 +1090,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
logger.warning(
|
||||
f'Unable to handle stoploss on exchange for {trade.pair}: {exception}')
|
||||
# Check if we can sell our current pair
|
||||
if trade.open_order_id is None and trade.is_open and self.handle_trade(trade):
|
||||
if not trade.has_open_orders and trade.is_open and self.handle_trade(trade):
|
||||
trades_closed += 1
|
||||
|
||||
except DependencyException as exception:
|
||||
@@ -1214,7 +1209,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
"""
|
||||
|
||||
logger.debug('Handling stoploss on exchange %s ...', trade)
|
||||
|
||||
stoploss_order = None
|
||||
|
||||
try:
|
||||
@@ -1237,7 +1231,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.handle_protections(trade.pair, trade.trade_direction)
|
||||
return True
|
||||
|
||||
if trade.open_order_id or not trade.is_open:
|
||||
if trade.has_open_orders or not trade.is_open:
|
||||
# Trade has an open Buy or Sell order, Stoploss-handling can't happen in this case
|
||||
# as the Amount on the exchange is tied up in another trade.
|
||||
# The trade can be closed already (sell-order fill confirmation came in this iteration)
|
||||
@@ -1321,27 +1315,33 @@ class FreqtradeBot(LoggingMixin):
|
||||
Timeout setting takes priority over limit order adjustment request.
|
||||
:return: None
|
||||
"""
|
||||
for trade in Trade.get_open_order_trades():
|
||||
try:
|
||||
if not trade.open_order_id:
|
||||
for trade in Trade.get_open_trades():
|
||||
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
|
||||
|
||||
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 (
|
||||
open_order and self.strategy.ft_check_timed_out(
|
||||
trade, open_order, datetime.now(timezone.utc)
|
||||
)
|
||||
):
|
||||
self.handle_cancel_order(
|
||||
order, open_order.order_id, trade, constants.CANCEL_REASON['TIMEOUT']
|
||||
)
|
||||
else:
|
||||
self.replace_order(order, open_order, 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()
|
||||
@@ -1349,9 +1349,9 @@ 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 = self.handle_cancel_exit(trade, order, order_id, reason)
|
||||
canceled_count = trade.get_exit_order_count()
|
||||
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
||||
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
|
||||
@@ -1406,7 +1406,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
|
||||
@@ -1434,25 +1434,28 @@ class FreqtradeBot(LoggingMixin):
|
||||
:return: None
|
||||
"""
|
||||
|
||||
for trade in Trade.get_open_order_trades():
|
||||
if not trade.open_order_id:
|
||||
continue
|
||||
try:
|
||||
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
|
||||
for trade in Trade.get_open_trades():
|
||||
for open_order in trade.open_orders:
|
||||
try:
|
||||
order = self.exchange.fetch_order(open_order.order_id, trade.pair)
|
||||
except (ExchangeError):
|
||||
logger.info("Can't query order for %s due to %s", trade, traceback.format_exc())
|
||||
continue
|
||||
|
||||
if order['side'] == trade.entry_side:
|
||||
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
||||
if order['side'] == trade.entry_side:
|
||||
self.handle_cancel_enter(
|
||||
trade, order, open_order.order_id, constants.CANCEL_REASON['ALL_CANCELLED']
|
||||
)
|
||||
|
||||
elif order['side'] == trade.exit_side:
|
||||
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
||||
elif order['side'] == trade.exit_side:
|
||||
self.handle_cancel_exit(
|
||||
trade, order, open_order.order_id, constants.CANCEL_REASON['ALL_CANCELLED']
|
||||
)
|
||||
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
|
||||
@@ -1461,7 +1464,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
"""
|
||||
was_trade_fully_canceled = False
|
||||
side = trade.entry_side.capitalize()
|
||||
if not trade.open_order_id:
|
||||
if not trade.has_open_orders:
|
||||
logger.warning(f"No open order for {trade}.")
|
||||
return False
|
||||
|
||||
@@ -1474,16 +1477,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
|
||||
@@ -1503,14 +1506,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']}"
|
||||
@@ -1520,7 +1521,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
|
||||
@@ -1538,7 +1542,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_amt} would result in an unexitable trade.")
|
||||
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||
|
||||
@@ -1555,7 +1559,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
|
||||
@@ -1564,14 +1568,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)
|
||||
|
||||
@@ -1705,7 +1707,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side, amount, limit)
|
||||
trade.orders.append(order_obj)
|
||||
|
||||
trade.open_order_id = order['id']
|
||||
trade.exit_order_status = ''
|
||||
trade.close_rate_requested = limit
|
||||
trade.exit_reason = exit_reason
|
||||
@@ -1713,7 +1714,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
self._notify_exit(trade, order_type, sub_trade=bool(sub_trade_amt), order=order_obj)
|
||||
# In case of market sell orders the order can be closed immediately
|
||||
if order.get('status', 'unknown') in ('closed', 'expired'):
|
||||
self.update_trade_state(trade, trade.open_order_id, order)
|
||||
self.update_trade_state(trade, order_obj.order_id, order)
|
||||
Trade.commit()
|
||||
|
||||
return True
|
||||
@@ -1927,11 +1928,11 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade.amount, abs_tol=constants.MATH_CLOSE_PREC)
|
||||
if order.ft_order_side == trade.exit_side:
|
||||
# Exit notification
|
||||
if send_msg and not stoploss_order and not trade.open_order_id:
|
||||
if send_msg and not stoploss_order and order.order_id not in trade.open_orders_ids:
|
||||
self._notify_exit(trade, '', fill=True, sub_trade=sub_trade, order=order)
|
||||
if not trade.is_open:
|
||||
self.handle_protections(trade.pair, trade.trade_direction)
|
||||
elif send_msg and not trade.open_order_id and not stoploss_order:
|
||||
elif send_msg and order.order_id not in trade.open_orders_ids and not stoploss_order:
|
||||
# Enter fill
|
||||
self._notify_enter(trade, order, order.order_type, fill=True, sub_trade=sub_trade)
|
||||
|
||||
|
||||
@@ -593,7 +593,6 @@ class Backtesting:
|
||||
"""
|
||||
if order and self._get_order_filled(order.ft_price, row):
|
||||
order.close_bt_order(current_date, trade)
|
||||
trade.open_order_id = None
|
||||
if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount):
|
||||
self._call_adjust_stop(current_date, trade, order.ft_price)
|
||||
# pass
|
||||
@@ -862,7 +861,6 @@ class Backtesting:
|
||||
self.trade_id_counter += 1
|
||||
trade = LocalTrade(
|
||||
id=self.trade_id_counter,
|
||||
open_order_id=self.order_id_counter,
|
||||
pair=pair,
|
||||
base_currency=base_currency,
|
||||
stake_currency=self.config['stake_currency'],
|
||||
@@ -924,8 +922,7 @@ class Backtesting:
|
||||
)
|
||||
order._trade_bt = trade
|
||||
trade.orders.append(order)
|
||||
if not self._try_close_open_order(order, trade, current_time, row):
|
||||
trade.open_order_id = str(self.order_id_counter)
|
||||
self._try_close_open_order(order, trade, current_time, row)
|
||||
trade.recalc_trade_from_orders()
|
||||
|
||||
return trade
|
||||
@@ -937,7 +934,7 @@ class Backtesting:
|
||||
"""
|
||||
for pair in open_trades.keys():
|
||||
for trade in list(open_trades[pair]):
|
||||
if trade.open_order_id and trade.nr_of_successful_entries == 0:
|
||||
if trade.has_open_orders and trade.nr_of_successful_entries == 0:
|
||||
# Ignore trade if entry-order did not fill yet
|
||||
continue
|
||||
exit_row = data[pair][-1]
|
||||
@@ -1014,13 +1011,11 @@ class Backtesting:
|
||||
else:
|
||||
# Close additional entry order
|
||||
del trade.orders[trade.orders.index(order)]
|
||||
trade.open_order_id = None
|
||||
return False
|
||||
if order.side == trade.exit_side:
|
||||
self.timedout_exit_orders += 1
|
||||
# Close exit order and retry exiting on next signal.
|
||||
del trade.orders[trade.orders.index(order)]
|
||||
trade.open_order_id = None
|
||||
return False
|
||||
return None
|
||||
|
||||
@@ -1048,7 +1043,6 @@ class Backtesting:
|
||||
return False
|
||||
else:
|
||||
del trade.orders[trade.orders.index(order)]
|
||||
trade.open_order_id = None
|
||||
self.canceled_entry_orders += 1
|
||||
|
||||
# place new order if result was not None
|
||||
@@ -1059,7 +1053,7 @@ class Backtesting:
|
||||
order.safe_remaining * order.ft_price / trade.leverage),
|
||||
direction='short' if trade.is_short else 'long')
|
||||
# Delete trade if no successful entries happened (if placing the new order failed)
|
||||
if trade.open_order_id is None and trade.nr_of_successful_entries == 0:
|
||||
if not trade.has_open_orders and trade.nr_of_successful_entries == 0:
|
||||
return True
|
||||
self.replaced_entry_orders += 1
|
||||
else:
|
||||
@@ -1144,7 +1138,7 @@ class Backtesting:
|
||||
self.wallets.update()
|
||||
|
||||
# 4. Create exit orders (if any)
|
||||
if not trade.open_order_id:
|
||||
if not trade.has_open_orders:
|
||||
self._check_trade_exit(trade, row) # Place exit order if necessary
|
||||
|
||||
# 5. Process exit orders.
|
||||
|
||||
@@ -157,7 +157,7 @@ def migrate_trades_and_orders_table(
|
||||
fee_open, fee_open_cost, fee_open_currency,
|
||||
fee_close, fee_close_cost, fee_close_currency, open_rate,
|
||||
open_rate_requested, close_rate, close_rate_requested, close_profit,
|
||||
stake_amount, amount, amount_requested, open_date, close_date, open_order_id,
|
||||
stake_amount, amount, amount_requested, open_date, close_date,
|
||||
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
|
||||
is_stop_loss_trailing, stoploss_order_id, stoploss_last_update,
|
||||
max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag,
|
||||
@@ -174,7 +174,7 @@ def migrate_trades_and_orders_table(
|
||||
{fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency,
|
||||
open_rate, {open_rate_requested} open_rate_requested, close_rate,
|
||||
{close_rate_requested} close_rate_requested, close_profit,
|
||||
stake_amount, amount, {amount_requested}, open_date, close_date, open_order_id,
|
||||
stake_amount, amount, {amount_requested}, open_date, close_date,
|
||||
{stop_loss} stop_loss, {stop_loss_pct} stop_loss_pct,
|
||||
{initial_stop_loss} initial_stop_loss,
|
||||
{initial_stop_loss_pct} initial_stop_loss_pct,
|
||||
@@ -272,6 +272,13 @@ def set_sqlite_to_wal(engine):
|
||||
|
||||
def fix_old_dry_orders(engine):
|
||||
with engine.begin() as connection:
|
||||
|
||||
# Update current dry-run Orders where
|
||||
# - current Order is open
|
||||
# - current Trade is closed
|
||||
# - current Order trade_id not equal to current Trade.id
|
||||
# - current Order not stoploss
|
||||
|
||||
stmt = update(Order).where(
|
||||
Order.ft_is_open.is_(True),
|
||||
tuple_(Order.ft_trade_id, Order.order_id).not_in(
|
||||
@@ -285,12 +292,13 @@ def fix_old_dry_orders(engine):
|
||||
).values(ft_is_open=False)
|
||||
connection.execute(stmt)
|
||||
|
||||
# Close dry-run orders for closed trades.
|
||||
stmt = update(Order).where(
|
||||
Order.ft_is_open.is_(True),
|
||||
tuple_(Order.ft_trade_id, Order.order_id).not_in(
|
||||
Order.ft_trade_id.not_in(
|
||||
select(
|
||||
Trade.id, Trade.open_order_id
|
||||
).where(Trade.open_order_id.is_not(None))
|
||||
Trade.id
|
||||
).where(Trade.is_open.is_(True))
|
||||
),
|
||||
Order.ft_order_side != 'stoploss',
|
||||
Order.order_id.like('dry%')
|
||||
|
||||
@@ -352,7 +352,6 @@ class LocalTrade:
|
||||
amount_requested: Optional[float] = None
|
||||
open_date: datetime
|
||||
close_date: Optional[datetime] = None
|
||||
open_order_id: Optional[str] = None
|
||||
# absolute value of the stop loss
|
||||
stop_loss: float = 0.0
|
||||
# percentage value of the stop loss
|
||||
@@ -494,6 +493,32 @@ class LocalTrade:
|
||||
except IndexError:
|
||||
return ''
|
||||
|
||||
@property
|
||||
def open_orders(self) -> List[Order]:
|
||||
"""
|
||||
All open orders for this trade excluding stoploss orders
|
||||
"""
|
||||
return [o for o in self.orders if o.ft_is_open and o.ft_order_side != 'stoploss']
|
||||
|
||||
@property
|
||||
def has_open_orders(self) -> int:
|
||||
"""
|
||||
True if there are open orders for this trade excluding stoploss orders
|
||||
"""
|
||||
open_orders_wo_sl = [
|
||||
o for o in self.orders
|
||||
if o.ft_order_side not in ['stoploss'] and o.ft_is_open
|
||||
]
|
||||
return len(open_orders_wo_sl) > 0
|
||||
|
||||
@property
|
||||
def open_orders_ids(self) -> List[str]:
|
||||
open_orders_ids_wo_sl = [
|
||||
oo.order_id for oo in self.open_orders
|
||||
if oo.ft_order_side not in ['stoploss']
|
||||
]
|
||||
return open_orders_ids_wo_sl
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for key in kwargs:
|
||||
setattr(self, key, kwargs[key])
|
||||
@@ -512,8 +537,8 @@ class LocalTrade:
|
||||
)
|
||||
|
||||
def to_json(self, minified: bool = False) -> Dict[str, Any]:
|
||||
filled_orders = self.select_filled_or_open_orders()
|
||||
orders = [order.to_json(self.entry_side, minified) for order in filled_orders]
|
||||
filled_or_open_orders = self.select_filled_or_open_orders()
|
||||
orders_json = [order.to_json(self.entry_side, minified) for order in filled_or_open_orders]
|
||||
|
||||
return {
|
||||
'trade_id': self.id,
|
||||
@@ -589,11 +614,11 @@ class LocalTrade:
|
||||
'is_short': self.is_short,
|
||||
'trading_mode': self.trading_mode,
|
||||
'funding_fees': self.funding_fees,
|
||||
'open_order_id': self.open_order_id,
|
||||
'amount_precision': self.amount_precision,
|
||||
'price_precision': self.price_precision,
|
||||
'precision_mode': self.precision_mode,
|
||||
'orders': orders,
|
||||
'orders': orders_json,
|
||||
'has_open_orders': self.has_open_orders,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -711,24 +736,13 @@ class LocalTrade:
|
||||
if self.is_open:
|
||||
payment = "SELL" if self.is_short else "BUY"
|
||||
logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.')
|
||||
# condition to avoid reset value when updating fees
|
||||
if self.open_order_id == order.order_id:
|
||||
self.open_order_id = None
|
||||
else:
|
||||
logger.warning(
|
||||
f'Got different open_order_id {self.open_order_id} != {order.order_id}')
|
||||
|
||||
self.recalc_trade_from_orders()
|
||||
elif order.ft_order_side == self.exit_side:
|
||||
if self.is_open:
|
||||
payment = "BUY" if self.is_short else "SELL"
|
||||
# * On margin shorts, you buy a little bit more than the amount (amount + interest)
|
||||
logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.')
|
||||
# condition to avoid reset value when updating fees
|
||||
if self.open_order_id == order.order_id:
|
||||
self.open_order_id = None
|
||||
else:
|
||||
logger.warning(
|
||||
f'Got different open_order_id {self.open_order_id} != {order.order_id}')
|
||||
|
||||
elif order.ft_order_side == 'stoploss' and order.status not in ('open', ):
|
||||
self.stoploss_order_id = None
|
||||
@@ -761,7 +775,6 @@ class LocalTrade:
|
||||
self.close_date = self.close_date or datetime.utcnow()
|
||||
self.is_open = False
|
||||
self.exit_order_status = 'closed'
|
||||
self.open_order_id = None
|
||||
self.recalc_trade_from_orders(is_closing=True)
|
||||
if show_msg:
|
||||
logger.info(f"Marking {self} as closed as the trade is fulfilled "
|
||||
@@ -1309,7 +1322,6 @@ class Trade(ModelBase, LocalTrade):
|
||||
open_date: Mapped[datetime] = mapped_column(
|
||||
nullable=False, default=datetime.utcnow) # type: ignore
|
||||
close_date: Mapped[Optional[datetime]] = mapped_column() # type: ignore
|
||||
open_order_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) # type: ignore
|
||||
# absolute value of the stop loss
|
||||
stop_loss: Mapped[float] = mapped_column(Float(), nullable=True, default=0.0) # type: ignore
|
||||
# percentage value of the stop loss
|
||||
@@ -1466,14 +1478,6 @@ class Trade(ModelBase, LocalTrade):
|
||||
# raise an exception.
|
||||
return Trade.session.scalars(query)
|
||||
|
||||
@staticmethod
|
||||
def get_open_order_trades() -> List['Trade']:
|
||||
"""
|
||||
Returns all open trades
|
||||
NOTE: Not supported in Backtesting.
|
||||
"""
|
||||
return cast(List[Trade], Trade.get_trades(Trade.open_order_id.isnot(None)).all())
|
||||
|
||||
@staticmethod
|
||||
def get_open_trades_without_assigned_fees():
|
||||
"""
|
||||
@@ -1772,7 +1776,6 @@ class Trade(ModelBase, LocalTrade):
|
||||
is_short=data["is_short"],
|
||||
trading_mode=data["trading_mode"],
|
||||
funding_fees=data["funding_fees"],
|
||||
open_order_id=data["open_order_id"],
|
||||
)
|
||||
for order in data["orders"]:
|
||||
|
||||
|
||||
@@ -308,7 +308,7 @@ class TradeSchema(BaseModel):
|
||||
|
||||
min_rate: Optional[float] = None
|
||||
max_rate: Optional[float] = None
|
||||
open_order_id: Optional[str] = None
|
||||
has_open_orders: bool
|
||||
orders: List[OrderSchema]
|
||||
|
||||
leverage: Optional[float] = None
|
||||
@@ -333,8 +333,6 @@ class OpenTradeSchema(TradeSchema):
|
||||
total_profit_fiat: Optional[float] = None
|
||||
total_profit_ratio: Optional[float] = None
|
||||
|
||||
open_order: Optional[str] = None
|
||||
|
||||
|
||||
class TradeResponse(BaseModel):
|
||||
trades: List[TradeSchema]
|
||||
|
||||
@@ -26,7 +26,7 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.loggers import bufferHandler
|
||||
from freqtrade.misc import decimals_per_coin
|
||||
from freqtrade.persistence import KeyStoreKeys, KeyValueStore, Order, PairLocks, Trade
|
||||
from freqtrade.persistence import KeyStoreKeys, KeyValueStore, PairLocks, Trade
|
||||
from freqtrade.persistence.models import PairLock
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||
@@ -171,13 +171,20 @@ class RPC:
|
||||
else:
|
||||
results = []
|
||||
for trade in trades:
|
||||
order: Optional[Order] = None
|
||||
current_profit_fiat: Optional[float] = None
|
||||
total_profit_fiat: Optional[float] = None
|
||||
|
||||
# prepare open orders details
|
||||
oo_details: Optional[str] = ""
|
||||
oo_details_lst = [
|
||||
f'({oo.order_type} {oo.side} rem={oo.safe_remaining:.8f})'
|
||||
for oo in trade.open_orders
|
||||
if oo.ft_order_side not in ['stoploss']
|
||||
]
|
||||
oo_details = ', '.join(oo_details_lst)
|
||||
|
||||
total_profit_abs = 0.0
|
||||
total_profit_ratio: Optional[float] = None
|
||||
if trade.open_order_id:
|
||||
order = trade.select_order_by_order_id(trade.open_order_id)
|
||||
# calculate profit and send message to user
|
||||
if trade.is_open:
|
||||
try:
|
||||
@@ -234,7 +241,6 @@ class RPC:
|
||||
profit_pct=round(current_profit * 100, 2),
|
||||
profit_abs=current_profit_abs,
|
||||
profit_fiat=current_profit_fiat,
|
||||
|
||||
total_profit_abs=total_profit_abs,
|
||||
total_profit_fiat=total_profit_fiat,
|
||||
total_profit_ratio=total_profit_ratio,
|
||||
@@ -243,10 +249,7 @@ class RPC:
|
||||
stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2),
|
||||
stoploss_entry_dist=stoploss_entry_dist,
|
||||
stoploss_entry_dist_ratio=round(stoploss_entry_dist_ratio, 8),
|
||||
open_order=(
|
||||
f'({order.order_type} {order.side} rem={order.safe_remaining:.8f})' if
|
||||
order else None
|
||||
),
|
||||
open_orders=oo_details
|
||||
))
|
||||
results.append(trade_dict)
|
||||
return results
|
||||
@@ -288,18 +291,22 @@ class RPC:
|
||||
profit_str += f" ({fiat_profit:.2f})"
|
||||
fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) \
|
||||
else fiat_profit_sum + fiat_profit
|
||||
open_order = (trade.select_order_by_order_id(
|
||||
trade.open_order_id) if trade.open_order_id else None)
|
||||
|
||||
active_attempt_side_symbols = [
|
||||
'*' if (oo and oo.ft_order_side == trade.entry_side) else '**'
|
||||
for oo in trade.open_orders
|
||||
]
|
||||
|
||||
# exemple: '*.**.**' trying to enter, exit and exit with 3 different orders
|
||||
active_attempt_side_symbols_str = '.'.join(active_attempt_side_symbols)
|
||||
|
||||
detail_trade = [
|
||||
f'{trade.id} {direction_str}',
|
||||
trade.pair + ('*' if (open_order
|
||||
and open_order.ft_order_side == trade.entry_side) else '')
|
||||
+ ('**' if (open_order and
|
||||
open_order.ft_order_side == trade.exit_side is not None) else ''),
|
||||
trade.pair + active_attempt_side_symbols_str,
|
||||
shorten_date(dt_humanize(trade.open_date, only_distance=True)),
|
||||
profit_str
|
||||
]
|
||||
|
||||
if self._config.get('position_adjustment_enable', False):
|
||||
max_entry_str = ''
|
||||
if self._config.get('max_entry_position_adjustment', -1) > 0:
|
||||
@@ -780,21 +787,25 @@ class RPC:
|
||||
|
||||
def __exec_force_exit(self, trade: Trade, ordertype: Optional[str],
|
||||
amount: Optional[float] = None) -> bool:
|
||||
# Check if there is there is an open order
|
||||
fully_canceled = False
|
||||
if trade.open_order_id:
|
||||
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
|
||||
# Check if there is there are open orders
|
||||
trade_entry_cancelation_registry = []
|
||||
for oo in trade.open_orders:
|
||||
trade_entry_cancelation_res = {'order_id': oo.order_id, 'cancel_state': False}
|
||||
order = self._freqtrade.exchange.fetch_order(oo.order_id, trade.pair)
|
||||
|
||||
if order['side'] == trade.entry_side:
|
||||
fully_canceled = self._freqtrade.handle_cancel_enter(
|
||||
trade, order, CANCEL_REASON['FORCE_EXIT'])
|
||||
trade, order, oo.order_id, CANCEL_REASON['FORCE_EXIT'])
|
||||
trade_entry_cancelation_res['cancel_state'] = fully_canceled
|
||||
trade_entry_cancelation_registry.append(trade_entry_cancelation_res)
|
||||
|
||||
if order['side'] == trade.exit_side:
|
||||
# Cancel order - so it is placed anew with a fresh price.
|
||||
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_EXIT'])
|
||||
self._freqtrade.handle_cancel_exit(
|
||||
trade, order, oo.order_id, CANCEL_REASON['FORCE_EXIT'])
|
||||
|
||||
if not fully_canceled:
|
||||
if trade.open_order_id is not None:
|
||||
if all(tocr['cancel_state'] is False for tocr in trade_entry_cancelation_registry):
|
||||
if trade.has_open_orders:
|
||||
# Order cancellation failed, so we can't exit.
|
||||
return False
|
||||
# Get current rate and execute sell
|
||||
@@ -893,10 +904,10 @@ class RPC:
|
||||
if trade:
|
||||
is_short = trade.is_short
|
||||
if not self._freqtrade.strategy.position_adjustment_enable:
|
||||
raise RPCException(f'position for {pair} already open - id: {trade.id}')
|
||||
if trade.open_order_id is not None:
|
||||
raise RPCException(f'position for {pair} already open - id: {trade.id} '
|
||||
f'and has open order {trade.open_order_id}')
|
||||
raise RPCException(f"position for {pair} already open - id: {trade.id}")
|
||||
if trade.has_open_orders:
|
||||
raise RPCException(f"position for {pair} already open - id: {trade.id} "
|
||||
f"and has open order {','.join(trade.open_orders_ids)}")
|
||||
else:
|
||||
if Trade.get_open_trade_count() >= self._config['max_open_trades']:
|
||||
raise RPCException("Maximum number of trades is reached.")
|
||||
@@ -933,16 +944,18 @@ class RPC:
|
||||
if not trade:
|
||||
logger.warning('cancel_open_order: Invalid trade_id received.')
|
||||
raise RPCException('Invalid trade_id.')
|
||||
if not trade.open_order_id:
|
||||
if not trade.has_open_orders:
|
||||
logger.warning('cancel_open_order: No open order for trade_id.')
|
||||
raise RPCException('No open order for trade_id.')
|
||||
|
||||
try:
|
||||
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
|
||||
except ExchangeError as e:
|
||||
logger.info(f"Cannot query order for {trade} due to {e}.", exc_info=True)
|
||||
raise RPCException("Order not found.")
|
||||
self._freqtrade.handle_cancel_order(order, trade, CANCEL_REASON['USER_CANCEL'])
|
||||
for open_order in trade.open_orders:
|
||||
try:
|
||||
order = self._freqtrade.exchange.fetch_order(open_order.order_id, trade.pair)
|
||||
except ExchangeError as e:
|
||||
logger.info(f"Cannot query order for {trade} due to {e}.", exc_info=True)
|
||||
raise RPCException("Order not found.")
|
||||
self._freqtrade.handle_cancel_order(
|
||||
order, open_order.order_id, trade, CANCEL_REASON['USER_CANCEL'])
|
||||
Trade.commit()
|
||||
|
||||
def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]:
|
||||
@@ -958,9 +971,9 @@ class RPC:
|
||||
raise RPCException('invalid argument')
|
||||
|
||||
# Try cancelling regular order if that exists
|
||||
if trade.open_order_id:
|
||||
for open_order in trade.open_orders:
|
||||
try:
|
||||
self._freqtrade.exchange.cancel_order(trade.open_order_id, trade.pair)
|
||||
self._freqtrade.exchange.cancel_order(open_order.order_id, trade.pair)
|
||||
c_count += 1
|
||||
except (ExchangeError):
|
||||
pass
|
||||
|
||||
@@ -647,10 +647,10 @@ class Telegram(RPCHandler):
|
||||
("`({stop_loss_ratio:.2%})`" if r['stop_loss_ratio'] else ""))
|
||||
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8g}` "
|
||||
"`({stoploss_current_dist_ratio:.2%})`")
|
||||
if r['open_order']:
|
||||
if r.get('open_orders'):
|
||||
lines.append(
|
||||
"*Open Order:* `{open_order}`"
|
||||
+ "- `{exit_order_status}`" if r['exit_order_status'] else "")
|
||||
"*Open Order:* `{open_orders}`"
|
||||
+ ("- `{exit_order_status}`" if r['exit_order_status'] else ""))
|
||||
|
||||
lines_detail = self._prepare_order_details(
|
||||
r['orders'], r['quote_currency'], r['is_open'])
|
||||
|
||||
@@ -2601,7 +2601,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,
|
||||
@@ -2613,7 +2612,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',
|
||||
@@ -2639,7 +2638,6 @@ def open_trade_usdt():
|
||||
pair='ADA/USDT',
|
||||
open_rate=2.0,
|
||||
exchange='binance',
|
||||
open_order_id='123456789_exit',
|
||||
amount=30.0,
|
||||
fee_open=0.0,
|
||||
fee_close=0.0,
|
||||
|
||||
@@ -46,7 +46,6 @@ def mock_trade_1(fee, is_short: bool):
|
||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17),
|
||||
open_rate=0.123,
|
||||
exchange='binance',
|
||||
open_order_id=f'dry_run_buy_{direc(is_short)}_12345',
|
||||
strategy='StrategyTestV3',
|
||||
timeframe=5,
|
||||
is_short=is_short
|
||||
@@ -210,7 +209,6 @@ def mock_trade_4(fee, is_short: bool):
|
||||
is_open=True,
|
||||
open_rate=0.123,
|
||||
exchange='binance',
|
||||
open_order_id=f'prod_buy_{direc(is_short)}_12345',
|
||||
strategy='StrategyTestV3',
|
||||
timeframe=5,
|
||||
is_short=is_short,
|
||||
@@ -327,7 +325,6 @@ def mock_trade_6(fee, is_short: bool):
|
||||
exchange='binance',
|
||||
strategy='SampleStrategy',
|
||||
enter_tag='TEST2',
|
||||
open_order_id=f"prod_sell_{direc(is_short)}_6",
|
||||
timeframe=5,
|
||||
is_short=is_short
|
||||
)
|
||||
@@ -411,7 +408,6 @@ def short_trade(fee):
|
||||
# close_profit_abs=-0.6925113200000013,
|
||||
exchange='binance',
|
||||
is_open=True,
|
||||
open_order_id=None,
|
||||
strategy='DefaultStrategy',
|
||||
timeframe=5,
|
||||
exit_reason='sell_signal',
|
||||
@@ -502,7 +498,6 @@ def leverage_trade(fee):
|
||||
close_profit_abs=2.5983135000000175,
|
||||
exchange='kraken',
|
||||
is_open=False,
|
||||
open_order_id='dry_run_leverage_buy_12368',
|
||||
strategy='DefaultStrategy',
|
||||
timeframe=5,
|
||||
exit_reason='sell_signal',
|
||||
|
||||
@@ -66,7 +66,6 @@ def mock_trade_usdt_1(fee, is_short: bool):
|
||||
close_profit_abs=-4.09,
|
||||
exchange='binance',
|
||||
strategy='SampleStrategy',
|
||||
open_order_id=f'prod_exit_1_{direc(is_short)}',
|
||||
timeframe=5,
|
||||
is_short=is_short,
|
||||
)
|
||||
@@ -123,7 +122,6 @@ def mock_trade_usdt_2(fee, is_short: bool):
|
||||
close_profit_abs=3.9875,
|
||||
exchange='binance',
|
||||
is_open=False,
|
||||
open_order_id=f'12366_{direc(is_short)}',
|
||||
strategy='StrategyTestV2',
|
||||
timeframe=5,
|
||||
enter_tag='TEST1',
|
||||
@@ -231,7 +229,6 @@ def mock_trade_usdt_4(fee, is_short: bool):
|
||||
is_open=True,
|
||||
open_rate=2.0,
|
||||
exchange='binance',
|
||||
open_order_id=f'prod_buy_12345_{direc(is_short)}',
|
||||
strategy='StrategyTestV2',
|
||||
timeframe=5,
|
||||
is_short=is_short,
|
||||
@@ -340,7 +337,6 @@ def mock_trade_usdt_6(fee, is_short: bool):
|
||||
open_rate=10.0,
|
||||
exchange='binance',
|
||||
strategy='SampleStrategy',
|
||||
open_order_id=f'prod_exit_6_{direc(is_short)}',
|
||||
timeframe=5,
|
||||
is_short=is_short,
|
||||
)
|
||||
@@ -378,7 +374,6 @@ def mock_trade_usdt_7(fee, is_short: bool):
|
||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17),
|
||||
open_rate=2.0,
|
||||
exchange='binance',
|
||||
open_order_id=None,
|
||||
strategy='StrategyTestV2',
|
||||
timeframe=5,
|
||||
is_short=is_short,
|
||||
|
||||
@@ -15,6 +15,7 @@ from freqtrade.persistence import Trade, init_db
|
||||
from freqtrade.persistence.base import ModelBase
|
||||
from freqtrade.persistence.migrations import get_last_sequence_ids, set_sequence_ids
|
||||
from freqtrade.persistence.models import PairLock
|
||||
from freqtrade.persistence.trade_model import Order
|
||||
from tests.conftest import log_has
|
||||
|
||||
|
||||
@@ -217,6 +218,23 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
|
||||
{amount},
|
||||
0,
|
||||
{amount * 0.00258580}
|
||||
),
|
||||
(
|
||||
-- Order without reference trade
|
||||
2,
|
||||
'buy',
|
||||
'ETC/BTC',
|
||||
1,
|
||||
'dry_buy_order55',
|
||||
'canceled',
|
||||
'ETC/BTC',
|
||||
'limit',
|
||||
'buy',
|
||||
0.00258580,
|
||||
{amount},
|
||||
{amount},
|
||||
0,
|
||||
{amount * 0.00258580}
|
||||
)
|
||||
"""
|
||||
engine = create_engine('sqlite://')
|
||||
@@ -238,9 +256,10 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
|
||||
# Run init to test migration
|
||||
init_db(default_conf['db_url'])
|
||||
|
||||
trades = Trade.session.scalars(select(Trade).filter(Trade.id == 1)).all()
|
||||
trades = Trade.session.scalars(select(Trade)).all()
|
||||
assert len(trades) == 1
|
||||
trade = trades[0]
|
||||
assert trade.id == 1
|
||||
assert trade.fee_open == fee.return_value
|
||||
assert trade.fee_close == fee.return_value
|
||||
assert trade.open_rate_requested is None
|
||||
@@ -281,12 +300,18 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
|
||||
|
||||
assert orders[1].order_id == 'dry_buy_order22'
|
||||
assert orders[1].ft_order_side == 'buy'
|
||||
assert orders[1].ft_is_open is False
|
||||
assert orders[1].ft_is_open is True
|
||||
|
||||
assert orders[2].order_id == 'dry_stop_order_id11X'
|
||||
assert orders[2].ft_order_side == 'stoploss'
|
||||
assert orders[2].ft_is_open is False
|
||||
|
||||
orders1 = Order.session.scalars(select(Order)).all()
|
||||
assert len(orders1) == 5
|
||||
order = orders1[4]
|
||||
assert order.ft_trade_id == 2
|
||||
assert order.ft_is_open is False
|
||||
|
||||
|
||||
def test_migrate_too_old(mocker, default_conf, fee, caplog):
|
||||
"""
|
||||
|
||||
@@ -10,7 +10,8 @@ from freqtrade.enums import TradingMode
|
||||
from freqtrade.exceptions import DependencyException
|
||||
from freqtrade.persistence import LocalTrade, Order, Trade, init_db
|
||||
from freqtrade.util import dt_now
|
||||
from tests.conftest import create_mock_trades, create_mock_trades_with_leverage, log_has, log_has_re
|
||||
from tests.conftest import (create_mock_trades, create_mock_trades_usdt,
|
||||
create_mock_trades_with_leverage, log_has, log_has_re)
|
||||
|
||||
|
||||
spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURES
|
||||
@@ -457,15 +458,14 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_
|
||||
leverage=lev,
|
||||
trading_mode=trading_mode
|
||||
)
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.close_profit is None
|
||||
assert trade.close_date is None
|
||||
|
||||
trade.open_order_id = enter_order['id']
|
||||
oobj = Order.parse_from_ccxt_object(enter_order, 'ADA/USDT', entry_side)
|
||||
trade.orders.append(oobj)
|
||||
trade.update_trade(oobj)
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.open_rate == open_rate
|
||||
assert trade.close_profit is None
|
||||
assert trade.close_date is None
|
||||
@@ -476,13 +476,12 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_
|
||||
caplog)
|
||||
|
||||
caplog.clear()
|
||||
trade.open_order_id = enter_order['id']
|
||||
time_machine.move_to("2022-03-31 21:45:05 +00:00")
|
||||
oobj = Order.parse_from_ccxt_object(exit_order, 'ADA/USDT', exit_side)
|
||||
trade.orders.append(oobj)
|
||||
trade.update_trade(oobj)
|
||||
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.close_rate == close_rate
|
||||
assert pytest.approx(trade.close_profit) == profit
|
||||
assert trade.close_date is not None
|
||||
@@ -511,11 +510,10 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
|
||||
leverage=1.0,
|
||||
)
|
||||
|
||||
trade.open_order_id = 'mocked_market_buy'
|
||||
oobj = Order.parse_from_ccxt_object(market_buy_order_usdt, 'ADA/USDT', 'buy')
|
||||
trade.orders.append(oobj)
|
||||
trade.update_trade(oobj)
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.open_rate == 2.0
|
||||
assert trade.close_profit is None
|
||||
assert trade.close_date is None
|
||||
@@ -526,11 +524,10 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
|
||||
|
||||
caplog.clear()
|
||||
trade.is_open = True
|
||||
trade.open_order_id = 'mocked_market_sell'
|
||||
oobj = Order.parse_from_ccxt_object(market_sell_order_usdt, 'ADA/USDT', 'sell')
|
||||
trade.orders.append(oobj)
|
||||
trade.update_trade(oobj)
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.close_rate == 2.2
|
||||
assert pytest.approx(trade.close_profit) == 0.094513715710723
|
||||
assert trade.close_date is not None
|
||||
@@ -580,7 +577,6 @@ def test_calc_open_close_trade_price(
|
||||
)
|
||||
entry_order = limit_order[trade.entry_side]
|
||||
exit_order = limit_order[trade.exit_side]
|
||||
trade.open_order_id = f'something-{is_short}-{lev}-{exchange}'
|
||||
|
||||
oobj = Order.parse_from_ccxt_object(entry_order, 'ADA/USDT', trade.entry_side)
|
||||
oobj._trade_live = trade
|
||||
@@ -678,7 +674,6 @@ def test_calc_close_trade_price_exception(limit_buy_order_usdt, fee):
|
||||
leverage=1.0,
|
||||
)
|
||||
|
||||
trade.open_order_id = 'something'
|
||||
oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
|
||||
trade.update_trade(oobj)
|
||||
assert trade.calc_close_trade_value(trade.close_rate) == 0.0
|
||||
@@ -697,7 +692,7 @@ def test_update_open_order(limit_buy_order_usdt):
|
||||
trading_mode=margin
|
||||
)
|
||||
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.close_profit is None
|
||||
assert trade.close_date is None
|
||||
|
||||
@@ -705,7 +700,7 @@ def test_update_open_order(limit_buy_order_usdt):
|
||||
oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy')
|
||||
trade.update_trade(oobj)
|
||||
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.close_profit is None
|
||||
assert trade.close_date is None
|
||||
|
||||
@@ -778,7 +773,6 @@ def test_calc_open_trade_value(
|
||||
is_short=is_short,
|
||||
trading_mode=trading_mode
|
||||
)
|
||||
trade.open_order_id = 'open_trade'
|
||||
oobj = Order.parse_from_ccxt_object(
|
||||
limit_buy_order_usdt, 'ADA/USDT', 'sell' if is_short else 'buy')
|
||||
trade.update_trade(oobj) # Buy @ 2.0
|
||||
@@ -833,7 +827,6 @@ def test_calc_close_trade_price(
|
||||
trading_mode=trading_mode,
|
||||
funding_fees=funding_fees
|
||||
)
|
||||
trade.open_order_id = 'close_trade'
|
||||
assert round(trade.calc_close_trade_value(rate=close_rate), 8) == result
|
||||
|
||||
|
||||
@@ -1156,7 +1149,6 @@ def test_calc_profit(
|
||||
trading_mode=trading_mode,
|
||||
funding_fees=funding_fees
|
||||
)
|
||||
trade.open_order_id = 'something'
|
||||
|
||||
profit_res = trade.calculate_profit(close_rate)
|
||||
assert pytest.approx(profit_res.profit_abs) == round(profit, 8)
|
||||
@@ -1352,6 +1344,24 @@ def test_get_open_lev(fee, use_db):
|
||||
Trade.use_db = True
|
||||
|
||||
|
||||
@pytest.mark.parametrize('is_short', [True, False])
|
||||
@pytest.mark.parametrize('use_db', [True, False])
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_get_open_orders(fee, is_short, use_db):
|
||||
Trade.use_db = use_db
|
||||
Trade.reset_trades()
|
||||
|
||||
create_mock_trades_usdt(fee, is_short, use_db)
|
||||
# Trade.commit()
|
||||
trade = Trade.get_trades_proxy(pair="XRP/USDT")[0]
|
||||
# assert trade.id == 3
|
||||
assert len(trade.orders) == 2
|
||||
assert len(trade.open_orders) == 0
|
||||
assert not trade.has_open_orders
|
||||
|
||||
Trade.use_db = True
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_to_json(fee):
|
||||
|
||||
@@ -1367,7 +1377,6 @@ def test_to_json(fee):
|
||||
open_rate=0.123,
|
||||
exchange='binance',
|
||||
enter_tag=None,
|
||||
open_order_id='dry_run_buy_12345',
|
||||
precision_mode=1,
|
||||
amount_precision=8.0,
|
||||
price_precision=7.0,
|
||||
@@ -1383,7 +1392,6 @@ def test_to_json(fee):
|
||||
'is_open': None,
|
||||
'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT),
|
||||
'open_timestamp': int(trade.open_date.timestamp() * 1000),
|
||||
'open_order_id': 'dry_run_buy_12345',
|
||||
'close_date': None,
|
||||
'close_timestamp': None,
|
||||
'open_rate': 0.123,
|
||||
@@ -1438,6 +1446,7 @@ def test_to_json(fee):
|
||||
'price_precision': 7.0,
|
||||
'precision_mode': 1,
|
||||
'orders': [],
|
||||
'has_open_orders': False,
|
||||
}
|
||||
|
||||
# Simulate dry_run entries
|
||||
@@ -1505,7 +1514,6 @@ def test_to_json(fee):
|
||||
'is_open': None,
|
||||
'max_rate': None,
|
||||
'min_rate': None,
|
||||
'open_order_id': None,
|
||||
'open_rate_requested': None,
|
||||
'open_trade_value': 12.33075,
|
||||
'exit_reason': None,
|
||||
@@ -1524,6 +1532,7 @@ def test_to_json(fee):
|
||||
'price_precision': 8.0,
|
||||
'precision_mode': 2,
|
||||
'orders': [],
|
||||
'has_open_orders': False,
|
||||
}
|
||||
|
||||
|
||||
@@ -2066,7 +2075,6 @@ def test_Trade_object_idem():
|
||||
'total_open_trades_stakes',
|
||||
'get_closed_trades_without_assigned_fees',
|
||||
'get_open_trades_without_assigned_fees',
|
||||
'get_open_order_trades',
|
||||
'get_trades',
|
||||
'get_trades_query',
|
||||
'get_exit_reason_performance',
|
||||
@@ -2676,7 +2684,7 @@ def test_recalc_trade_from_orders_dca(data) -> None:
|
||||
assert len(trade.orders) == idx + 1
|
||||
if idx < len(data) - 1:
|
||||
assert trade.is_open is True
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.amount == result[0]
|
||||
assert trade.open_rate == result[1]
|
||||
assert trade.stake_amount == result[2]
|
||||
@@ -2690,4 +2698,4 @@ def test_recalc_trade_from_orders_dca(data) -> None:
|
||||
assert not trade.is_open
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
|
||||
@@ -42,7 +42,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'strategy': ANY,
|
||||
'enter_tag': ANY,
|
||||
'timeframe': 5,
|
||||
'open_order_id': ANY,
|
||||
'close_date': None,
|
||||
'close_timestamp': None,
|
||||
'open_rate': 1.098e-05,
|
||||
@@ -75,7 +74,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'stoploss_current_dist_pct': -10.01,
|
||||
'stoploss_entry_dist': -0.00010402,
|
||||
'stoploss_entry_dist_ratio': -0.10376381,
|
||||
'open_order': None,
|
||||
'open_orders': '',
|
||||
'realized_profit': 0.0,
|
||||
'realized_profit_ratio': None,
|
||||
'total_profit_abs': -4.09e-06,
|
||||
@@ -91,6 +90,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'amount_precision': 8.0,
|
||||
'price_precision': 8.0,
|
||||
'precision_mode': 2,
|
||||
'has_open_orders': False,
|
||||
'orders': [{
|
||||
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
|
||||
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
|
||||
@@ -128,7 +128,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'profit_pct': 0.0,
|
||||
'profit_abs': 0.0,
|
||||
'total_profit_abs': 0.0,
|
||||
'open_order': '(limit buy rem=91.07468123)',
|
||||
'open_orders': '(limit buy rem=91.07468123)',
|
||||
'has_open_orders': True,
|
||||
})
|
||||
response_unfilled['orders'][0].update({
|
||||
'is_open': True,
|
||||
@@ -146,7 +147,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
results = rpc._rpc_trade_status()
|
||||
# Reuse above object, only remaining changed.
|
||||
response_unfilled['orders'][0].update({
|
||||
'remaining': None
|
||||
'remaining': None,
|
||||
})
|
||||
assert results[0] == response_unfilled
|
||||
|
||||
@@ -165,6 +166,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
response.update({
|
||||
'max_stake_amount': 0.001,
|
||||
'total_profit_ratio': pytest.approx(-0.00409153),
|
||||
'has_open_orders': False,
|
||||
})
|
||||
assert results[0] == response
|
||||
|
||||
@@ -779,7 +781,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
|
||||
'amount': amount,
|
||||
'remaining': amount,
|
||||
'filled': 0.0,
|
||||
'id': trade.orders[0].order_id,
|
||||
'id': trade.orders[-1].order_id,
|
||||
}
|
||||
)
|
||||
cancel_order_3 = mocker.patch(
|
||||
@@ -791,7 +793,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
|
||||
'amount': amount,
|
||||
'remaining': amount,
|
||||
'filled': 0.0,
|
||||
'id': trade.orders[0].order_id,
|
||||
'id': trade.orders[-1].order_id,
|
||||
}
|
||||
)
|
||||
msg = rpc._rpc_force_exit('3')
|
||||
@@ -800,7 +802,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
|
||||
assert cancel_order_3.call_count == 1
|
||||
assert cancel_order_mock.call_count == 0
|
||||
|
||||
trade = Trade.session.scalars(select(Trade).filter(Trade.id == '2')).first()
|
||||
trade = Trade.session.scalars(select(Trade).filter(Trade.id == '4')).first()
|
||||
amount = trade.amount
|
||||
# make an limit-buy open trade, if there is no 'filled', don't sell it
|
||||
mocker.patch(
|
||||
@@ -829,7 +831,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:
|
||||
assert msg == {'result': 'Created exit order for trade 4.'}
|
||||
assert cancel_order_4.call_count == 1
|
||||
assert cancel_order_mock.call_count == 0
|
||||
assert trade.amount == amount
|
||||
assert pytest.approx(trade.amount) == amount
|
||||
|
||||
|
||||
def test_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None:
|
||||
@@ -1097,7 +1099,8 @@ def test_rpc_force_entry(mocker, default_conf, ticker, fee, limit_buy_order_open
|
||||
trade = rpc._rpc_force_entry(pair, 0.0001, order_type='limit', stake_amount=0.05)
|
||||
assert trade.stake_amount == 0.05
|
||||
assert trade.buy_tag == 'force_entry'
|
||||
assert trade.open_order_id == 'mocked_limit_buy'
|
||||
|
||||
assert trade.open_orders_ids[-1] == 'mocked_limit_buy'
|
||||
|
||||
freqtradebot.strategy.position_adjustment_enable = True
|
||||
with pytest.raises(RPCException, match=r'position for LTC/BTC already open.*open order.*'):
|
||||
|
||||
@@ -1026,7 +1026,6 @@ def test_api_performance(botclient, fee):
|
||||
exchange='binance',
|
||||
stake_amount=1,
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456",
|
||||
is_open=False,
|
||||
fee_close=fee.return_value,
|
||||
fee_open=fee.return_value,
|
||||
@@ -1043,7 +1042,6 @@ def test_api_performance(botclient, fee):
|
||||
stake_amount=1,
|
||||
exchange='binance',
|
||||
open_rate=0.412,
|
||||
open_order_id="123456",
|
||||
is_open=False,
|
||||
fee_close=fee.return_value,
|
||||
fee_open=fee.return_value,
|
||||
@@ -1066,11 +1064,11 @@ def test_api_performance(botclient, fee):
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'is_short,current_rate,open_order_id,open_trade_value',
|
||||
[(True, 1.098e-05, 'dry_run_buy_short_12345', 15.0911775),
|
||||
(False, 1.099e-05, 'dry_run_buy_long_12345', 15.1668225)])
|
||||
'is_short,current_rate,open_trade_value',
|
||||
[(True, 1.098e-05, 15.0911775),
|
||||
(False, 1.099e-05, 15.1668225)])
|
||||
def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
|
||||
current_rate, open_order_id, open_trade_value):
|
||||
current_rate, open_trade_value):
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot)
|
||||
mocker.patch.multiple(
|
||||
@@ -1111,7 +1109,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
|
||||
'current_rate': current_rate,
|
||||
'open_date': ANY,
|
||||
'open_timestamp': ANY,
|
||||
'open_order': None,
|
||||
'open_rate': 0.123,
|
||||
'pair': 'ETH/BTC',
|
||||
'base_currency': 'ETH',
|
||||
@@ -1144,7 +1141,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
|
||||
"is_short": is_short,
|
||||
'max_rate': ANY,
|
||||
'min_rate': ANY,
|
||||
'open_order_id': open_order_id,
|
||||
'open_rate_requested': ANY,
|
||||
'open_trade_value': open_trade_value,
|
||||
'exit_reason': None,
|
||||
@@ -1162,6 +1158,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
|
||||
'price_precision': None,
|
||||
'precision_mode': None,
|
||||
'orders': [ANY],
|
||||
'has_open_orders': True,
|
||||
}
|
||||
|
||||
mocker.patch(f'{EXMS}.get_rate',
|
||||
@@ -1291,7 +1288,6 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
|
||||
exchange='binance',
|
||||
stake_amount=1,
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456",
|
||||
open_date=datetime.now(timezone.utc),
|
||||
is_open=False,
|
||||
is_short=False,
|
||||
@@ -1352,7 +1348,6 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
|
||||
'is_short': False,
|
||||
'max_rate': None,
|
||||
'min_rate': None,
|
||||
'open_order_id': '123456',
|
||||
'open_rate_requested': None,
|
||||
'open_trade_value': 0.24605460,
|
||||
'exit_reason': None,
|
||||
@@ -1369,6 +1364,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
|
||||
'amount_precision': None,
|
||||
'price_precision': None,
|
||||
'precision_mode': None,
|
||||
'has_open_orders': False,
|
||||
'orders': [],
|
||||
}
|
||||
|
||||
|
||||
@@ -872,7 +872,8 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
|
||||
trade.is_short = is_short
|
||||
assert trade
|
||||
assert trade.is_open is True
|
||||
assert trade.open_order_id == '22'
|
||||
assert trade.has_open_orders
|
||||
assert '22' in trade.open_orders_ids
|
||||
|
||||
# Test calling with price
|
||||
open_order['id'] = '33'
|
||||
@@ -898,7 +899,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
|
||||
trade = Trade.session.scalars(select(Trade)).all()[2]
|
||||
trade.is_short = is_short
|
||||
assert trade
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.open_rate == 10
|
||||
assert trade.stake_amount == round(order['average'] * order['filled'] / leverage, 8)
|
||||
assert pytest.approx(trade.liquidation_price) == liq_price
|
||||
@@ -916,7 +917,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
|
||||
trade = Trade.session.scalars(select(Trade)).all()[3]
|
||||
trade.is_short = is_short
|
||||
assert trade
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.open_rate == 0.5
|
||||
assert trade.stake_amount == round(order['average'] * order['filled'] / leverage, 8)
|
||||
|
||||
@@ -1118,7 +1119,6 @@ def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_sho
|
||||
freqtrade.enter_positions()
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
trade.is_short = is_short
|
||||
trade.open_order_id = None
|
||||
trade.stoploss_order_id = None
|
||||
trade.is_open = True
|
||||
trades = [trade]
|
||||
@@ -1163,7 +1163,6 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
trade.is_short = is_short
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
trade.stoploss_order_id = None
|
||||
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
@@ -1174,7 +1173,6 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
|
||||
# should do nothing and return false
|
||||
stop_order_dict.update({'id': "102"})
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
trade.stoploss_order_id = "102"
|
||||
trade.orders.append(
|
||||
Order(
|
||||
@@ -1198,7 +1196,6 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
|
||||
# should set a stoploss immediately and return False
|
||||
caplog.clear()
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
trade.stoploss_order_id = "102"
|
||||
|
||||
canceled_stoploss_order = MagicMock(return_value={'id': '103_1', 'status': 'canceled'})
|
||||
@@ -1223,7 +1220,6 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
trade.is_short = is_short
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
trade.stoploss_order_id = "104"
|
||||
trade.orders.append(Order(
|
||||
ft_order_side='stoploss',
|
||||
@@ -1351,7 +1347,6 @@ def test_handle_stoploss_on_exchange_partial(
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
trade.is_short = is_short
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
trade.stoploss_order_id = None
|
||||
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
@@ -1410,7 +1405,6 @@ def test_handle_stoploss_on_exchange_partial_cancel_here(
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
trade.is_short = is_short
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
trade.stoploss_order_id = None
|
||||
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
@@ -1468,8 +1462,8 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog,
|
||||
'last': 1.9
|
||||
}),
|
||||
create_order=MagicMock(side_effect=[
|
||||
{'id': enter_order['id']},
|
||||
{'id': exit_order['id']},
|
||||
enter_order,
|
||||
exit_order,
|
||||
]),
|
||||
get_fee=fee,
|
||||
)
|
||||
@@ -1485,7 +1479,6 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog,
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade.is_short == is_short
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
trade.stoploss_order_id = "100"
|
||||
trade.orders.append(
|
||||
Order(
|
||||
@@ -1663,7 +1656,6 @@ def test_handle_stoploss_on_exchange_trailing(
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
trade.is_short = is_short
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
trade.stoploss_order_id = '100'
|
||||
trade.stoploss_last_update = dt_now() - timedelta(minutes=20)
|
||||
trade.orders.append(
|
||||
@@ -1793,7 +1785,6 @@ def test_handle_stoploss_on_exchange_trailing_error(
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
trade.is_short = is_short
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
trade.stoploss_order_id = "abcd"
|
||||
trade.stop_loss = 0.2
|
||||
trade.stoploss_last_update = (dt_now() - timedelta(minutes=601)).replace(tzinfo=None)
|
||||
@@ -1873,8 +1864,8 @@ def test_handle_stoploss_on_exchange_custom_stop(
|
||||
'last': 1.9
|
||||
}),
|
||||
create_order=MagicMock(side_effect=[
|
||||
{'id': enter_order['id']},
|
||||
{'id': exit_order['id']},
|
||||
enter_order,
|
||||
exit_order,
|
||||
]),
|
||||
get_fee=fee,
|
||||
)
|
||||
@@ -1907,7 +1898,6 @@ def test_handle_stoploss_on_exchange_custom_stop(
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
trade.is_short = is_short
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
trade.stoploss_order_id = '100'
|
||||
trade.stoploss_last_update = dt_now() - timedelta(minutes=601)
|
||||
trade.orders.append(
|
||||
@@ -2045,7 +2035,6 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde
|
||||
freqtrade.enter_positions()
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
trade.stoploss_order_id = '100'
|
||||
trade.stoploss_last_update = dt_now()
|
||||
trade.orders.append(
|
||||
@@ -2152,7 +2141,6 @@ def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog
|
||||
|
||||
order_id = '123'
|
||||
trade = Trade(
|
||||
open_order_id=order_id,
|
||||
pair='ETH/USDT',
|
||||
fee_open=0.001,
|
||||
fee_close=0.001,
|
||||
@@ -2198,7 +2186,6 @@ def test_exit_positions_exception(mocker, default_conf_usdt, limit_order, caplog
|
||||
|
||||
order_id = '123'
|
||||
trade = Trade(
|
||||
open_order_id=order_id,
|
||||
pair='ETH/USDT',
|
||||
fee_open=0.001,
|
||||
fee_close=0.001,
|
||||
@@ -2217,9 +2204,9 @@ def test_exit_positions_exception(mocker, default_conf_usdt, limit_order, caplog
|
||||
ft_amount=trade.amount,
|
||||
ft_price=trade.open_rate,
|
||||
order_id=order_id,
|
||||
ft_is_open=False,
|
||||
|
||||
))
|
||||
trade.open_order_id = None
|
||||
Trade.session.add(trade)
|
||||
Trade.commit()
|
||||
freqtrade.wallets.update()
|
||||
@@ -2248,7 +2235,6 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca
|
||||
order_id = order['id']
|
||||
|
||||
trade = Trade(
|
||||
open_order_id=order_id,
|
||||
fee_open=0.001,
|
||||
fee_close=0.001,
|
||||
open_rate=0.01,
|
||||
@@ -2272,19 +2258,17 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca
|
||||
# Test amount not modified by fee-logic
|
||||
assert not log_has_re(r'Applying fee to .*', caplog)
|
||||
caplog.clear()
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.amount == order['amount']
|
||||
|
||||
trade.open_order_id = order_id
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=0.01)
|
||||
assert trade.amount == 30.0
|
||||
# test amount modified by fee-logic
|
||||
freqtrade.update_trade_state(trade, order_id)
|
||||
assert trade.amount == 29.99
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
# Assert we call handle_trade() if trade is feasible for execution
|
||||
freqtrade.update_trade_state(trade, order_id)
|
||||
|
||||
@@ -2328,7 +2312,6 @@ def test_update_trade_state_withorderdict(
|
||||
open_date=dt_now(),
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_order_id=order_id,
|
||||
is_open=True,
|
||||
leverage=1,
|
||||
is_short=is_short,
|
||||
@@ -2361,15 +2344,15 @@ def test_update_trade_state_exception(mocker, default_conf_usdt, is_short, limit
|
||||
|
||||
# TODO: should not be magicmock
|
||||
trade = MagicMock()
|
||||
trade.open_order_id = '123'
|
||||
trade.amount = 123
|
||||
open_order_id = '123'
|
||||
|
||||
# Test raise of OperationalException exception
|
||||
mocker.patch(
|
||||
'freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
|
||||
side_effect=DependencyException()
|
||||
)
|
||||
freqtrade.update_trade_state(trade, trade.open_order_id)
|
||||
freqtrade.update_trade_state(trade, open_order_id)
|
||||
assert log_has('Could not update trade amount: ', caplog)
|
||||
|
||||
|
||||
@@ -2379,13 +2362,13 @@ def test_update_trade_state_orderexception(mocker, default_conf_usdt, caplog) ->
|
||||
|
||||
# TODO: should not be magicmock
|
||||
trade = MagicMock()
|
||||
trade.open_order_id = '123'
|
||||
open_order_id = '123'
|
||||
|
||||
# Test raise of OperationalException exception
|
||||
grm_mock = mocker.patch("freqtrade.freqtradebot.FreqtradeBot.get_real_amount", MagicMock())
|
||||
freqtrade.update_trade_state(trade, trade.open_order_id)
|
||||
freqtrade.update_trade_state(trade, open_order_id)
|
||||
assert grm_mock.call_count == 0
|
||||
assert log_has(f'Unable to fetch order {trade.open_order_id}: ', caplog)
|
||||
assert log_has(f'Unable to fetch order {open_order_id}: ', caplog)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
@@ -2413,7 +2396,6 @@ def test_update_trade_state_sell(
|
||||
fee_open=0.0025,
|
||||
fee_close=0.0025,
|
||||
open_date=dt_now(),
|
||||
open_order_id=open_order['id'],
|
||||
is_open=True,
|
||||
interest_rate=0.0005,
|
||||
leverage=1,
|
||||
@@ -2425,7 +2407,7 @@ def test_update_trade_state_sell(
|
||||
order = Order.parse_from_ccxt_object(open_order, 'LTC/ETH', exit_side(is_short))
|
||||
trade.orders.append(order)
|
||||
assert order.status == 'open'
|
||||
freqtrade.update_trade_state(trade, trade.open_order_id, l_order)
|
||||
freqtrade.update_trade_state(trade, trade.open_orders_ids[-1], l_order)
|
||||
assert trade.amount == l_order['amount']
|
||||
# Wallet needs to be updated after closing a limit-sell order to reenable buying
|
||||
assert wallet_mock.call_count == 1
|
||||
@@ -2475,7 +2457,7 @@ def test_handle_trade(
|
||||
patch_get_signal(freqtrade, enter_long=False, exit_short=is_short,
|
||||
exit_long=not is_short, exit_tag='sell_signal1')
|
||||
assert freqtrade.handle_trade(trade) is True
|
||||
assert trade.open_order_id == exit_order['id']
|
||||
assert trade.open_orders_ids[-1] == exit_order['id']
|
||||
|
||||
# Simulate fulfilled LIMIT_SELL order for trade
|
||||
trade.orders[-1].ft_is_open = False
|
||||
@@ -2706,7 +2688,7 @@ def test_manage_open_orders_entry_usercustom(
|
||||
) -> None:
|
||||
|
||||
old_order = limit_sell_order_old if is_short else limit_buy_order_old
|
||||
old_order['id'] = open_trade.open_order_id
|
||||
old_order['id'] = open_trade.open_orders_ids[0]
|
||||
|
||||
default_conf_usdt["unfilledtimeout"] = {"entry": 1400, "exit": 30}
|
||||
|
||||
@@ -2741,7 +2723,11 @@ def test_manage_open_orders_entry_usercustom(
|
||||
freqtrade.manage_open_orders()
|
||||
assert cancel_order_mock.call_count == 0
|
||||
trades = Trade.session.scalars(
|
||||
select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
|
||||
select(Trade)
|
||||
.where(Order.ft_is_open.is_(True))
|
||||
.where(Order.ft_order_side != "stoploss")
|
||||
.where(Order.ft_trade_id == Trade.id)
|
||||
).all()
|
||||
nb_trades = len(trades)
|
||||
assert nb_trades == 1
|
||||
assert freqtrade.strategy.check_entry_timeout.call_count == 1
|
||||
@@ -2750,7 +2736,11 @@ def test_manage_open_orders_entry_usercustom(
|
||||
freqtrade.manage_open_orders()
|
||||
assert cancel_order_mock.call_count == 0
|
||||
trades = Trade.session.scalars(
|
||||
select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
|
||||
select(Trade)
|
||||
.where(Order.ft_is_open.is_(True))
|
||||
.where(Order.ft_order_side != "stoploss")
|
||||
.where(Order.ft_trade_id == Trade.id)
|
||||
).all()
|
||||
nb_trades = len(trades)
|
||||
assert nb_trades == 1
|
||||
assert freqtrade.strategy.check_entry_timeout.call_count == 1
|
||||
@@ -2761,7 +2751,11 @@ def test_manage_open_orders_entry_usercustom(
|
||||
assert cancel_order_wr_mock.call_count == 1
|
||||
assert rpc_mock.call_count == 2
|
||||
trades = Trade.session.scalars(
|
||||
select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
|
||||
select(Trade)
|
||||
.where(Order.ft_is_open.is_(True))
|
||||
.where(Order.ft_order_side != "stoploss")
|
||||
.where(Order.ft_trade_id == Trade.id)
|
||||
).all()
|
||||
nb_trades = len(trades)
|
||||
assert nb_trades == 0
|
||||
assert freqtrade.strategy.check_entry_timeout.call_count == 1
|
||||
@@ -2774,7 +2768,7 @@ def test_manage_open_orders_entry(
|
||||
) -> None:
|
||||
old_order = limit_sell_order_old if is_short else limit_buy_order_old
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
open_trade.open_order_id = old_order['id']
|
||||
|
||||
order = Order.parse_from_ccxt_object(old_order, 'mocked', 'buy')
|
||||
open_trade.orders[0] = order
|
||||
limit_buy_cancel = deepcopy(old_order)
|
||||
@@ -2801,7 +2795,11 @@ def test_manage_open_orders_entry(
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert rpc_mock.call_count == 2
|
||||
trades = Trade.session.scalars(
|
||||
select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
|
||||
select(Trade)
|
||||
.where(Order.ft_is_open.is_(True))
|
||||
.where(Order.ft_order_side != "stoploss")
|
||||
.where(Order.ft_trade_id == Trade.id)
|
||||
).all()
|
||||
nb_trades = len(trades)
|
||||
assert nb_trades == 0
|
||||
# Custom user entry-timeout is never called
|
||||
@@ -2817,7 +2815,7 @@ def test_adjust_entry_cancel(
|
||||
) -> None:
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
old_order = limit_sell_order_old if is_short else limit_buy_order_old
|
||||
old_order['id'] = open_trade.open_order_id
|
||||
old_order['id'] = open_trade.open_orders[0].order_id
|
||||
limit_buy_cancel = deepcopy(old_order)
|
||||
limit_buy_cancel['status'] = 'canceled'
|
||||
cancel_order_mock = MagicMock(return_value=limit_buy_cancel)
|
||||
@@ -2840,7 +2838,10 @@ def test_adjust_entry_cancel(
|
||||
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=None)
|
||||
freqtrade.manage_open_orders()
|
||||
trades = Trade.session.scalars(
|
||||
select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
|
||||
select(Trade)
|
||||
.where(Order.ft_trade_id == Trade.id)
|
||||
).all()
|
||||
|
||||
assert len(trades) == 0
|
||||
assert len(Order.session.scalars(select(Order)).all()) == 0
|
||||
assert log_has_re(
|
||||
@@ -2859,7 +2860,7 @@ def test_adjust_entry_maintain_replace(
|
||||
) -> None:
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
old_order = limit_sell_order_old if is_short else limit_buy_order_old
|
||||
old_order['id'] = open_trade.open_order_id
|
||||
old_order['id'] = open_trade.open_orders_ids[0]
|
||||
limit_buy_cancel = deepcopy(old_order)
|
||||
limit_buy_cancel['status'] = 'canceled'
|
||||
cancel_order_mock = MagicMock(return_value=limit_buy_cancel)
|
||||
@@ -2868,7 +2869,8 @@ def test_adjust_entry_maintain_replace(
|
||||
fetch_ticker=ticker_usdt,
|
||||
fetch_order=MagicMock(return_value=old_order),
|
||||
cancel_order_with_result=cancel_order_mock,
|
||||
get_fee=fee
|
||||
get_fee=fee,
|
||||
_dry_is_price_crossed=MagicMock(return_value=False),
|
||||
)
|
||||
|
||||
open_trade.is_short = is_short
|
||||
@@ -2882,7 +2884,10 @@ def test_adjust_entry_maintain_replace(
|
||||
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=old_order['price'])
|
||||
freqtrade.manage_open_orders()
|
||||
trades = Trade.session.scalars(
|
||||
select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
|
||||
select(Trade)
|
||||
.where(Order.ft_is_open.is_(True))
|
||||
.where(Order.ft_trade_id == Trade.id)
|
||||
).all()
|
||||
assert len(trades) == 1
|
||||
assert len(Order.get_open_orders()) == 1
|
||||
# Entry adjustment is called
|
||||
@@ -2891,9 +2896,16 @@ def test_adjust_entry_maintain_replace(
|
||||
# Check that order is replaced
|
||||
freqtrade.get_valid_enter_price_and_stake = MagicMock(return_value={100, 10, 1})
|
||||
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1234)
|
||||
|
||||
freqtrade.manage_open_orders()
|
||||
|
||||
assert freqtrade.strategy.adjust_entry_price.call_count == 1
|
||||
|
||||
trades = Trade.session.scalars(
|
||||
select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
|
||||
select(Trade)
|
||||
.where(Order.ft_is_open.is_(True))
|
||||
.where(Order.ft_trade_id == Trade.id)
|
||||
).all()
|
||||
assert len(trades) == 1
|
||||
nb_all_orders = len(Order.session.scalars(select(Order)).all())
|
||||
assert nb_all_orders == 2
|
||||
@@ -2917,6 +2929,8 @@ def test_check_handle_cancelled_buy(
|
||||
cancel_order_mock = MagicMock()
|
||||
patch_exchange(mocker)
|
||||
old_order.update({"status": "canceled", 'filled': 0.0})
|
||||
old_order['side'] = 'buy' if is_short else 'sell'
|
||||
old_order['id'] = open_trade.open_orders[0].order_id
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_ticker=ticker_usdt,
|
||||
@@ -2925,7 +2939,6 @@ def test_check_handle_cancelled_buy(
|
||||
get_fee=fee
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||
open_trade.orders = []
|
||||
open_trade.is_short = is_short
|
||||
Trade.session.add(open_trade)
|
||||
Trade.commit()
|
||||
@@ -2935,10 +2948,13 @@ def test_check_handle_cancelled_buy(
|
||||
assert cancel_order_mock.call_count == 0
|
||||
assert rpc_mock.call_count == 2
|
||||
trades = Trade.session.scalars(
|
||||
select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
|
||||
select(Trade)
|
||||
.where(Order.ft_is_open.is_(True))
|
||||
.where(Order.ft_trade_id == Trade.id)
|
||||
).all()
|
||||
assert len(trades) == 0
|
||||
assert log_has_re(
|
||||
f"{'Sell' if is_short else 'Buy'} order cancelled on exchange for Trade.*", caplog)
|
||||
exit_name = 'Buy' if is_short else 'Sell'
|
||||
assert log_has_re(f"{exit_name} order cancelled on exchange for Trade.*", caplog)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
@@ -2966,10 +2982,7 @@ def test_manage_open_orders_buy_exception(
|
||||
freqtrade.manage_open_orders()
|
||||
assert cancel_order_mock.call_count == 0
|
||||
assert rpc_mock.call_count == 1
|
||||
trades = Trade.session.scalars(
|
||||
select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
|
||||
nb_trades = len(trades)
|
||||
assert nb_trades == 1
|
||||
assert len(open_trade.open_orders) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
@@ -2978,7 +2991,7 @@ def test_manage_open_orders_exit_usercustom(
|
||||
is_short, open_trade_usdt, caplog
|
||||
) -> None:
|
||||
default_conf_usdt["unfilledtimeout"] = {"entry": 1440, "exit": 1440, "exit_timeout_count": 1}
|
||||
open_trade_usdt.open_order_id = limit_sell_order_old['id']
|
||||
|
||||
if is_short:
|
||||
limit_sell_order_old['side'] = 'buy'
|
||||
open_trade_usdt.is_short = is_short
|
||||
@@ -3035,13 +3048,10 @@ def test_manage_open_orders_exit_usercustom(
|
||||
assert rpc_mock.call_count == 2
|
||||
assert freqtrade.strategy.check_exit_timeout.call_count == 1
|
||||
assert freqtrade.strategy.check_entry_timeout.call_count == 0
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
# cancelling didn't succeed - order-id remains open.
|
||||
assert trade.open_order_id is not None
|
||||
|
||||
# 2nd canceled trade - Fail execute exit
|
||||
caplog.clear()
|
||||
open_trade_usdt.open_order_id = limit_sell_order_old['id']
|
||||
|
||||
mocker.patch('freqtrade.persistence.Trade.get_exit_order_count', return_value=1)
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit',
|
||||
side_effect=DependencyException)
|
||||
@@ -3051,7 +3061,6 @@ def test_manage_open_orders_exit_usercustom(
|
||||
et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit')
|
||||
caplog.clear()
|
||||
# 2nd canceled trade ...
|
||||
open_trade_usdt.open_order_id = limit_sell_order_old['id']
|
||||
|
||||
# If cancelling fails - no emergency exit!
|
||||
with patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit', return_value=False):
|
||||
@@ -3069,7 +3078,7 @@ def test_manage_open_orders_exit(
|
||||
) -> None:
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
cancel_order_mock = MagicMock()
|
||||
limit_sell_order_old['id'] = open_trade_usdt.open_order_id
|
||||
limit_sell_order_old['id'] = '123456789_exit'
|
||||
limit_sell_order_old['side'] = 'buy' if is_short else 'sell'
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
@@ -3111,7 +3120,7 @@ def test_check_handle_cancelled_exit(
|
||||
cancel_order_mock = MagicMock()
|
||||
limit_sell_order_old.update({"status": "canceled", 'filled': 0.0})
|
||||
limit_sell_order_old['side'] = 'buy' if is_short else 'sell'
|
||||
limit_sell_order_old['id'] = open_trade_usdt.open_order_id
|
||||
limit_sell_order_old['id'] = open_trade_usdt.open_orders[0].order_id
|
||||
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
@@ -3148,7 +3157,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.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'
|
||||
@@ -3172,11 +3182,13 @@ 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)
|
||||
).all()
|
||||
assert len(trades) == 1
|
||||
assert trades[0].amount == 23.0
|
||||
assert trades[0].stake_amount == open_trade.open_rate * trades[0].amount / leverage
|
||||
assert trades[0].stake_amount != prior_stake
|
||||
assert not trades[0].has_open_orders
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
@@ -3188,8 +3200,8 @@ def test_manage_open_orders_partial_fee(
|
||||
open_trade.is_short = is_short
|
||||
open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy'
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
limit_buy_order_old_partial['id'] = open_trade.open_order_id
|
||||
limit_buy_order_old_partial_canceled['id'] = open_trade.open_order_id
|
||||
limit_buy_order_old_partial['id'] = open_trade.orders[0].order_id
|
||||
limit_buy_order_old_partial_canceled['id'] = open_trade.open_orders_ids[0]
|
||||
limit_buy_order_old_partial['side'] = 'sell' if is_short else 'buy'
|
||||
limit_buy_order_old_partial_canceled['side'] = 'sell' if is_short else 'buy'
|
||||
|
||||
@@ -3220,12 +3232,14 @@ def test_manage_open_orders_partial_fee(
|
||||
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)
|
||||
.where(Order.ft_trade_id == Trade.id)
|
||||
).all()
|
||||
assert len(trades) == 1
|
||||
# Verify that trade has been updated
|
||||
assert trades[0].amount == (limit_buy_order_old_partial['amount'] -
|
||||
limit_buy_order_old_partial['remaining']) - 0.023
|
||||
assert trades[0].open_order_id is None
|
||||
assert not trades[0].has_open_orders
|
||||
assert trades[0].fee_updated(open_trade.entry_side)
|
||||
assert pytest.approx(trades[0].fee_open) == 0.001
|
||||
|
||||
@@ -3239,8 +3253,8 @@ def test_manage_open_orders_partial_except(
|
||||
open_trade.is_short = is_short
|
||||
open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy'
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
limit_buy_order_old_partial_canceled['id'] = open_trade.open_order_id
|
||||
limit_buy_order_old_partial['id'] = open_trade.open_order_id
|
||||
limit_buy_order_old_partial_canceled['id'] = open_trade.open_orders_ids[0]
|
||||
limit_buy_order_old_partial['id'] = open_trade.open_orders_ids[0]
|
||||
if is_short:
|
||||
limit_buy_order_old_partial['side'] = 'sell'
|
||||
cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled)
|
||||
@@ -3271,13 +3285,14 @@ def test_manage_open_orders_partial_except(
|
||||
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)
|
||||
).all()
|
||||
assert len(trades) == 1
|
||||
# Verify that trade has been updated
|
||||
|
||||
assert trades[0].amount == (limit_buy_order_old_partial['amount'] -
|
||||
limit_buy_order_old_partial['remaining'])
|
||||
assert trades[0].open_order_id is None
|
||||
assert not trades[0].has_open_orders
|
||||
assert trades[0].fee_open == fee()
|
||||
|
||||
|
||||
@@ -3335,32 +3350,34 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_
|
||||
l_order['filled'] = 0.0
|
||||
l_order['status'] = 'open'
|
||||
reason = CANCEL_REASON['TIMEOUT']
|
||||
assert freqtrade.handle_cancel_enter(trade, l_order, reason)
|
||||
assert freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders_ids[0], reason)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
|
||||
cancel_order_mock.reset_mock()
|
||||
caplog.clear()
|
||||
l_order['filled'] = 0.01
|
||||
assert not freqtrade.handle_cancel_enter(trade, l_order, reason)
|
||||
assert not freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders_ids[0], reason)
|
||||
assert cancel_order_mock.call_count == 0
|
||||
assert log_has_re("Order .* for .* not cancelled, as the filled amount.* unexitable.*", caplog)
|
||||
|
||||
caplog.clear()
|
||||
cancel_order_mock.reset_mock()
|
||||
l_order['filled'] = 2
|
||||
assert not freqtrade.handle_cancel_enter(trade, l_order, reason)
|
||||
assert not freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders_ids[0], reason)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
|
||||
# Order remained open for some reason (cancel failed)
|
||||
cancel_buy_order['status'] = 'open'
|
||||
cancel_order_mock = MagicMock(return_value=cancel_buy_order)
|
||||
trade.open_order_id = 'some_open_order'
|
||||
|
||||
mocker.patch(f'{EXMS}.cancel_order_with_result', cancel_order_mock)
|
||||
assert not freqtrade.handle_cancel_enter(trade, l_order, reason)
|
||||
assert not freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders_ids[0], reason)
|
||||
assert log_has_re(r"Order .* for .* not cancelled.", caplog)
|
||||
# min_pair_stake empty should not crash
|
||||
mocker.patch(f'{EXMS}.get_min_pair_stake_amount', return_value=None)
|
||||
assert not freqtrade.handle_cancel_enter(trade, limit_order[entry_side(is_short)], reason)
|
||||
assert not freqtrade.handle_cancel_enter(
|
||||
trade, limit_order[entry_side(is_short)], trade.open_orders_ids[0], reason
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
@@ -3381,7 +3398,9 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_sho
|
||||
trade = mock_trade_usdt_4(fee, is_short)
|
||||
Trade.session.add(trade)
|
||||
Trade.commit()
|
||||
assert freqtrade.handle_cancel_enter(trade, limit_buy_order_canceled_empty, reason)
|
||||
assert freqtrade.handle_cancel_enter(
|
||||
trade, limit_buy_order_canceled_empty, trade.open_orders_ids[0], reason
|
||||
)
|
||||
assert cancel_order_mock.call_count == 0
|
||||
assert log_has_re(
|
||||
f'{trade.entry_side.capitalize()} order fully cancelled. '
|
||||
@@ -3418,7 +3437,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order
|
||||
l_order['filled'] = 0.0
|
||||
l_order['status'] = 'open'
|
||||
reason = CANCEL_REASON['TIMEOUT']
|
||||
assert freqtrade.handle_cancel_enter(trade, l_order, reason)
|
||||
assert freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders_ids[0], reason)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
|
||||
cancel_order_mock.reset_mock()
|
||||
@@ -3426,7 +3445,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order
|
||||
order = deepcopy(l_order)
|
||||
order['status'] = 'canceled'
|
||||
mocker.patch(f'{EXMS}.fetch_order', return_value=order)
|
||||
assert not freqtrade.handle_cancel_enter(trade, l_order, reason)
|
||||
assert not freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders_ids[0], reason)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
|
||||
|
||||
@@ -3456,7 +3475,6 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee, is_short,
|
||||
amount=amount * leverage,
|
||||
exchange='binance',
|
||||
open_rate=entry_price,
|
||||
open_order_id="sell_123456",
|
||||
open_date=dt_now() - timedelta(days=2),
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
@@ -3509,26 +3527,26 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee, is_short,
|
||||
'status': "open"}
|
||||
reason = CANCEL_REASON['TIMEOUT']
|
||||
send_msg_mock.reset_mock()
|
||||
assert freqtrade.handle_cancel_exit(trade, order, reason)
|
||||
assert freqtrade.handle_cancel_exit(trade, order, order['id'], reason)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert send_msg_mock.call_count == 1
|
||||
assert trade.close_rate is None
|
||||
assert trade.exit_reason is None
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
|
||||
send_msg_mock.reset_mock()
|
||||
|
||||
# Partial exit - below exit threshold
|
||||
order['amount'] = amount * leverage
|
||||
order['filled'] = amount * 0.99 * leverage
|
||||
assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
||||
assert not freqtrade.handle_cancel_exit(trade, order, order['id'], reason)
|
||||
# Assert cancel_order was not called (callcount remains unchanged)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert send_msg_mock.call_count == 1
|
||||
assert (send_msg_mock.call_args_list[0][0][0]['reason']
|
||||
== CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'])
|
||||
|
||||
assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
||||
assert not freqtrade.handle_cancel_exit(trade, order, order['id'], reason)
|
||||
|
||||
assert (send_msg_mock.call_args_list[0][0][0]['reason']
|
||||
== CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'])
|
||||
@@ -3540,7 +3558,7 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee, is_short,
|
||||
send_msg_mock.reset_mock()
|
||||
|
||||
order['filled'] = amount * 0.5 * leverage
|
||||
assert freqtrade.handle_cancel_exit(trade, order, reason)
|
||||
assert freqtrade.handle_cancel_exit(trade, order, order['id'], reason)
|
||||
assert send_msg_mock.call_count == 1
|
||||
assert (send_msg_mock.call_args_list[0][0][0]['reason']
|
||||
== CANCEL_REASON['PARTIALLY_FILLED'])
|
||||
@@ -3556,17 +3574,16 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
|
||||
|
||||
# TODO: should not be magicmock
|
||||
trade = MagicMock()
|
||||
trade.open_order_id = '125'
|
||||
order_id = '125'
|
||||
reason = CANCEL_REASON['TIMEOUT']
|
||||
order = {'remaining': 1,
|
||||
'id': '125',
|
||||
'amount': 1,
|
||||
'status': "open"}
|
||||
assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
||||
assert not freqtrade.handle_cancel_exit(trade, order, order_id, reason)
|
||||
|
||||
# mocker.patch(f'{EXMS}.cancel_order_with_result', return_value=order)
|
||||
# assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
||||
# assert trade.open_order_id == '125'
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short, open_rate, amt", [
|
||||
@@ -4013,7 +4030,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
|
||||
freqtrade.exit_positions(trades)
|
||||
assert trade
|
||||
assert trade.stoploss_order_id == '123'
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
|
||||
# Assuming stoploss on exchange is hit
|
||||
# stoploss_order_id should become None
|
||||
@@ -4308,7 +4325,6 @@ def test__safe_exit_amount(default_conf_usdt, fee, caplog, mocker, amount_wallet
|
||||
amount=amount,
|
||||
exchange='binance',
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456",
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
)
|
||||
@@ -4651,7 +4667,6 @@ def test_get_real_amount_quote(default_conf_usdt, trades_for_order, buy_order_fe
|
||||
open_rate=0.245441,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_order_id="123456"
|
||||
)
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
|
||||
@@ -4679,7 +4694,6 @@ def test_get_real_amount_quote_dust(default_conf_usdt, trades_for_order, buy_ord
|
||||
open_rate=0.245441,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_order_id="123456"
|
||||
)
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
|
||||
@@ -4703,7 +4717,6 @@ def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mock
|
||||
open_rate=0.245441,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_order_id="123456"
|
||||
)
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
|
||||
@@ -4756,7 +4769,6 @@ def test_get_real_amount(
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456"
|
||||
)
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
|
||||
@@ -4803,8 +4815,7 @@ def test_get_real_amount_multi(
|
||||
exchange='binance',
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456"
|
||||
open_rate=0.245441
|
||||
)
|
||||
|
||||
# Fake markets entry to enable fee parsing
|
||||
@@ -4849,7 +4860,6 @@ def test_get_real_amount_invalid_order(default_conf_usdt, trades_for_order, buy_
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456"
|
||||
)
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
|
||||
@@ -4871,7 +4881,6 @@ def test_get_real_amount_fees_order(default_conf_usdt, market_buy_order_usdt_dou
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456"
|
||||
)
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
|
||||
@@ -4898,7 +4907,6 @@ def test_get_real_amount_wrong_amount(default_conf_usdt, trades_for_order, buy_o
|
||||
open_rate=0.245441,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_order_id="123456"
|
||||
)
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
|
||||
@@ -4923,7 +4931,6 @@ def test_get_real_amount_wrong_amount_rounding(default_conf_usdt, trades_for_ord
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456"
|
||||
)
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
|
||||
@@ -4942,7 +4949,6 @@ def test_get_real_amount_open_trade_usdt(default_conf_usdt, fee, mocker):
|
||||
open_rate=0.245441,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_order_id="123456"
|
||||
)
|
||||
order = {
|
||||
'id': 'mocked_order',
|
||||
@@ -5002,8 +5008,7 @@ def test_get_real_amount_in_point(default_conf_usdt, buy_order_fee, fee, mocker,
|
||||
exchange='binance',
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456"
|
||||
open_rate=0.245441
|
||||
)
|
||||
limit_buy_order_usdt['amount'] = amount
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
@@ -5045,7 +5050,6 @@ def test_apply_fee_conditional(default_conf_usdt, fee, mocker, caplog,
|
||||
open_rate=0.245441,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_order_id="123456"
|
||||
)
|
||||
order = Order(
|
||||
ft_order_side='buy',
|
||||
@@ -5083,8 +5087,7 @@ def test_apply_fee_conditional_multibuy(default_conf_usdt, fee, mocker, caplog,
|
||||
exchange='binance',
|
||||
open_rate=0.245441,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_order_id="123456"
|
||||
fee_close=fee.return_value
|
||||
)
|
||||
# One closed order
|
||||
order = Order(
|
||||
@@ -5558,7 +5561,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
|
||||
return_value={'status': 'open'})
|
||||
|
||||
def reset_open_orders(trade):
|
||||
trade.open_order_id = None
|
||||
|
||||
trade.stoploss_order_id = None
|
||||
trade.is_short = is_short
|
||||
|
||||
@@ -5570,7 +5573,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
|
||||
# No open order
|
||||
trade = trades[1]
|
||||
reset_open_orders(trade)
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.stoploss_order_id is None
|
||||
|
||||
freqtrade.handle_insufficient_funds(trade)
|
||||
@@ -5580,7 +5583,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
|
||||
assert mock_fo.call_count == 0
|
||||
assert mock_uts.call_count == 0
|
||||
# No change to orderid - as update_trade_state is mocked
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.stoploss_order_id is None
|
||||
|
||||
caplog.clear()
|
||||
@@ -5589,7 +5592,9 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
|
||||
# Open buy order
|
||||
trade = trades[3]
|
||||
reset_open_orders(trade)
|
||||
assert trade.open_order_id is None
|
||||
|
||||
# This part in not relevant anymore
|
||||
# assert not trade.has_open_orders
|
||||
assert trade.stoploss_order_id is None
|
||||
|
||||
freqtrade.handle_insufficient_funds(trade)
|
||||
@@ -5598,7 +5603,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
|
||||
assert mock_fo.call_count == 1
|
||||
assert mock_uts.call_count == 1
|
||||
# Found open buy order
|
||||
assert trade.open_order_id is not None
|
||||
assert trade.has_open_orders
|
||||
assert trade.stoploss_order_id is None
|
||||
|
||||
caplog.clear()
|
||||
@@ -5607,7 +5612,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
|
||||
# Open stoploss order
|
||||
trade = trades[4]
|
||||
reset_open_orders(trade)
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.stoploss_order_id is None
|
||||
|
||||
freqtrade.handle_insufficient_funds(trade)
|
||||
@@ -5616,7 +5621,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
|
||||
assert mock_fo.call_count == 1
|
||||
assert mock_uts.call_count == 2
|
||||
# stoploss_order_id is "refound" and added to the trade
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.stoploss_order_id is not None
|
||||
|
||||
caplog.clear()
|
||||
@@ -5626,7 +5631,8 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
|
||||
# Open sell order
|
||||
trade = trades[5]
|
||||
reset_open_orders(trade)
|
||||
assert trade.open_order_id is None
|
||||
# This part in not relevant anymore
|
||||
# assert not trade.has_open_orders
|
||||
assert trade.stoploss_order_id is None
|
||||
|
||||
freqtrade.handle_insufficient_funds(trade)
|
||||
@@ -5635,7 +5641,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
|
||||
assert mock_fo.call_count == 1
|
||||
assert mock_uts.call_count == 1
|
||||
# sell-orderid is "refound" and added to the trade
|
||||
assert trade.open_order_id == order['id']
|
||||
assert trade.open_orders_ids[0] == order['id']
|
||||
assert trade.stoploss_order_id is None
|
||||
|
||||
caplog.clear()
|
||||
@@ -5662,10 +5668,7 @@ def test_handle_onexchange_order(mocker, default_conf_usdt, limit_order, is_shor
|
||||
exit_order,
|
||||
])
|
||||
|
||||
order_id = entry_order['id']
|
||||
|
||||
trade = Trade(
|
||||
open_order_id=order_id,
|
||||
pair='ETH/USDT',
|
||||
fee_open=0.001,
|
||||
fee_close=0.001,
|
||||
@@ -5989,7 +5992,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade
|
||||
assert trade.is_open is True
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.open_rate == 11
|
||||
assert trade.stake_amount == 110
|
||||
|
||||
@@ -5999,7 +6002,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade
|
||||
assert trade.is_open is True
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.open_rate == 11
|
||||
assert trade.stake_amount == 110
|
||||
assert not trade.fee_updated('buy')
|
||||
@@ -6009,7 +6012,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade
|
||||
assert trade.is_open is True
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.open_rate == 11
|
||||
assert trade.stake_amount == 110
|
||||
assert not trade.fee_updated('buy')
|
||||
@@ -6037,7 +6040,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
|
||||
assert len(orders) == 2
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade
|
||||
assert trade.open_order_id == '651'
|
||||
assert '651' in trade.open_orders_ids
|
||||
assert trade.open_rate == 11
|
||||
assert trade.amount == 10
|
||||
assert trade.stake_amount == 110
|
||||
@@ -6074,7 +6077,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
|
||||
# Assert trade is as expected
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade
|
||||
assert trade.open_order_id == '651'
|
||||
assert '651' in trade.open_orders_ids
|
||||
assert trade.open_rate == 11
|
||||
assert trade.amount == 10
|
||||
assert trade.stake_amount == 110
|
||||
@@ -6111,7 +6114,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
|
||||
# Assert trade is as expected (averaged dca)
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert pytest.approx(trade.open_rate) == 9.90909090909
|
||||
assert trade.amount == 22
|
||||
assert pytest.approx(trade.stake_amount) == 218
|
||||
@@ -6153,7 +6156,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
|
||||
# Assert trade is as expected (averaged dca)
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert pytest.approx(trade.open_rate) == 8.729729729729
|
||||
assert trade.amount == 37
|
||||
assert trade.stake_amount == 323
|
||||
@@ -6191,7 +6194,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
|
||||
# Assert trade is as expected (averaged dca)
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.is_open
|
||||
assert trade.amount == 22
|
||||
assert trade.stake_amount == 192.05405405405406
|
||||
@@ -6268,7 +6271,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None:
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade
|
||||
assert trade.is_open is True
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.open_rate == bid
|
||||
assert trade.stake_amount == bid * amount
|
||||
|
||||
@@ -6278,7 +6281,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None:
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade
|
||||
assert trade.is_open is True
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.open_rate == bid
|
||||
assert trade.stake_amount == bid * amount
|
||||
assert not trade.fee_updated(trade.entry_side)
|
||||
@@ -6288,7 +6291,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None:
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade
|
||||
assert trade.is_open is True
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.open_rate == bid
|
||||
assert trade.stake_amount == bid * amount
|
||||
assert not trade.fee_updated(trade.entry_side)
|
||||
@@ -6323,7 +6326,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None:
|
||||
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.amount == 50
|
||||
assert trade.open_rate == 11
|
||||
assert trade.stake_amount == 550
|
||||
@@ -6365,7 +6368,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None:
|
||||
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.amount == 50
|
||||
assert trade.open_rate == 11
|
||||
assert trade.stake_amount == 550
|
||||
@@ -6465,7 +6468,7 @@ def test_position_adjust3(mocker, default_conf_usdt, fee, data) -> None:
|
||||
assert trade
|
||||
if idx < len(data) - 1:
|
||||
assert trade.is_open is True
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.amount == result[0]
|
||||
assert trade.open_rate == result[1]
|
||||
assert trade.stake_amount == result[2]
|
||||
@@ -6478,7 +6481,7 @@ def test_position_adjust3(mocker, default_conf_usdt, fee, data) -> None:
|
||||
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert trade.is_open is False
|
||||
|
||||
|
||||
|
||||
@@ -103,7 +103,6 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
||||
|
||||
trade.orders.append(oobj)
|
||||
trade.stoploss_order_id = f"stop{idx}"
|
||||
trade.open_order_id = None
|
||||
|
||||
n = freqtrade.exit_positions(trades)
|
||||
assert n == 2
|
||||
@@ -194,8 +193,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati
|
||||
|
||||
for trade in trades:
|
||||
assert pytest.approx(trade.stake_amount) == result1
|
||||
# Reset trade open order id's
|
||||
trade.open_order_id = None
|
||||
|
||||
trades = Trade.get_open_trades()
|
||||
assert len(trades) == 5
|
||||
bals = freqtrade.wallets.get_all_balances()
|
||||
@@ -386,7 +384,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
|
||||
assert len(Trade.get_trades().all()) == 1
|
||||
trade: Trade = Trade.get_trades().first()
|
||||
assert len(trade.orders) == 1
|
||||
assert trade.open_order_id is not None
|
||||
assert trade.has_open_orders
|
||||
assert pytest.approx(trade.stake_amount) == 60
|
||||
assert trade.open_rate == 1.96
|
||||
assert trade.stop_loss_pct == -0.1
|
||||
@@ -399,7 +397,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
assert len(trade.orders) == 1
|
||||
assert trade.open_order_id is not None
|
||||
assert trade.has_open_orders
|
||||
assert pytest.approx(trade.stake_amount) == 60
|
||||
|
||||
# Cancel order and place new one
|
||||
@@ -407,7 +405,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
assert len(trade.orders) == 2
|
||||
assert trade.open_order_id is not None
|
||||
assert trade.has_open_orders
|
||||
# Open rate is not adjusted yet
|
||||
assert trade.open_rate == 1.96
|
||||
assert trade.stop_loss_pct == -0.1
|
||||
@@ -421,7 +419,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
assert len(trade.orders) == 2
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
# Open rate is not adjusted yet
|
||||
assert trade.open_rate == 1.99
|
||||
assert pytest.approx(trade.stake_amount) == 60
|
||||
@@ -438,7 +436,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
assert len(trade.orders) == 3
|
||||
assert trade.open_order_id is not None
|
||||
assert trade.has_open_orders
|
||||
assert trade.open_rate == 1.99
|
||||
assert trade.orders[-1].price == 1.96
|
||||
assert trade.orders[-1].cost == 120 * leverage
|
||||
@@ -449,7 +447,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
assert len(trade.orders) == 4
|
||||
assert trade.open_order_id is not None
|
||||
assert trade.has_open_orders
|
||||
assert trade.open_rate == 1.99
|
||||
assert pytest.approx(trade.stake_amount) == 60
|
||||
assert trade.orders[-1].price == 1.95
|
||||
@@ -463,7 +461,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
assert len(trade.orders) == 4
|
||||
assert trade.open_order_id is None
|
||||
assert not trade.has_open_orders
|
||||
assert pytest.approx(trade.open_rate) == 1.963153456
|
||||
assert trade.orders[-1].price == 1.95
|
||||
assert pytest.approx(trade.orders[-1].cost) == 120 * leverage
|
||||
@@ -522,7 +520,11 @@ def test_dca_order_adjust_entry_replace_fails(
|
||||
freqtrade.enter_positions()
|
||||
|
||||
trades = Trade.session.scalars(
|
||||
select(Trade).filter(Trade.open_order_id.is_not(None))).all()
|
||||
select(Trade)
|
||||
.where(Order.ft_is_open.is_(True))
|
||||
.where(Order.ft_order_side != "stoploss")
|
||||
.where(Order.ft_trade_id == Trade.id)
|
||||
).all()
|
||||
assert len(trades) == 1
|
||||
|
||||
mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=False)
|
||||
@@ -539,14 +541,22 @@ def test_dca_order_adjust_entry_replace_fails(
|
||||
|
||||
assert freqtrade.strategy.adjust_trade_position.call_count == 1
|
||||
trades = Trade.session.scalars(
|
||||
select(Trade).filter(Trade.open_order_id.is_not(None))).all()
|
||||
select(Trade)
|
||||
.where(Order.ft_is_open.is_(True))
|
||||
.where(Order.ft_order_side != "stoploss")
|
||||
.where(Order.ft_trade_id == Trade.id)
|
||||
).all()
|
||||
assert len(trades) == 2
|
||||
|
||||
# We now have 2 orders open
|
||||
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=2.05)
|
||||
freqtrade.manage_open_orders()
|
||||
trades = Trade.session.scalars(
|
||||
select(Trade).filter(Trade.open_order_id.is_not(None))).all()
|
||||
select(Trade)
|
||||
.where(Order.ft_is_open.is_(True))
|
||||
.where(Order.ft_order_side != "stoploss")
|
||||
.where(Order.ft_trade_id == Trade.id)
|
||||
).all()
|
||||
assert len(trades) == 2
|
||||
assert len(Order.get_open_orders()) == 2
|
||||
# Entry adjustment is called
|
||||
|
||||
Reference in New Issue
Block a user