mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-15 20:53:58 +00:00
Merge branch 'freqtrade:develop' into develop
This commit is contained in:
commit
c12adea655
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue
Block a user