Merge branch 'freqtrade:develop' into develop

This commit is contained in:
Simon Waiblinger 2024-05-26 15:59:29 +02:00 committed by GitHub
commit c12adea655
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 145 additions and 5 deletions

View File

@ -24,6 +24,10 @@ class Htx(Exchange):
"ohlcv_candle_limit": 1000,
"l2_limit_range": [5, 10, 20],
"l2_limit_range_required": False,
"ohlcv_candle_limit_per_timeframe": {
"1w": 500,
"1M": 500,
},
}
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:

View File

@ -492,10 +492,11 @@ class FreqtradeBot(LoggingMixin):
except ExchangeError:
logger.warning(f"Error updating {order.order_id}.")
def handle_onexchange_order(self, trade: Trade):
def handle_onexchange_order(self, trade: Trade) -> bool:
"""
Try refinding a order that is not in the database.
Only used balance disappeared, which would make exiting impossible.
:return: True if the trade was deleted, False otherwise
"""
try:
orders = self.exchange.fetch_orders(
@ -541,6 +542,19 @@ class FreqtradeBot(LoggingMixin):
trade.exit_reason = prev_exit_reason
total = self.wallets.get_total(trade.base_currency) if trade.base_currency else 0
if total < trade.amount:
if trade.fully_canceled_entry_order_count == len(trade.orders):
logger.warning(
f"Trade only had fully canceled entry orders. "
f"Removing {trade} from database."
)
self._notify_enter_cancel(
trade,
order_type=self.strategy.order_types["entry"],
reason=constants.CANCEL_REASON["FULLY_CANCELLED"],
)
trade.delete()
return True
if total > trade.amount * 0.98:
logger.warning(
f"{trade} has a total of {trade.amount} {trade.base_currency}, "
@ -566,6 +580,7 @@ class FreqtradeBot(LoggingMixin):
except Exception:
# catching https://github.com/freqtrade/freqtrade/issues/9025
logger.warning("Error finding onexchange order", exc_info=True)
return False
#
# enter positions / open trades logic and methods
@ -1007,7 +1022,13 @@ class FreqtradeBot(LoggingMixin):
# Update fees if order is non-opened
if order_status in constants.NON_OPEN_EXCHANGE_STATES:
self.update_trade_state(trade, order_id, order)
fully_canceled = self.update_trade_state(trade, order_id, order)
if fully_canceled and mode != "replace":
# Fully canceled orders, may happen with some time in force setups (IOC).
# Should be handled immediately.
self.handle_cancel_enter(
trade, order, order_obj, constants.CANCEL_REASON["TIMEOUT"]
)
return True
@ -1229,7 +1250,9 @@ class FreqtradeBot(LoggingMixin):
f"Not enough {trade.safe_base_currency} in wallet to exit {trade}. "
"Trying to recover."
)
self.handle_onexchange_order(trade)
if self.handle_onexchange_order(trade):
# Trade was deleted. Don't continue.
continue
try:
try:

View File

@ -32,4 +32,6 @@ class ProfitDrawDownHyperOptLoss(IHyperOptLoss):
except ValueError:
relative_account_drawdown = 0
return -1 * (total_profit - (relative_account_drawdown * total_profit) / DRAWDOWN_MULT)
return -1 * (
total_profit - (relative_account_drawdown * total_profit) * (1 - DRAWDOWN_MULT)
)

View File

@ -957,7 +957,24 @@ class LocalTrade:
def update_order(self, order: Dict) -> None:
Order.update_orders(self.orders, order)
def get_canceled_exit_order_count(self) -> int:
@property
def fully_canceled_entry_order_count(self) -> int:
"""
Get amount of failed exiting orders
assumes full exits.
"""
return len(
[
o
for o in self.orders
if o.ft_order_side == self.entry_side
and o.status in CANCELED_EXCHANGE_STATES
and o.filled == 0
]
)
@property
def canceled_exit_order_count(self) -> int:
"""
Get amount of failed exiting orders
assumes full exits.
@ -970,6 +987,13 @@ class LocalTrade:
]
)
def get_canceled_exit_order_count(self) -> int:
"""
Get amount of failed exiting orders
assumes full exits.
"""
return self.canceled_exit_order_count
def _calc_open_trade_value(self, amount: float, open_rate: float) -> float:
"""
Calculate the open_rate including open_fee.

View File

@ -1146,6 +1146,36 @@ def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_order
assert not freqtrade.execute_entry(pair, stake_amount)
@pytest.mark.parametrize("is_short", [False, True])
def test_execute_entry_fully_canceled_on_create(
mocker, default_conf_usdt, fee, limit_order_open, is_short
) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mock_hce = mocker.spy(freqtrade, "handle_cancel_enter")
order = limit_order_open[entry_side(is_short)]
pair = "ETH/USDT"
order["symbol"] = pair
order["status"] = "canceled"
order["filled"] = 0.0
mocker.patch.multiple(
EXMS,
fetch_ticker=MagicMock(return_value={"bid": 1.9, "ask": 2.2, "last": 1.9}),
create_order=MagicMock(return_value=order),
get_rate=MagicMock(return_value=0.11),
get_min_pair_stake_amount=MagicMock(return_value=1),
get_fee=fee,
)
stake_amount = 2
assert freqtrade.execute_entry(pair, stake_amount)
assert mock_hce.call_count == 1
# an order that immediately cancels completely should delete the order.
trades = Trade.get_trades().all()
assert len(trades) == 0
@pytest.mark.parametrize("is_short", [False, True])
def test_execute_entry_min_leverage(mocker, default_conf_usdt, fee, limit_order, is_short) -> None:
default_conf_usdt["trading_mode"] = "futures"
@ -4978,6 +5008,47 @@ def test_handle_onexchange_order_exit(mocker, default_conf_usdt, limit_order, is
assert trade.amount == 5.0
@pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize("is_short", [False, True])
def test_handle_onexchange_order_fully_canceled_enter(
mocker, default_conf_usdt, limit_order, is_short, caplog
):
default_conf_usdt["dry_run"] = False
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
entry_order = limit_order[entry_side(is_short)]
entry_order["status"] = "canceled"
entry_order["filled"] = 0.0
mock_fo = mocker.patch(
f"{EXMS}.fetch_orders",
return_value=[
entry_order,
],
)
mocker.patch(f"{EXMS}.get_rate", return_value=entry_order["price"])
trade = Trade(
pair="ETH/USDT",
fee_open=0.001,
fee_close=0.001,
open_rate=entry_order["price"],
open_date=dt_now(),
stake_amount=entry_order["cost"],
amount=entry_order["amount"],
exchange="binance",
is_short=is_short,
leverage=1,
)
trade.orders.append(Order.parse_from_ccxt_object(entry_order, "ADA/USDT", entry_side(is_short)))
Trade.session.add(trade)
assert freqtrade.handle_onexchange_order(trade) is True
assert log_has_re(r"Trade only had fully canceled entry orders\. .*", caplog)
assert mock_fo.call_count == 1
trades = Trade.get_trades().all()
assert len(trades) == 0
def test_get_valid_price(mocker, default_conf_usdt) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)

View File

@ -1961,9 +1961,25 @@ def test_get_canceled_exit_order_count(fee, is_short):
trade = Trade.get_trades([Trade.pair == "ETC/BTC"]).first()
# No canceled order.
assert trade.get_canceled_exit_order_count() == 0
# Property returns the same result
assert trade.canceled_exit_order_count == 0
trade.orders[-1].status = "canceled"
assert trade.get_canceled_exit_order_count() == 1
assert trade.canceled_exit_order_count == 1
@pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize("is_short", [True, False])
def test_fully_canceled_entry_order_count(fee, is_short):
create_mock_trades(fee, is_short=is_short)
trade = Trade.get_trades([Trade.pair == "ETC/BTC"]).first()
# No canceled order.
assert trade.fully_canceled_entry_order_count == 0
trade.orders[0].status = "canceled"
trade.orders[0].filled = 0
assert trade.fully_canceled_entry_order_count == 1
@pytest.mark.usefixtures("init_persistence")