merge upstream
This commit is contained in:
@@ -135,6 +135,7 @@ def exchange_futures(request, exchange_conf, class_mocker):
|
||||
class_mocker.patch(
|
||||
'freqtrade.exchange.binance.Binance.fill_leverage_tiers')
|
||||
class_mocker.patch('freqtrade.exchange.exchange.Exchange.fetch_trading_fees')
|
||||
class_mocker.patch('freqtrade.exchange.okx.Okx.additional_exchange_init')
|
||||
exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=True)
|
||||
|
||||
yield exchange, request.param
|
||||
|
||||
@@ -150,6 +150,8 @@ def test_remove_credentials(default_conf, caplog) -> None:
|
||||
def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||
aei_mock = mocker.patch('freqtrade.exchange.Exchange.additional_exchange_init')
|
||||
|
||||
caplog.set_level(logging.INFO)
|
||||
conf = copy.deepcopy(default_conf)
|
||||
conf['exchange']['ccxt_async_config'] = {'aiohttp_trust_env': True, 'asyncio_loop': True}
|
||||
@@ -159,6 +161,7 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
||||
caplog)
|
||||
assert ex._api_async.aiohttp_trust_env
|
||||
assert not ex._api.aiohttp_trust_env
|
||||
assert aei_mock.call_count == 1
|
||||
|
||||
# Reset logging and config
|
||||
caplog.clear()
|
||||
@@ -4833,8 +4836,10 @@ def test__get_params(mocker, default_conf, exchange_name):
|
||||
|
||||
if exchange_name == 'okx':
|
||||
params2['tdMode'] = 'isolated'
|
||||
params2['posSide'] = 'net'
|
||||
|
||||
assert exchange._get_params(
|
||||
side="buy",
|
||||
ordertype='market',
|
||||
reduceOnly=False,
|
||||
time_in_force='gtc',
|
||||
@@ -4842,6 +4847,7 @@ def test__get_params(mocker, default_conf, exchange_name):
|
||||
) == params1
|
||||
|
||||
assert exchange._get_params(
|
||||
side="buy",
|
||||
ordertype='market',
|
||||
reduceOnly=False,
|
||||
time_in_force='ioc',
|
||||
@@ -4849,6 +4855,7 @@ def test__get_params(mocker, default_conf, exchange_name):
|
||||
) == params1
|
||||
|
||||
assert exchange._get_params(
|
||||
side="buy",
|
||||
ordertype='limit',
|
||||
reduceOnly=False,
|
||||
time_in_force='gtc',
|
||||
@@ -4861,6 +4868,7 @@ def test__get_params(mocker, default_conf, exchange_name):
|
||||
exchange._params = {'test': True}
|
||||
|
||||
assert exchange._get_params(
|
||||
side="buy",
|
||||
ordertype='limit',
|
||||
reduceOnly=True,
|
||||
time_in_force='ioc',
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.enums import MarginMode, TradingMode
|
||||
from tests.conftest import get_patched_exchange
|
||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
|
||||
|
||||
def test_get_maintenance_ratio_and_amt_okx(
|
||||
@@ -170,6 +173,70 @@ def test_get_max_pair_stake_amount_okx(default_conf, mocker, leverage_tiers):
|
||||
assert exchange.get_max_pair_stake_amount('TTT/USDT', 1.0) == float('inf') # Not in tiers
|
||||
|
||||
|
||||
@pytest.mark.parametrize('mode,side,reduceonly,result', [
|
||||
('net', 'buy', False, 'net'),
|
||||
('net', 'sell', True, 'net'),
|
||||
('net', 'sell', False, 'net'),
|
||||
('net', 'buy', True, 'net'),
|
||||
('longshort', 'buy', False, 'long'),
|
||||
('longshort', 'sell', True, 'long'),
|
||||
('longshort', 'sell', False, 'short'),
|
||||
('longshort', 'buy', True, 'short'),
|
||||
])
|
||||
def test__get_posSide(default_conf, mocker, mode, side, reduceonly, result):
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, id="okx")
|
||||
exchange.net_only = mode == 'net'
|
||||
assert exchange._get_posSide(side, reduceonly) == result
|
||||
|
||||
|
||||
def test_additional_exchange_init_okx(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
api_mock.fetch_accounts = MagicMock(return_value=[
|
||||
{'id': '2555',
|
||||
'type': '2',
|
||||
'currency': None,
|
||||
'info': {'acctLv': '2',
|
||||
'autoLoan': False,
|
||||
'ctIsoMode': 'automatic',
|
||||
'greeksType': 'PA',
|
||||
'level': 'Lv1',
|
||||
'levelTmp': '',
|
||||
'mgnIsoMode': 'automatic',
|
||||
'posMode': 'long_short_mode',
|
||||
'uid': '2555'}}])
|
||||
default_conf['dry_run'] = False
|
||||
exchange = get_patched_exchange(mocker, default_conf, id="okx", api_mock=api_mock)
|
||||
assert api_mock.fetch_accounts.call_count == 0
|
||||
exchange.trading_mode = TradingMode.FUTURES
|
||||
# Default to netOnly
|
||||
assert exchange.net_only
|
||||
exchange.additional_exchange_init()
|
||||
assert api_mock.fetch_accounts.call_count == 1
|
||||
assert not exchange.net_only
|
||||
|
||||
api_mock.fetch_accounts = MagicMock(return_value=[
|
||||
{'id': '2555',
|
||||
'type': '2',
|
||||
'currency': None,
|
||||
'info': {'acctLv': '2',
|
||||
'autoLoan': False,
|
||||
'ctIsoMode': 'automatic',
|
||||
'greeksType': 'PA',
|
||||
'level': 'Lv1',
|
||||
'levelTmp': '',
|
||||
'mgnIsoMode': 'automatic',
|
||||
'posMode': 'net_mode',
|
||||
'uid': '2555'}}])
|
||||
exchange.additional_exchange_init()
|
||||
assert api_mock.fetch_accounts.call_count == 1
|
||||
assert exchange.net_only
|
||||
default_conf['trading_mode'] = 'futures'
|
||||
default_conf['margin_mode'] = 'isolated'
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'okx',
|
||||
"additional_exchange_init", "fetch_accounts")
|
||||
|
||||
|
||||
def test_load_leverage_tiers_okx(default_conf, mocker, markets):
|
||||
api_mock = MagicMock()
|
||||
type(api_mock).has = PropertyMock(return_value={
|
||||
|
||||
@@ -40,6 +40,8 @@ class BTContainer(NamedTuple):
|
||||
custom_entry_price: Optional[float] = None
|
||||
custom_exit_price: Optional[float] = None
|
||||
leverage: float = 1.0
|
||||
timeout: Optional[int] = None
|
||||
adjust_entry_price: Optional[float] = None
|
||||
|
||||
|
||||
def _get_frame_time_from_offset(offset):
|
||||
|
||||
@@ -754,6 +754,62 @@ tc47 = BTContainer(data=[
|
||||
trades=[]
|
||||
)
|
||||
|
||||
# Test 48: Custom-entry-price below all candles - readjust order
|
||||
tc48 = BTContainer(data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||
[1, 5000, 5500, 4951, 5000, 6172, 0, 0], # timeout
|
||||
[2, 4900, 5250, 4500, 5100, 6172, 0, 0], # Order readjust
|
||||
[3, 5100, 5100, 4650, 4750, 6172, 0, 1],
|
||||
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.087,
|
||||
use_exit_signal=True, timeout=1000,
|
||||
custom_entry_price=4200, adjust_entry_price=5200,
|
||||
trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=False)]
|
||||
)
|
||||
|
||||
|
||||
# Test 49: Custom-entry-price short above all candles - readjust order
|
||||
tc49 = BTContainer(data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
[0, 5000, 5050, 4950, 5000, 6172, 0, 0, 1, 0],
|
||||
[1, 5000, 5200, 4951, 5000, 6172, 0, 0, 0, 0], # timeout
|
||||
[2, 4900, 5250, 4900, 5100, 6172, 0, 0, 0, 0], # Order readjust
|
||||
[3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 1],
|
||||
[4, 4750, 4950, 4350, 4750, 6172, 0, 0, 0, 0]],
|
||||
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=0.05,
|
||||
use_exit_signal=True, timeout=1000,
|
||||
custom_entry_price=5300, adjust_entry_price=5000,
|
||||
trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=True)]
|
||||
)
|
||||
|
||||
# Test 50: Custom-entry-price below all candles - readjust order cancels order
|
||||
tc50 = BTContainer(data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0], # Enter long - place order
|
||||
[1, 5000, 5500, 4951, 5000, 6172, 0, 0], # Order readjust - cancel order
|
||||
[2, 4900, 5250, 4500, 5100, 6172, 0, 0],
|
||||
[3, 5100, 5100, 4650, 4750, 6172, 0, 0],
|
||||
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=0.0,
|
||||
use_exit_signal=True, timeout=1000,
|
||||
custom_entry_price=4200, adjust_entry_price=None,
|
||||
trades=[]
|
||||
)
|
||||
|
||||
# Test 51: Custom-entry-price below all candles - readjust order leaves order in place and timeout.
|
||||
tc51 = BTContainer(data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0], # Enter long - place order
|
||||
[1, 5000, 5500, 4951, 5000, 6172, 0, 0], # Order readjust - replace order
|
||||
[2, 4900, 5250, 4500, 5100, 6172, 0, 0], # Order readjust - maintain order
|
||||
[3, 5100, 5100, 4650, 4750, 6172, 0, 0], # Timeout
|
||||
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=0.0,
|
||||
use_exit_signal=True, timeout=60,
|
||||
custom_entry_price=4200, adjust_entry_price=4100,
|
||||
trades=[]
|
||||
)
|
||||
|
||||
TESTS = [
|
||||
tc0,
|
||||
@@ -804,6 +860,10 @@ TESTS = [
|
||||
tc45,
|
||||
tc46,
|
||||
tc47,
|
||||
tc48,
|
||||
tc49,
|
||||
tc50,
|
||||
tc51,
|
||||
]
|
||||
|
||||
|
||||
@@ -817,6 +877,11 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer)
|
||||
default_conf["timeframe"] = tests_timeframe
|
||||
default_conf["trailing_stop"] = data.trailing_stop
|
||||
default_conf["trailing_only_offset_is_reached"] = data.trailing_only_offset_is_reached
|
||||
if data.timeout:
|
||||
default_conf['unfilledtimeout'].update({
|
||||
'entry': data.timeout,
|
||||
'exit': data.timeout,
|
||||
})
|
||||
# Only add this to configuration If it's necessary
|
||||
if data.trailing_stop_positive is not None:
|
||||
default_conf["trailing_stop_positive"] = data.trailing_stop_positive
|
||||
@@ -840,6 +905,8 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer)
|
||||
backtesting.strategy.custom_entry_price = MagicMock(return_value=data.custom_entry_price)
|
||||
if data.custom_exit_price:
|
||||
backtesting.strategy.custom_exit_price = MagicMock(return_value=data.custom_exit_price)
|
||||
backtesting.strategy.adjust_entry_price = MagicMock(return_value=data.adjust_entry_price)
|
||||
|
||||
backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss
|
||||
backtesting.strategy.leverage = lambda **kwargs: data.leverage
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
@@ -250,14 +250,16 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog):
|
||||
assert not PairLocks.is_global_lock()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('only_per_side', [False, True])
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_LowProfitPairs(mocker, default_conf, fee, caplog):
|
||||
def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side):
|
||||
default_conf['protections'] = [{
|
||||
"method": "LowProfitPairs",
|
||||
"lookback_period": 400,
|
||||
"stop_duration": 60,
|
||||
"trade_limit": 2,
|
||||
"required_profit": 0.0,
|
||||
"only_per_side": only_per_side,
|
||||
}]
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
message = r"Trading stopped due to .*"
|
||||
@@ -292,10 +294,11 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog):
|
||||
# Add positive trade
|
||||
Trade.query.session.add(generate_mock_trade(
|
||||
'XRP/BTC', fee.return_value, False, exit_reason=ExitType.ROI.value,
|
||||
min_ago_open=20, min_ago_close=10, profit_rate=1.15,
|
||||
min_ago_open=20, min_ago_close=10, profit_rate=1.15, is_short=True
|
||||
))
|
||||
assert not freqtrade.protections.stop_per_pair('XRP/BTC')
|
||||
assert not PairLocks.is_pair_locked('XRP/BTC')
|
||||
assert freqtrade.protections.stop_per_pair('XRP/BTC') != only_per_side
|
||||
assert not PairLocks.is_pair_locked('XRP/BTC', side='*')
|
||||
assert PairLocks.is_pair_locked('XRP/BTC', side='long') == only_per_side
|
||||
|
||||
Trade.query.session.add(generate_mock_trade(
|
||||
'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value,
|
||||
@@ -303,9 +306,10 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog):
|
||||
))
|
||||
|
||||
# Locks due to 2nd trade
|
||||
assert not freqtrade.protections.global_stop()
|
||||
assert freqtrade.protections.stop_per_pair('XRP/BTC')
|
||||
assert PairLocks.is_pair_locked('XRP/BTC')
|
||||
assert freqtrade.protections.global_stop() != only_per_side
|
||||
assert freqtrade.protections.stop_per_pair('XRP/BTC') != only_per_side
|
||||
assert PairLocks.is_pair_locked('XRP/BTC', side='long')
|
||||
assert PairLocks.is_pair_locked('XRP/BTC', side='*') != only_per_side
|
||||
assert not PairLocks.is_global_lock()
|
||||
|
||||
|
||||
|
||||
@@ -2364,7 +2364,7 @@ def test_bot_loop_start_called_once(mocker, default_conf_usdt, caplog):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
def test_check_handle_timedout_entry_usercustom(
|
||||
def test_manage_open_orders_entry_usercustom(
|
||||
default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade,
|
||||
limit_sell_order_old, fee, mocker, is_short
|
||||
) -> None:
|
||||
@@ -2396,12 +2396,12 @@ def test_check_handle_timedout_entry_usercustom(
|
||||
Trade.query.session.add(open_trade)
|
||||
|
||||
# Ensure default is to return empty (so not mocked yet)
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
assert cancel_order_mock.call_count == 0
|
||||
|
||||
# Return false - trade remains open
|
||||
freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False)
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
assert cancel_order_mock.call_count == 0
|
||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||
nb_trades = len(trades)
|
||||
@@ -2409,7 +2409,7 @@ def test_check_handle_timedout_entry_usercustom(
|
||||
assert freqtrade.strategy.check_entry_timeout.call_count == 1
|
||||
freqtrade.strategy.check_entry_timeout = MagicMock(side_effect=KeyError)
|
||||
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
assert cancel_order_mock.call_count == 0
|
||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||
nb_trades = len(trades)
|
||||
@@ -2418,7 +2418,7 @@ def test_check_handle_timedout_entry_usercustom(
|
||||
freqtrade.strategy.check_entry_timeout = MagicMock(return_value=True)
|
||||
|
||||
# Trade should be closed since the function returns true
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
assert cancel_order_wr_mock.call_count == 1
|
||||
assert rpc_mock.call_count == 1
|
||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||
@@ -2428,7 +2428,7 @@ def test_check_handle_timedout_entry_usercustom(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
def test_check_handle_timedout_entry(
|
||||
def test_manage_open_orders_entry(
|
||||
default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade,
|
||||
limit_sell_order_old, fee, mocker, is_short
|
||||
) -> None:
|
||||
@@ -2454,8 +2454,9 @@ def test_check_handle_timedout_entry(
|
||||
Trade.query.session.add(open_trade)
|
||||
|
||||
freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False)
|
||||
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1234)
|
||||
# check it does cancel buy orders over the time limit
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert rpc_mock.call_count == 1
|
||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||
@@ -2463,6 +2464,99 @@ def test_check_handle_timedout_entry(
|
||||
assert nb_trades == 0
|
||||
# Custom user buy-timeout is never called
|
||||
assert freqtrade.strategy.check_entry_timeout.call_count == 0
|
||||
# Entry adjustment is never called
|
||||
assert freqtrade.strategy.adjust_entry_price.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
def test_adjust_entry_cancel(
|
||||
default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade,
|
||||
limit_sell_order_old, fee, mocker, caplog, is_short
|
||||
) -> 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
|
||||
limit_buy_cancel = deepcopy(old_order)
|
||||
limit_buy_cancel['status'] = 'canceled'
|
||||
cancel_order_mock = MagicMock(return_value=limit_buy_cancel)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker_usdt,
|
||||
fetch_order=MagicMock(return_value=old_order),
|
||||
cancel_order_with_result=cancel_order_mock,
|
||||
get_fee=fee
|
||||
)
|
||||
|
||||
open_trade.is_short = is_short
|
||||
Trade.query.session.add(open_trade)
|
||||
|
||||
# Timeout to not interfere
|
||||
freqtrade.strategy.ft_check_timed_out = MagicMock(return_value=False)
|
||||
|
||||
# check that order is cancelled
|
||||
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=None)
|
||||
freqtrade.manage_open_orders()
|
||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||
assert len(trades) == 0
|
||||
assert len(Order.query.all()) == 0
|
||||
assert log_has_re(
|
||||
f"{'Sell' if is_short else 'Buy'} order user requested order cancel*", caplog)
|
||||
assert log_has_re(
|
||||
f"{'Sell' if is_short else 'Buy'} order fully cancelled.*", caplog)
|
||||
|
||||
# Entry adjustment is called
|
||||
assert freqtrade.strategy.adjust_entry_price.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
def test_adjust_entry_maintain_replace(
|
||||
default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade,
|
||||
limit_sell_order_old, fee, mocker, caplog, is_short
|
||||
) -> 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
|
||||
limit_buy_cancel = deepcopy(old_order)
|
||||
limit_buy_cancel['status'] = 'canceled'
|
||||
cancel_order_mock = MagicMock(return_value=limit_buy_cancel)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker_usdt,
|
||||
fetch_order=MagicMock(return_value=old_order),
|
||||
cancel_order_with_result=cancel_order_mock,
|
||||
get_fee=fee
|
||||
)
|
||||
|
||||
open_trade.is_short = is_short
|
||||
Trade.query.session.add(open_trade)
|
||||
|
||||
# Timeout to not interfere
|
||||
freqtrade.strategy.ft_check_timed_out = MagicMock(return_value=False)
|
||||
|
||||
# Check that order is maintained
|
||||
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=old_order['price'])
|
||||
freqtrade.manage_open_orders()
|
||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||
assert len(trades) == 1
|
||||
assert len(Order.get_open_orders()) == 1
|
||||
# Entry adjustment is called
|
||||
assert freqtrade.strategy.adjust_entry_price.call_count == 1
|
||||
|
||||
# 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()
|
||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||
assert len(trades) == 1
|
||||
nb_all_orders = len(Order.query.all())
|
||||
assert nb_all_orders == 2
|
||||
# New order seems to be in closed status?
|
||||
# nb_open_orders = len(Order.get_open_orders())
|
||||
# assert nb_open_orders == 1
|
||||
assert log_has_re(
|
||||
f"{'Sell' if is_short else 'Buy'} order cancelled to be replaced*", caplog)
|
||||
# Entry adjustment is called
|
||||
assert freqtrade.strategy.adjust_entry_price.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
@@ -2488,18 +2582,17 @@ def test_check_handle_cancelled_buy(
|
||||
Trade.query.session.add(open_trade)
|
||||
|
||||
# check it does cancel buy orders over the time limit
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
assert cancel_order_mock.call_count == 0
|
||||
assert rpc_mock.call_count == 1
|
||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||
nb_trades = len(trades)
|
||||
assert nb_trades == 0
|
||||
assert len(trades) == 0
|
||||
assert log_has_re(
|
||||
f"{'Sell' if is_short else 'Buy'} order cancelled on exchange for Trade.*", caplog)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
def test_check_handle_timedout_buy_exception(
|
||||
def test_manage_open_orders_buy_exception(
|
||||
default_conf_usdt, ticker_usdt, open_trade, is_short, fee, mocker
|
||||
) -> None:
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
@@ -2519,7 +2612,7 @@ def test_check_handle_timedout_buy_exception(
|
||||
Trade.query.session.add(open_trade)
|
||||
|
||||
# check it does cancel buy orders over the time limit
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
assert cancel_order_mock.call_count == 0
|
||||
assert rpc_mock.call_count == 0
|
||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||
@@ -2528,7 +2621,7 @@ def test_check_handle_timedout_buy_exception(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
def test_check_handle_timedout_exit_usercustom(
|
||||
def test_manage_open_orders_exit_usercustom(
|
||||
default_conf_usdt, ticker_usdt, limit_sell_order_old, mocker,
|
||||
is_short, open_trade_usdt, caplog
|
||||
) -> None:
|
||||
@@ -2559,13 +2652,13 @@ def test_check_handle_timedout_exit_usercustom(
|
||||
|
||||
Trade.query.session.add(open_trade_usdt)
|
||||
# Ensure default is false
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
assert cancel_order_mock.call_count == 0
|
||||
|
||||
freqtrade.strategy.check_exit_timeout = MagicMock(return_value=False)
|
||||
freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False)
|
||||
# Return false - No impact
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
assert cancel_order_mock.call_count == 0
|
||||
assert rpc_mock.call_count == 0
|
||||
assert open_trade_usdt.is_open is False
|
||||
@@ -2575,7 +2668,7 @@ def test_check_handle_timedout_exit_usercustom(
|
||||
freqtrade.strategy.check_exit_timeout = MagicMock(side_effect=KeyError)
|
||||
freqtrade.strategy.check_entry_timeout = MagicMock(side_effect=KeyError)
|
||||
# Return Error - No impact
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
assert cancel_order_mock.call_count == 0
|
||||
assert rpc_mock.call_count == 0
|
||||
assert open_trade_usdt.is_open is False
|
||||
@@ -2585,7 +2678,7 @@ def test_check_handle_timedout_exit_usercustom(
|
||||
# Return True - sells!
|
||||
freqtrade.strategy.check_exit_timeout = MagicMock(return_value=True)
|
||||
freqtrade.strategy.check_entry_timeout = MagicMock(return_value=True)
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert rpc_mock.call_count == 1
|
||||
assert open_trade_usdt.is_open is True
|
||||
@@ -2598,7 +2691,7 @@ def test_check_handle_timedout_exit_usercustom(
|
||||
mocker.patch('freqtrade.persistence.Trade.get_exit_order_count', return_value=1)
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit',
|
||||
side_effect=DependencyException)
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
assert log_has_re('Unable to emergency sell .*', caplog)
|
||||
|
||||
et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit')
|
||||
@@ -2608,16 +2701,16 @@ def test_check_handle_timedout_exit_usercustom(
|
||||
|
||||
# If cancelling fails - no emergency sell!
|
||||
with patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit', return_value=False):
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
assert et_mock.call_count == 0
|
||||
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
assert log_has_re('Emergency exiting trade.*', caplog)
|
||||
assert et_mock.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
def test_check_handle_timedout_exit(
|
||||
def test_manage_open_orders_exit(
|
||||
default_conf_usdt, ticker_usdt, limit_sell_order_old, mocker, is_short, open_trade_usdt
|
||||
) -> None:
|
||||
rpc_mock = patch_RPCManager(mocker)
|
||||
@@ -2644,7 +2737,7 @@ def test_check_handle_timedout_exit(
|
||||
freqtrade.strategy.check_exit_timeout = MagicMock(return_value=False)
|
||||
freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False)
|
||||
# check it does cancel sell orders over the time limit
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert rpc_mock.call_count == 1
|
||||
assert open_trade_usdt.is_open is True
|
||||
@@ -2680,7 +2773,7 @@ def test_check_handle_cancelled_exit(
|
||||
Trade.query.session.add(open_trade_usdt)
|
||||
|
||||
# check it does cancel sell orders over the time limit
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
assert cancel_order_mock.call_count == 0
|
||||
assert rpc_mock.call_count == 1
|
||||
assert open_trade_usdt.is_open is True
|
||||
@@ -2690,7 +2783,7 @@ def test_check_handle_cancelled_exit(
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
@pytest.mark.parametrize("leverage", [1, 3, 5, 10])
|
||||
def test_check_handle_timedout_partial(
|
||||
def test_manage_open_orders_partial(
|
||||
default_conf_usdt, ticker_usdt, limit_buy_order_old_partial, is_short, leverage,
|
||||
open_trade, mocker
|
||||
) -> None:
|
||||
@@ -2716,7 +2809,7 @@ def test_check_handle_timedout_partial(
|
||||
|
||||
# check it does cancel buy orders over the time limit
|
||||
# note this is for a partially-complete buy order
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert rpc_mock.call_count == 2
|
||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||
@@ -2727,7 +2820,7 @@ def test_check_handle_timedout_partial(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
def test_check_handle_timedout_partial_fee(
|
||||
def test_manage_open_orders_partial_fee(
|
||||
default_conf_usdt, ticker_usdt, open_trade, caplog, fee, is_short,
|
||||
limit_buy_order_old_partial, trades_for_order,
|
||||
limit_buy_order_old_partial_canceled, mocker
|
||||
@@ -2759,7 +2852,7 @@ def test_check_handle_timedout_partial_fee(
|
||||
Trade.query.session.add(open_trade)
|
||||
# cancelling a half-filled order should update the amount to the bought amount
|
||||
# and apply fees if necessary.
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
|
||||
assert log_has_re(r"Applying fee on amount for Trade.*", caplog)
|
||||
|
||||
@@ -2776,7 +2869,7 @@ def test_check_handle_timedout_partial_fee(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
def test_check_handle_timedout_partial_except(
|
||||
def test_manage_open_orders_partial_except(
|
||||
default_conf_usdt, ticker_usdt, open_trade, caplog, fee, is_short,
|
||||
limit_buy_order_old_partial, trades_for_order,
|
||||
limit_buy_order_old_partial_canceled, mocker
|
||||
@@ -2807,7 +2900,7 @@ def test_check_handle_timedout_partial_except(
|
||||
Trade.query.session.add(open_trade)
|
||||
# cancelling a half-filled order should update the amount to the bought amount
|
||||
# and apply fees if necessary.
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
|
||||
assert log_has_re(r"Could not update trade amount: .*", caplog)
|
||||
|
||||
@@ -2823,8 +2916,8 @@ def test_check_handle_timedout_partial_except(
|
||||
assert trades[0].fee_open == fee()
|
||||
|
||||
|
||||
def test_check_handle_timedout_exception(default_conf_usdt, ticker_usdt, open_trade_usdt, mocker,
|
||||
caplog) -> None:
|
||||
def test_manage_open_orders_exception(default_conf_usdt, ticker_usdt, open_trade_usdt, mocker,
|
||||
caplog) -> None:
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
cancel_order_mock = MagicMock()
|
||||
@@ -2845,7 +2938,7 @@ def test_check_handle_timedout_exception(default_conf_usdt, ticker_usdt, open_tr
|
||||
Trade.query.session.add(open_trade_usdt)
|
||||
|
||||
caplog.clear()
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
assert log_has_re(r"Cannot query order for Trade\(id=1, pair=ADA/USDT, amount=30.00000000, "
|
||||
r"is_short=False, leverage=1.0, "
|
||||
r"open_rate=2.00000000, open_since="
|
||||
@@ -3411,7 +3504,7 @@ def test_execute_trade_exit_with_stoploss_on_exchange(
|
||||
assert trade
|
||||
trades = [trade]
|
||||
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
freqtrade.exit_positions(trades)
|
||||
|
||||
# Increase the price and sell it
|
||||
@@ -3463,7 +3556,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
|
||||
|
||||
# Create some test data
|
||||
freqtrade.enter_positions()
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
trade = Trade.query.first()
|
||||
trades = [trade]
|
||||
assert trade.stoploss_order_id is None
|
||||
@@ -5233,7 +5326,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
|
||||
assert trade.stake_amount == 110
|
||||
assert not trade.fee_updated('buy')
|
||||
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
@@ -5339,7 +5432,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
|
||||
MagicMock(return_value=closed_dca_order_1))
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
|
||||
MagicMock(return_value=closed_dca_order_1))
|
||||
freqtrade.check_handle_timedout()
|
||||
freqtrade.manage_open_orders()
|
||||
|
||||
# Assert trade is as expected (averaged dca)
|
||||
trade = Trade.query.first()
|
||||
|
||||
@@ -351,3 +351,95 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
|
||||
|
||||
assert trade.nr_of_successful_entries == 2
|
||||
assert trade.nr_of_successful_exits == 1
|
||||
|
||||
|
||||
def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
|
||||
default_conf_usdt['position_adjustment_enable'] = True
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker_usdt,
|
||||
get_fee=fee,
|
||||
amount_to_precision=lambda s, x, y: y,
|
||||
price_to_precision=lambda s, x, y: y,
|
||||
)
|
||||
mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=False)
|
||||
|
||||
patch_get_signal(freqtrade)
|
||||
freqtrade.strategy.custom_entry_price = lambda **kwargs: ticker_usdt['ask'] * 0.96
|
||||
|
||||
freqtrade.enter_positions()
|
||||
|
||||
assert len(Trade.get_trades().all()) == 1
|
||||
trade = Trade.get_trades().first()
|
||||
assert len(trade.orders) == 1
|
||||
assert trade.open_order_id is not None
|
||||
assert pytest.approx(trade.stake_amount) == 60
|
||||
assert trade.open_rate == 1.96
|
||||
# No adjustment
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
assert len(trade.orders) == 1
|
||||
assert trade.open_order_id is not None
|
||||
assert pytest.approx(trade.stake_amount) == 60
|
||||
|
||||
# Cancel order and place new one
|
||||
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1.99)
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
assert len(trade.orders) == 2
|
||||
assert trade.open_order_id is not None
|
||||
# Open rate is not adjusted yet
|
||||
assert trade.open_rate == 1.96
|
||||
|
||||
# Fill order
|
||||
mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=True)
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
assert len(trade.orders) == 2
|
||||
assert trade.open_order_id is None
|
||||
# Open rate is not adjusted yet
|
||||
assert trade.open_rate == 1.99
|
||||
|
||||
# 2nd order - not filling
|
||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=120)
|
||||
mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=False)
|
||||
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
assert len(trade.orders) == 3
|
||||
assert trade.open_order_id is not None
|
||||
assert trade.open_rate == 1.99
|
||||
assert trade.orders[-1].price == 1.96
|
||||
assert trade.orders[-1].cost == 120
|
||||
|
||||
# Replace new order with diff. order at a lower price
|
||||
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1.95)
|
||||
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
assert len(trade.orders) == 4
|
||||
assert trade.open_order_id is not None
|
||||
assert trade.open_rate == 1.99
|
||||
assert trade.orders[-1].price == 1.95
|
||||
assert pytest.approx(trade.orders[-1].cost) == 120
|
||||
|
||||
# Fill DCA order
|
||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=None)
|
||||
mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=True)
|
||||
freqtrade.strategy.adjust_entry_price = MagicMock(side_effect=ValueError)
|
||||
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
assert len(trade.orders) == 4
|
||||
assert trade.open_order_id is None
|
||||
assert pytest.approx(trade.open_rate) == 1.963153456
|
||||
assert trade.orders[-1].price == 1.95
|
||||
assert pytest.approx(trade.orders[-1].cost) == 120
|
||||
assert trade.orders[-1].status == 'closed'
|
||||
|
||||
assert pytest.approx(trade.amount) == 91.689215
|
||||
# Check the 2 filled orders equal the above amount
|
||||
assert pytest.approx(trade.orders[1].amount) == 30.150753768
|
||||
assert pytest.approx(trade.orders[-1].amount) == 61.538461232
|
||||
|
||||
Reference in New Issue
Block a user