Merge branch 'develop' into feat/short

This commit is contained in:
Sam Germain
2022-03-03 13:51:52 -06:00
39 changed files with 604 additions and 210 deletions

View File

@@ -82,6 +82,12 @@ EXCHANGES = {
'leverage_tiers_public': True,
'leverage_in_spot_market': True,
},
'huobi': {
'pair': 'BTC/USDT',
'stake_currency': 'USDT',
'hasQuoteVolume': True,
'timeframe': '5m',
},
'bitvavo': {
'pair': 'BTC/EUR',
'stake_currency': 'EUR',
@@ -198,7 +204,10 @@ class TestCCXTExchange():
else:
next_limit = exchange.get_next_limit_in_list(
val, l2_limit_range, l2_limit_range_required)
if next_limit is None or next_limit > 200:
if next_limit is None:
assert len(l2['asks']) > 100
assert len(l2['asks']) > 100
elif next_limit > 200:
# Large orderbook sizes can be a problem for some exchanges (bitrex ...)
assert len(l2['asks']) > 200
assert len(l2['asks']) > 200

View File

@@ -168,7 +168,7 @@ def test_exchange_resolver(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
exchange = ExchangeResolver.load_exchange('huobi', default_conf)
exchange = ExchangeResolver.load_exchange('zaif', default_conf)
assert isinstance(exchange, Exchange)
assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog)
caplog.clear()

View File

@@ -0,0 +1,109 @@
from random import randint
from unittest.mock import MagicMock
import ccxt
import pytest
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
from tests.conftest import get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers
@pytest.mark.parametrize('limitratio,expected', [
(None, 220 * 0.99),
(0.99, 220 * 0.99),
(0.98, 220 * 0.98),
])
def test_stoploss_order_huobi(default_conf, mocker, limitratio, expected):
api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
order_type = 'stop-limit'
api_mock.create_order = MagicMock(return_value={
'id': order_id,
'info': {
'foo': 'bar'
}
})
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
with pytest.raises(OperationalException):
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
api_mock.create_order.reset_mock()
order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio}
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types)
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
# Price should be 1% below stopprice
assert api_mock.create_order.call_args_list[0][1]['price'] == expected
assert api_mock.create_order.call_args_list[0][1]['params'] == {"stopPrice": 220,
"operator": "lte",
}
# test exception handling
with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
with pytest.raises(InvalidOrderException):
api_mock.create_order = MagicMock(
side_effect=ccxt.InvalidOrder("binance Order would trigger immediately."))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "huobi",
"stoploss", "create_order", retries=1,
pair='ETH/BTC', amount=1, stop_price=220, order_types={})
def test_stoploss_order_dry_run_huobi(default_conf, mocker):
api_mock = MagicMock()
order_type = 'stop-limit'
default_conf['dry_run'] = True
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
with pytest.raises(OperationalException):
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
assert 'id' in order
assert 'info' in order
assert 'type' in order
assert order['type'] == order_type
assert order['price'] == 220
assert order['amount'] == 1
def test_stoploss_adjust_huobi(mocker, default_conf):
exchange = get_patched_exchange(mocker, default_conf, id='huobi')
order = {
'type': 'stop',
'price': 1500,
'stopPrice': '1500',
}
assert exchange.stoploss_adjust(1501, order)
assert not exchange.stoploss_adjust(1499, order)
# Test with invalid order case
order['type'] = 'stop_loss'
assert not exchange.stoploss_adjust(1501, order)

View File

@@ -0,0 +1,120 @@
from random import randint
from unittest.mock import MagicMock
import ccxt
import pytest
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
from tests.conftest import get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers
@pytest.mark.parametrize('order_type', ['market', 'limit'])
@pytest.mark.parametrize('limitratio,expected', [
(None, 220 * 0.99),
(0.99, 220 * 0.99),
(0.98, 220 * 0.98),
])
def test_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, order_type):
api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
api_mock.create_order = MagicMock(return_value={
'id': order_id,
'info': {
'foo': 'bar'
}
})
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
if order_type == 'limit':
with pytest.raises(OperationalException):
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
order_types={
'stoploss': order_type,
'stoploss_on_exchange_limit_ratio': 1.05})
api_mock.create_order.reset_mock()
order_types = {'stoploss': order_type}
if limitratio is not None:
order_types.update({'stoploss_on_exchange_limit_ratio': limitratio})
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types)
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
# Price should be 1% below stopprice
if order_type == 'limit':
assert api_mock.create_order.call_args_list[0][1]['price'] == expected
else:
assert api_mock.create_order.call_args_list[0][1]['price'] is None
assert api_mock.create_order.call_args_list[0][1]['params'] == {
'stopPrice': 220,
'stop': 'loss'
}
# test exception handling
with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
with pytest.raises(InvalidOrderException):
api_mock.create_order = MagicMock(
side_effect=ccxt.InvalidOrder("kucoin Order would trigger immediately."))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kucoin",
"stoploss", "create_order", retries=1,
pair='ETH/BTC', amount=1, stop_price=220, order_types={})
def test_stoploss_order_dry_run_kucoin(default_conf, mocker):
api_mock = MagicMock()
order_type = 'market'
default_conf['dry_run'] = True
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
with pytest.raises(OperationalException):
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
order_types={'stoploss': 'limit',
'stoploss_on_exchange_limit_ratio': 1.05})
api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
assert 'id' in order
assert 'info' in order
assert 'type' in order
assert order['type'] == order_type
assert order['price'] == 220
assert order['amount'] == 1
def test_stoploss_adjust_kucoin(mocker, default_conf):
exchange = get_patched_exchange(mocker, default_conf, id='kucoin')
order = {
'type': 'limit',
'price': 1500,
'stopPrice': 1500,
'info': {'stopPrice': 1500, 'stop': "limit"},
}
assert exchange.stoploss_adjust(1501, order)
assert not exchange.stoploss_adjust(1499, order)
# Test with invalid order case
order['info']['stop'] = None
assert not exchange.stoploss_adjust(1501, order)

View File

@@ -80,7 +80,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'close_rate': None,
'current_rate': 1.099e-05,
'amount': 91.07468123,
'amount_requested': 91.07468123,
'amount_requested': 91.07468124,
'stake_amount': 0.001,
'trade_duration': None,
'trade_duration_s': None,
@@ -116,14 +116,16 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'is_short': False,
'funding_fees': 0.0,
'trading_mode': TradingMode.SPOT,
'filled_entry_orders': [{
'amount': 91.07468123, 'average': 1.098e-05,
# 'filled_entery_orders': [{
'orders': [{
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
'is_open': False, 'pair': 'ETH/BTC',
'remaining': ANY, 'status': ANY}],
'filled_exit_orders': [],
'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY,
'remaining': ANY, 'status': ANY
}],
# 'filled_exit_orders': [],
}
mocker.patch('freqtrade.exchange.Exchange.get_rate',
@@ -162,7 +164,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'close_rate': None,
'current_rate': ANY,
'amount': 91.07468123,
'amount_requested': 91.07468123,
'amount_requested': 91.07468124,
'trade_duration': ANY,
'trade_duration_s': ANY,
'stake_amount': 0.001,
@@ -198,14 +200,15 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'is_short': False,
'funding_fees': 0.0,
'trading_mode': TradingMode.SPOT,
'filled_entry_orders': [{
'amount': 91.07468123, 'average': 1.098e-05,
# 'filled_entry_orders': [{
'orders': [{
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
'is_open': False, 'pair': 'ETH/BTC',
'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY,
'remaining': ANY, 'status': ANY}],
'filled_exit_orders': [],
# 'filled_exit_orders': [],
}

View File

@@ -971,6 +971,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
'interest_rate': 0.0,
'funding_fees': None,
'trading_mode': ANY,
'orders': [ANY],
}
mocker.patch('freqtrade.exchange.Exchange.get_rate',
@@ -1044,8 +1045,8 @@ def test_api_blacklist(botclient, mocker):
"NOTHING/BTC": {
"error_msg": "Pair NOTHING/BTC is not in the current blacklist."
}
},
}
},
}
rc = client_delete(
client,
f"{BASE_URI}/blacklist?pairs_to_delete=HOT/BTC&pairs_to_delete=ETH/BTC")
@@ -1170,6 +1171,7 @@ def test_api_forceentry(botclient, mocker, fee, endpoint):
'interest_rate': None,
'funding_fees': None,
'trading_mode': 'spot',
'orders': [],
}
@@ -1452,6 +1454,11 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
ftbot, client = botclient
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
rc = client_get(client, f"{BASE_URI}/backtest")
# Backtest prevented in default mode
assert_response(rc, 502)
ftbot.config['runmode'] = RunMode.WEBSERVER
# Backtesting not started yet
rc = client_get(client, f"{BASE_URI}/backtest")
assert_response(rc)

View File

@@ -208,6 +208,7 @@ def test_telegram_status(default_conf, update, mocker) -> None:
'is_open': True,
'is_short': False,
'filled_entry_orders': [],
'orders': []
}]),
)
@@ -240,6 +241,8 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
create_mock_trades(fee)
trades = Trade.get_open_trades()
trade = trades[0]
# Average may be empty on some exchanges
trade.orders[0].average = 0
trade.orders.append(Order(
order_id='5412vbb',
ft_order_side='buy',
@@ -250,7 +253,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
order_type="market",
side="buy",
price=trade.open_rate * 0.95,
average=trade.open_rate * 0.95,
average=0,
filled=trade.amount,
remaining=0,
cost=trade.amount,
@@ -626,7 +629,7 @@ def test_weekly_wrong_input(default_conf, update, ticker, mocker) -> None:
context.args = ["this week"]
telegram._weekly(update=update, context=context)
assert str('Weekly Profit over the last 8 weeks (starting from Monday)</b>:') \
in msg_mock.call_args_list[0][0][0]
in msg_mock.call_args_list[0][0][0]
def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
@@ -1872,7 +1875,7 @@ def test_send_msg_protection_notification(default_conf, mocker, time_machine) ->
(RPCMessageType.BUY_FILL, 'Longed', 'long_signal_01', 1.0),
(RPCMessageType.BUY_FILL, 'Longed', 'long_signal_02', 2.0),
(RPCMessageType.SHORT_FILL, 'Shorted', 'short_signal_01', 2.0),
])
])
def test_send_msg_buy_fill_notification(default_conf, mocker, message_type, entered,
enter_signal, leverage) -> None:
@@ -1902,7 +1905,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker, message_type, ente
f"{leverage_text}"
'*Open Rate:* `0.00001099`\n'
'*Total:* `(0.01465333 BTC, 180.895 USD)`'
)
)
def test_send_msg_sell_notification(default_conf, mocker) -> None:
@@ -1944,7 +1947,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n'
'*Close Rate:* `0.00003201`'
)
)
msg_mock.reset_mock()
telegram.send_msg({
@@ -1978,7 +1981,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n'
'*Close Rate:* `0.00003201`'
)
)
# Reset singleton function to avoid random breaks
telegram._rpc._fiat_converter.convert_amount = old_convamount
@@ -2142,7 +2145,7 @@ def test_send_msg_buy_notification_no_fiat(
('Long', 'long_signal_01', 1.0),
('Long', 'long_signal_01', 5.0),
('Short', 'short_signal_01', 2.0),
])
])
def test_send_msg_sell_notification_no_fiat(
default_conf, mocker, direction, enter_signal, leverage) -> None:
del default_conf['fiat_display_currency']
@@ -2184,7 +2187,7 @@ def test_send_msg_sell_notification_no_fiat(
'*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n'
'*Close Rate:* `0.00003201`'
)
)
@pytest.mark.parametrize('msg,expected', [

View File

@@ -6,7 +6,7 @@ import time
from copy import deepcopy
from math import isclose
from typing import List
from unittest.mock import ANY, MagicMock, PropertyMock
from unittest.mock import ANY, MagicMock, PropertyMock, patch
import arrow
import pytest
@@ -1407,7 +1407,7 @@ def test_handle_stoploss_on_exchange_trailing(
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
stoploss_order_mock.assert_called_once_with(
amount=amt,
amount=pytest.approx(amt),
pair='ETH/USDT',
order_types=freqtrade.strategy.order_types,
stop_price=stop_price[1],
@@ -1611,7 +1611,7 @@ def test_handle_stoploss_on_exchange_custom_stop(
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
# Long uses modified ask - offset, short modified bid + offset
stoploss_order_mock.assert_called_once_with(
amount=trade.amount,
amount=pytest.approx(trade.amount),
pair='ETH/USDT',
order_types=freqtrade.strategy.order_types,
stop_price=4.4 * 0.96 if not is_short else 0.95 * 1.04,
@@ -1741,7 +1741,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
assert trade.stop_loss == 4.4 * 0.99
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
stoploss_order_mock.assert_called_once_with(
amount=11.41438356,
amount=pytest.approx(11.41438356),
pair='NEO/BTC',
order_types=freqtrade.strategy.order_types,
stop_price=4.4 * 0.99,
@@ -2534,9 +2534,14 @@ def test_check_handle_timedout_sell_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 sell!
with patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit', return_value=False):
freqtrade.check_handle_timedout()
assert et_mock.call_count == 0
freqtrade.check_handle_timedout()
assert log_has_re('Emergencyselling trade.*', caplog)
assert et_mock.call_count == 1
@@ -2896,9 +2901,12 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
exchange='binance',
open_rate=0.245441,
open_order_id="123456",
open_date=arrow.utcnow().datetime,
open_date=arrow.utcnow().shift(days=-2).datetime,
fee_open=fee.return_value,
fee_close=fee.return_value,
close_rate=0.555,
close_date=arrow.utcnow().datetime,
sell_reason="sell_reason_whatever",
)
order = {'remaining': 1,
'amount': 1,
@@ -2907,17 +2915,23 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
assert freqtrade.handle_cancel_exit(trade, order, reason)
assert cancel_order_mock.call_count == 1
assert send_msg_mock.call_count == 1
assert trade.close_rate is None
assert trade.sell_reason is None
send_msg_mock.reset_mock()
order['amount'] = 2
assert freqtrade.handle_cancel_exit(trade, order, reason
) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
assert not freqtrade.handle_cancel_exit(trade, order, 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 freqtrade.handle_cancel_exit(trade, order, reason
) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
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)
send_msg_mock.call_args_list[0][0][0]['reason'] = CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
# Message should not be iterated again
assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
assert send_msg_mock.call_count == 1
@@ -2936,7 +2950,7 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
order = {'remaining': 1,
'amount': 1,
'status': "open"}
assert freqtrade.handle_cancel_exit(trade, order, reason) == 'error cancelling order'
assert not freqtrade.handle_cancel_exit(trade, order, reason)
@pytest.mark.parametrize("is_short, open_rate, amt", [

View File

@@ -1610,6 +1610,7 @@ def test_to_json(default_conf, fee):
'funding_fees': None,
'filled_entry_orders': [],
'filled_exit_orders': [],
'orders': [],
}
# Simulate dry_run entries
@@ -1686,6 +1687,7 @@ def test_to_json(default_conf, fee):
'funding_fees': None,
'filled_entry_orders': [],
'filled_exit_orders': [],
'orders': [],
}