Merge pull request #8779 from Axel-CH/feature/multiple_open_orders

Feature: Multiple open orders
This commit is contained in:
Matthias
2023-09-10 17:22:26 +02:00
committed by GitHub
16 changed files with 408 additions and 358 deletions

View File

@@ -373,7 +373,10 @@ class FreqtradeBot(LoggingMixin):
"Order is older than 5 days. Assuming order was fully cancelled.")
fo = order.to_ccxt_object()
fo['status'] = 'canceled'
self.handle_cancel_order(fo, order.trade, constants.CANCEL_REASON['TIMEOUT'])
self.handle_cancel_order(
fo, order.order_id, order.trade,
constants.CANCEL_REASON['TIMEOUT']
)
except ExchangeError as e:
@@ -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)

View File

@@ -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.

View File

@@ -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%')

View File

@@ -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"]:

View File

@@ -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]

View File

@@ -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

View File

@@ -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'])

View File

@@ -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,

View File

@@ -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',

View File

@@ -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,

View File

@@ -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):
"""

View File

@@ -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

View File

@@ -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.*'):

View File

@@ -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': [],
}

View File

@@ -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

View File

@@ -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