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,
|
"ohlcv_candle_limit": 1000,
|
||||||
"l2_limit_range": [5, 10, 20],
|
"l2_limit_range": [5, 10, 20],
|
||||||
"l2_limit_range_required": False,
|
"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:
|
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
|
||||||
|
|
|
@ -492,10 +492,11 @@ class FreqtradeBot(LoggingMixin):
|
||||||
except ExchangeError:
|
except ExchangeError:
|
||||||
logger.warning(f"Error updating {order.order_id}.")
|
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.
|
Try refinding a order that is not in the database.
|
||||||
Only used balance disappeared, which would make exiting impossible.
|
Only used balance disappeared, which would make exiting impossible.
|
||||||
|
:return: True if the trade was deleted, False otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
orders = self.exchange.fetch_orders(
|
orders = self.exchange.fetch_orders(
|
||||||
|
@ -541,6 +542,19 @@ class FreqtradeBot(LoggingMixin):
|
||||||
trade.exit_reason = prev_exit_reason
|
trade.exit_reason = prev_exit_reason
|
||||||
total = self.wallets.get_total(trade.base_currency) if trade.base_currency else 0
|
total = self.wallets.get_total(trade.base_currency) if trade.base_currency else 0
|
||||||
if total < trade.amount:
|
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:
|
if total > trade.amount * 0.98:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"{trade} has a total of {trade.amount} {trade.base_currency}, "
|
f"{trade} has a total of {trade.amount} {trade.base_currency}, "
|
||||||
|
@ -566,6 +580,7 @@ class FreqtradeBot(LoggingMixin):
|
||||||
except Exception:
|
except Exception:
|
||||||
# catching https://github.com/freqtrade/freqtrade/issues/9025
|
# catching https://github.com/freqtrade/freqtrade/issues/9025
|
||||||
logger.warning("Error finding onexchange order", exc_info=True)
|
logger.warning("Error finding onexchange order", exc_info=True)
|
||||||
|
return False
|
||||||
|
|
||||||
#
|
#
|
||||||
# enter positions / open trades logic and methods
|
# enter positions / open trades logic and methods
|
||||||
|
@ -1007,7 +1022,13 @@ class FreqtradeBot(LoggingMixin):
|
||||||
|
|
||||||
# Update fees if order is non-opened
|
# Update fees if order is non-opened
|
||||||
if order_status in constants.NON_OPEN_EXCHANGE_STATES:
|
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
|
return True
|
||||||
|
|
||||||
|
@ -1229,7 +1250,9 @@ class FreqtradeBot(LoggingMixin):
|
||||||
f"Not enough {trade.safe_base_currency} in wallet to exit {trade}. "
|
f"Not enough {trade.safe_base_currency} in wallet to exit {trade}. "
|
||||||
"Trying to recover."
|
"Trying to recover."
|
||||||
)
|
)
|
||||||
self.handle_onexchange_order(trade)
|
if self.handle_onexchange_order(trade):
|
||||||
|
# Trade was deleted. Don't continue.
|
||||||
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -32,4 +32,6 @@ class ProfitDrawDownHyperOptLoss(IHyperOptLoss):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
relative_account_drawdown = 0
|
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:
|
def update_order(self, order: Dict) -> None:
|
||||||
Order.update_orders(self.orders, order)
|
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
|
Get amount of failed exiting orders
|
||||||
assumes full exits.
|
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:
|
def _calc_open_trade_value(self, amount: float, open_rate: float) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate the open_rate including open_fee.
|
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)
|
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])
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
def test_execute_entry_min_leverage(mocker, default_conf_usdt, fee, limit_order, is_short) -> None:
|
def test_execute_entry_min_leverage(mocker, default_conf_usdt, fee, limit_order, is_short) -> None:
|
||||||
default_conf_usdt["trading_mode"] = "futures"
|
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
|
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:
|
def test_get_valid_price(mocker, default_conf_usdt) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(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()
|
trade = Trade.get_trades([Trade.pair == "ETC/BTC"]).first()
|
||||||
# No canceled order.
|
# No canceled order.
|
||||||
assert trade.get_canceled_exit_order_count() == 0
|
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"
|
trade.orders[-1].status = "canceled"
|
||||||
assert trade.get_canceled_exit_order_count() == 1
|
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")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
|
Loading…
Reference in New Issue
Block a user