merged lev-freqtradebot with lev-strat

This commit is contained in:
Sam Germain
2021-09-19 19:06:43 -06:00
55 changed files with 2704 additions and 1036 deletions

View File

@@ -18,7 +18,7 @@ from freqtrade import constants
from freqtrade.commands import Arguments
from freqtrade.data.converter import ohlcv_to_dataframe
from freqtrade.edge import Edge, PairInfo
from freqtrade.enums import RunMode
from freqtrade.enums import Collateral, RunMode, TradingMode
from freqtrade.enums.signaltype import SignalDirection
from freqtrade.exchange import Exchange
from freqtrade.freqtradebot import FreqtradeBot
@@ -90,7 +90,13 @@ def patched_configuration_load_config_file(mocker, config) -> None:
)
def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> None:
def patch_exchange(
mocker,
api_mock=None,
id='binance',
mock_markets=True,
mock_supported_modes=True
) -> None:
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
@@ -99,10 +105,22 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id))
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title()))
mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2))
if mock_markets:
mocker.patch('freqtrade.exchange.Exchange.markets',
PropertyMock(return_value=get_markets()))
if mock_supported_modes:
mocker.patch(
f'freqtrade.exchange.{id.capitalize()}._supported_trading_mode_collateral_pairs',
PropertyMock(return_value=[
(TradingMode.MARGIN, Collateral.CROSS),
(TradingMode.MARGIN, Collateral.ISOLATED),
(TradingMode.FUTURES, Collateral.CROSS),
(TradingMode.FUTURES, Collateral.ISOLATED)
])
)
if api_mock:
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
else:
@@ -110,8 +128,8 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No
def get_patched_exchange(mocker, config, api_mock=None, id='binance',
mock_markets=True) -> Exchange:
patch_exchange(mocker, api_mock, id, mock_markets)
mock_markets=True, mock_supported_modes=True) -> Exchange:
patch_exchange(mocker, api_mock, id, mock_markets, mock_supported_modes)
config['exchange']['name'] = id
try:
exchange = ExchangeResolver.load_exchange(id, config)

View File

@@ -5,7 +5,7 @@ from unittest.mock import MagicMock, PropertyMock
import ccxt
import pytest
from freqtrade.enums import TradingMode
from freqtrade.enums import Collateral, TradingMode
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
from tests.conftest import get_mock_coro, get_patched_exchange, log_has_re
from tests.exchange.test_exchange import ccxt_exceptionhandlers
@@ -48,13 +48,20 @@ def test_stoploss_order_binance(
amount=1,
stop_price=190,
side=side,
order_types={'stoploss_on_exchange_limit_ratio': 1.05}
order_types={'stoploss_on_exchange_limit_ratio': 1.05},
leverage=1.0
)
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, side=side)
order = exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
order_types=order_types,
side=side,
leverage=1.0
)
assert 'id' in order
assert 'info' in order
@@ -71,17 +78,31 @@ def test_stoploss_order_binance(
with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side)
exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
order_types={},
side=side,
leverage=1.0)
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={}, side=side)
exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
order_types={},
side=side,
leverage=1.0
)
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance",
"stoploss", "create_order", retries=1,
pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side)
pair='ETH/BTC', amount=1, stop_price=220, order_types={},
side=side, leverage=1.0)
def test_stoploss_order_dry_run_binance(default_conf, mocker):
@@ -94,12 +115,25 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
with pytest.raises(OperationalException):
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, side="sell",
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
order = exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=190,
side="sell",
order_types={'stoploss_on_exchange_limit_ratio': 1.05},
leverage=1.0
)
api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell")
order = exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
order_types={},
side="sell",
leverage=1.0
)
assert 'id' in order
assert 'info' in order
@@ -194,6 +228,9 @@ def test_fill_leverage_brackets_binance(default_conf, mocker):
[1000000.0, 0.5]],
})
default_conf['dry_run'] = False
default_conf['trading_mode'] = TradingMode.FUTURES
default_conf['collateral'] = Collateral.ISOLATED
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
exchange.fill_leverage_brackets()
@@ -236,12 +273,59 @@ def test_fill_leverage_brackets_binance(default_conf, mocker):
)
def test_fill_leverage_brackets_binance_dryrun(default_conf, mocker):
api_mock = MagicMock()
default_conf['trading_mode'] = TradingMode.FUTURES
default_conf['collateral'] = Collateral.ISOLATED
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
exchange.fill_leverage_brackets()
leverage_brackets = {
"1000SHIB/USDT": [
[0.0, 0.01],
[5000.0, 0.025],
[25000.0, 0.05],
[100000.0, 0.1],
[250000.0, 0.125],
[1000000.0, 0.5]
],
"1INCH/USDT": [
[0.0, 0.012],
[5000.0, 0.025],
[25000.0, 0.05],
[100000.0, 0.1],
[250000.0, 0.125],
[1000000.0, 0.5]
],
"AAVE/USDT": [
[0.0, 0.01],
[50000.0, 0.02],
[250000.0, 0.05],
[1000000.0, 0.1],
[2000000.0, 0.125],
[5000000.0, 0.1665],
[10000000.0, 0.25]
],
"ADA/BUSD": [
[0.0, 0.025],
[100000.0, 0.05],
[500000.0, 0.1],
[1000000.0, 0.15],
[2000000.0, 0.25],
[5000000.0, 0.5]
]
}
for key, value in leverage_brackets.items():
assert exchange._leverage_brackets[key] == value
def test__set_leverage_binance(mocker, default_conf):
api_mock = MagicMock()
api_mock.set_leverage = MagicMock()
type(api_mock).has = PropertyMock(return_value={'setLeverage': True})
default_conf['dry_run'] = False
exchange = get_patched_exchange(mocker, default_conf, id="binance")
exchange._set_leverage(3.0, trading_mode=TradingMode.MARGIN)
@@ -288,3 +372,15 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog):
assert exchange._api_async.fetch_ohlcv.call_count == 2
assert res == ohlcv
assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog)
@pytest.mark.parametrize("trading_mode,collateral,config", [
("", "", {}),
("margin", "cross", {"options": {"defaultType": "margin"}}),
("futures", "isolated", {"options": {"defaultType": "future"}}),
])
def test__ccxt_config(default_conf, mocker, trading_mode, collateral, config):
default_conf['trading_mode'] = trading_mode
default_conf['collateral'] = collateral
exchange = get_patched_exchange(mocker, default_conf, id="binance")
assert exchange._ccxt_config == config

View File

@@ -132,6 +132,7 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog):
assert log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog)
assert ex._api.headers == {'hello': 'world'}
assert ex._ccxt_config == {}
Exchange._headers = {}
# TODO-lev: Test with options
@@ -403,7 +404,6 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
# With Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss, 3.0)
assert isclose(result, expected_result/3)
# TODO-lev: Min stake for base, kraken and ftx
# min amount is set
markets["ETH/BTC"]["limits"] = {
@@ -420,27 +420,21 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
# With Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0)
assert isclose(result, expected_result/5)
# TODO-lev: Min stake for base, kraken and ftx
# min amount and cost are set (cost is minimal)
markets["ETH/BTC"]["limits"] = {
'cost': {'min': 2},
'amount': {'min': 2}
}
mocker.patch(
'freqtrade.exchange.Exchange.markets',
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
expected_result = max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss))
result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
expected_result=max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss))
assert isclose(result, expected_result)
# With Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10)
result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10)
assert isclose(result, expected_result/10)
# TODO-lev: Min stake for base, kraken and ftx
# min amount and cost are set (amount is minial)
markets["ETH/BTC"]["limits"] = {
markets["ETH/BTC"]["limits"]={
'cost': {'min': 8},
'amount': {'min': 2}
}
@@ -448,39 +442,28 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
'freqtrade.exchange.Exchange.markets',
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
expected_result = max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss))
result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
expected_result=max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss))
assert isclose(result, expected_result)
# With Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 7.0)
result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 7.0)
assert isclose(result, expected_result/7.0)
# TODO-lev: Min stake for base, kraken and ftx
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4)
expected_result = max(8, 2 * 2) * 1.5
result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4)
expected_result=max(8, 2 * 2) * 1.5
assert isclose(result, expected_result)
# With Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4, 8.0)
result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4, 8.0)
assert isclose(result, expected_result/8.0)
# TODO-lev: Min stake for base, kraken and ftx
# Really big stoploss
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1)
expected_result = max(8, 2 * 2) * 1.5
assert isclose(result, expected_result)
# With Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0)
result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0)
assert isclose(result, expected_result/12)
# TODO-lev: Min stake for base, kraken and ftx
def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
exchange = get_patched_exchange(mocker, default_conf, id="binance")
stoploss = -0.05
markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}}
stoploss=-0.05
markets={'ETH/BTC': {'symbol': 'ETH/BTC'}}
# Real Binance data
markets["ETH/BTC"]["limits"] = {
markets["ETH/BTC"]["limits"]={
'cost': {'min': 0.0001},
'amount': {'min': 0.001}
}
@@ -488,28 +471,27 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
'freqtrade.exchange.Exchange.markets',
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss)
expected_result = max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss))
result=exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss)
expected_result=max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss))
assert round(result, 8) == round(expected_result, 8)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss, 3.0)
result=exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss, 3.0)
assert round(result, 8) == round(expected_result/3, 8)
# TODO-lev: Min stake for base, kraken and ftx
def test_set_sandbox(default_conf, mocker):
"""
Test working scenario
"""
api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value={
api_mock=MagicMock()
api_mock.load_markets=MagicMock(return_value = {
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
})
url_mock = PropertyMock(return_value={'test': "api-public.sandbox.gdax.com",
url_mock=PropertyMock(return_value = {'test': "api-public.sandbox.gdax.com",
'api': 'https://api.gdax.com'})
type(api_mock).urls = url_mock
exchange = get_patched_exchange(mocker, default_conf, api_mock)
liveurl = exchange._api.urls['api']
default_conf['exchange']['sandbox'] = True
type(api_mock).urls=url_mock
exchange=get_patched_exchange(mocker, default_conf, api_mock)
liveurl=exchange._api.urls['api']
default_conf['exchange']['sandbox']=True
exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname')
assert exchange._api.urls['api'] != liveurl
@@ -518,16 +500,16 @@ def test_set_sandbox_exception(default_conf, mocker):
"""
Test Fail scenario
"""
api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value={
api_mock=MagicMock()
api_mock.load_markets=MagicMock(return_value = {
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
})
url_mock = PropertyMock(return_value={'api': 'https://api.gdax.com'})
type(api_mock).urls = url_mock
url_mock=PropertyMock(return_value = {'api': 'https://api.gdax.com'})
type(api_mock).urls=url_mock
with pytest.raises(OperationalException, match=r'does not provide a sandbox api'):
exchange = get_patched_exchange(mocker, default_conf, api_mock)
default_conf['exchange']['sandbox'] = True
with pytest.raises(OperationalException, match = r'does not provide a sandbox api'):
exchange=get_patched_exchange(mocker, default_conf, api_mock)
default_conf['exchange']['sandbox']=True
exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname')
@@ -537,13 +519,13 @@ def test__load_async_markets(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange._load_markets')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
exchange = Exchange(default_conf)
exchange._api_async.load_markets = get_mock_coro(None)
exchange=Exchange(default_conf)
exchange._api_async.load_markets=get_mock_coro(None)
exchange._load_async_markets()
assert exchange._api_async.load_markets.call_count == 1
caplog.set_level(logging.DEBUG)
exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef"))
exchange._api_async.load_markets=Mock(side_effect = ccxt.BaseError("deadbeef"))
exchange._load_async_markets()
assert log_has('Could not load async markets. Reason: deadbeef', caplog)
@@ -551,8 +533,8 @@ def test__load_async_markets(default_conf, mocker, caplog):
def test__load_markets(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
api_mock = MagicMock()
api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError("SomeError"))
api_mock=MagicMock()
api_mock.load_markets=MagicMock(side_effect = ccxt.BaseError("SomeError"))
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
@@ -561,28 +543,28 @@ def test__load_markets(default_conf, mocker, caplog):
Exchange(default_conf)
assert log_has('Unable to initialize markets.', caplog)
expected_return = {'ETH/BTC': 'available'}
api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value=expected_return)
expected_return={'ETH/BTC': 'available'}
api_mock=MagicMock()
api_mock.load_markets=MagicMock(return_value = expected_return)
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
default_conf['exchange']['pair_whitelist'] = ['ETH/BTC']
ex = Exchange(default_conf)
default_conf['exchange']['pair_whitelist']=['ETH/BTC']
ex=Exchange(default_conf)
assert ex.markets == expected_return
def test_reload_markets(default_conf, mocker, caplog):
caplog.set_level(logging.DEBUG)
initial_markets = {'ETH/BTC': {}}
updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}}
initial_markets={'ETH/BTC': {}}
updated_markets={'ETH/BTC': {}, "LTC/BTC": {}}
api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value=initial_markets)
default_conf['exchange']['markets_refresh_interval'] = 10
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance",
mock_markets=False)
exchange._load_async_markets = MagicMock()
exchange._last_markets_refresh = arrow.utcnow().int_timestamp
api_mock=MagicMock()
api_mock.load_markets=MagicMock(return_value = initial_markets)
default_conf['exchange']['markets_refresh_interval']=10
exchange=get_patched_exchange(mocker, default_conf, api_mock, id = "binance",
mock_markets = False)
exchange._load_async_markets=MagicMock()
exchange._last_markets_refresh=arrow.utcnow().int_timestamp
assert exchange.markets == initial_markets
@@ -591,9 +573,9 @@ def test_reload_markets(default_conf, mocker, caplog):
assert exchange.markets == initial_markets
assert exchange._load_async_markets.call_count == 0
api_mock.load_markets = MagicMock(return_value=updated_markets)
api_mock.load_markets=MagicMock(return_value = updated_markets)
# more than 10 minutes have passed, reload is executed
exchange._last_markets_refresh = arrow.utcnow().int_timestamp - 15 * 60
exchange._last_markets_refresh=arrow.utcnow().int_timestamp - 15 * 60
exchange.reload_markets()
assert exchange.markets == updated_markets
assert exchange._load_async_markets.call_count == 1
@@ -603,10 +585,10 @@ def test_reload_markets(default_conf, mocker, caplog):
def test_reload_markets_exception(default_conf, mocker, caplog):
caplog.set_level(logging.DEBUG)
api_mock = MagicMock()
api_mock.load_markets = MagicMock(side_effect=ccxt.NetworkError("LoadError"))
default_conf['exchange']['markets_refresh_interval'] = 10
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
api_mock=MagicMock()
api_mock.load_markets=MagicMock(side_effect = ccxt.NetworkError("LoadError"))
default_conf['exchange']['markets_refresh_interval']=10
exchange=get_patched_exchange(mocker, default_conf, api_mock, id = "binance")
# less than 10 minutes have passed, no reload
exchange.reload_markets()
@@ -614,11 +596,11 @@ def test_reload_markets_exception(default_conf, mocker, caplog):
assert log_has_re(r"Could not reload markets.*", caplog)
@pytest.mark.parametrize("stake_currency", ['ETH', 'BTC', 'USDT'])
@ pytest.mark.parametrize("stake_currency", ['ETH', 'BTC', 'USDT'])
def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
default_conf['stake_currency'] = stake_currency
api_mock = MagicMock()
type(api_mock).load_markets = MagicMock(return_value={
default_conf['stake_currency']=stake_currency
api_mock=MagicMock()
type(api_mock).load_markets=MagicMock(return_value = {
'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'},
})
@@ -630,9 +612,9 @@ def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
def test_validate_stakecurrency_error(default_conf, mocker, caplog):
default_conf['stake_currency'] = 'XRP'
api_mock = MagicMock()
type(api_mock).load_markets = MagicMock(return_value={
default_conf['stake_currency']='XRP'
api_mock=MagicMock()
type(api_mock).load_markets=MagicMock(return_value = {
'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'},
})
@@ -1004,7 +986,13 @@ def test_create_dry_run_order(default_conf, mocker, side, exchange_name):
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
order = exchange.create_dry_run_order(
pair='ETH/BTC', ordertype='limit', side=side, amount=1, rate=200)
pair='ETH/BTC',
ordertype='limit',
side=side,
amount=1,
rate=200,
leverage=1.0
)
assert 'id' in order
assert f'dry_run_{side}_' in order["id"]
assert order["side"] == side
@@ -1027,7 +1015,13 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice,
)
order = exchange.create_dry_run_order(
pair='LTC/USDT', ordertype='limit', side=side, amount=1, rate=startprice)
pair='LTC/USDT',
ordertype='limit',
side=side,
amount=1,
rate=startprice,
leverage=1.0
)
assert order_book_l2_usd.call_count == 1
assert 'id' in order
assert f'dry_run_{side}_' in order["id"]
@@ -1073,7 +1067,13 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou
)
order = exchange.create_dry_run_order(
pair='LTC/USDT', ordertype='market', side=side, amount=amount, rate=rate)
pair='LTC/USDT',
ordertype='market',
side=side,
amount=amount,
rate=rate,
leverage=1.0
)
assert 'id' in order
assert f'dry_run_{side}_' in order["id"]
assert order["side"] == side
@@ -1083,10 +1083,7 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou
assert round(order["average"], 4) == round(endprice, 4)
@pytest.mark.parametrize("side", [
("buy"),
("sell")
])
@pytest.mark.parametrize("side", ["buy", "sell"])
@pytest.mark.parametrize("ordertype,rate,marketprice", [
("market", None, None),
("market", 200, True),
@@ -1108,9 +1105,17 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice,
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, id=exchange_name)
exchange._set_leverage = MagicMock()
exchange.set_margin_mode = MagicMock()
order = exchange.create_order(
pair='ETH/BTC', ordertype=ordertype, side=side, amount=1, rate=200)
pair='ETH/BTC',
ordertype=ordertype,
side=side,
amount=1,
rate=200,
leverage=1.0
)
assert 'id' in order
assert 'info' in order
@@ -1120,6 +1125,21 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice,
assert api_mock.create_order.call_args[0][2] == side
assert api_mock.create_order.call_args[0][3] == 1
assert api_mock.create_order.call_args[0][4] is rate
assert exchange._set_leverage.call_count == 0
assert exchange.set_margin_mode.call_count == 0
exchange.trading_mode = TradingMode.FUTURES
order = exchange.create_order(
pair='ETH/BTC',
ordertype=ordertype,
side=side,
amount=1,
rate=200,
leverage=3.0
)
assert exchange._set_leverage.call_count == 1
assert exchange.set_margin_mode.call_count == 1
def test_buy_dry_run(default_conf, mocker):
@@ -2658,7 +2678,14 @@ def test_get_fee(default_conf, mocker, exchange_name):
def test_stoploss_order_unsupported_exchange(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, id='bittrex')
with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"):
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell")
exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
order_types={},
side="sell",
leverage=1.0
)
with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"):
exchange.stoploss_adjust(1, {}, side="sell")
@@ -3006,7 +3033,6 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None:
(3, 5, 5),
(4, 5, 2),
(5, 5, 1),
])
def test_calculate_backoff(retrycount, max_retries, expected):
assert calculate_backoff(retrycount, max_retries) == expected
@@ -3018,7 +3044,7 @@ def test_calculate_backoff(retrycount, max_retries, expected):
(20.0, 5.0, 4.0),
(100.0, 100.0, 1.0)
])
def test_apply_leverage_to_stake_amount(
def test_get_stake_amount_considering_leverage(
exchange,
stake_amount,
leverage,
@@ -3027,7 +3053,8 @@ def test_apply_leverage_to_stake_amount(
default_conf
):
exchange = get_patched_exchange(mocker, default_conf, id=exchange)
assert exchange._apply_leverage_to_stake_amount(stake_amount, leverage) == min_stake_with_lev
assert exchange._get_stake_amount_considering_leverage(
stake_amount, leverage) == min_stake_with_lev
@pytest.mark.parametrize("exchange_name,trading_mode", [
@@ -3040,6 +3067,7 @@ def test__set_leverage(mocker, default_conf, exchange_name, trading_mode):
api_mock = MagicMock()
api_mock.set_leverage = MagicMock()
type(api_mock).has = PropertyMock(return_value={'setLeverage': True})
default_conf['dry_run'] = False
ccxt_exceptionhandlers(
mocker,
@@ -3063,6 +3091,7 @@ def test_set_margin_mode(mocker, default_conf, collateral):
api_mock = MagicMock()
api_mock.set_margin_mode = MagicMock()
type(api_mock).has = PropertyMock(return_value={'setMarginMode': True})
default_conf['dry_run'] = False
ccxt_exceptionhandlers(
mocker,
@@ -3117,7 +3146,8 @@ def test_validate_trading_mode_and_collateral(
collateral,
exception_thrown
):
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
exchange = get_patched_exchange(
mocker, default_conf, id=exchange_name, mock_supported_modes=False)
if (exception_thrown):
with pytest.raises(OperationalException):
exchange.validate_trading_mode_and_collateral(trading_mode, collateral)

View File

@@ -37,8 +37,14 @@ def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitrati
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
# stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, side=side,
order_types={'stoploss_on_exchange_limit_ratio': exchangelimitratio})
order = exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=190,
side=side,
order_types={'stoploss_on_exchange_limit_ratio': exchangelimitratio},
leverage=1.0
)
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
@@ -52,7 +58,14 @@ def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitrati
api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side)
order = exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
order_types={},
side=side,
leverage=1.0
)
assert 'id' in order
assert 'info' in order
@@ -65,8 +78,13 @@ def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitrati
assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220
api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
order_types={'stoploss': 'limit'}, side=side)
order = exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
order_types={'stoploss': 'limit'}, side=side,
leverage=1.0
)
assert 'id' in order
assert 'info' in order
@@ -83,17 +101,32 @@ def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitrati
with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side)
exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
order_types={},
side=side,
leverage=1.0
)
with pytest.raises(InvalidOrderException):
api_mock.create_order = MagicMock(
side_effect=ccxt.InvalidOrder("ftx Order would trigger immediately."))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side)
exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
order_types={},
side=side,
leverage=1.0
)
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx",
"stoploss", "create_order", retries=1,
pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side)
pair='ETH/BTC', amount=1, stop_price=220, order_types={},
side=side, leverage=1.0)
@pytest.mark.parametrize('side', [("sell"), ("buy")])
@@ -107,7 +140,14 @@ def test_stoploss_order_dry_run_ftx(default_conf, mocker, side):
api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side)
order = exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
order_types={},
side=side,
leverage=1.0
)
assert 'id' in order
assert 'info' in order
@@ -228,26 +268,3 @@ def test_fill_leverage_brackets_ftx(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, id="ftx")
exchange.fill_leverage_brackets()
assert exchange._leverage_brackets == {}
@pytest.mark.parametrize("trading_mode", [
(TradingMode.MARGIN),
(TradingMode.FUTURES)
])
def test__set_leverage(mocker, default_conf, trading_mode):
api_mock = MagicMock()
api_mock.set_leverage = MagicMock()
type(api_mock).has = PropertyMock(return_value={'setLeverage': True})
ccxt_exceptionhandlers(
mocker,
default_conf,
api_mock,
"ftx",
"_set_leverage",
"set_leverage",
pair="XRP/USDT",
leverage=5.0,
trading_mode=trading_mode
)

View File

@@ -197,7 +197,9 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr
order_types={
'stoploss': ordertype,
'stoploss_on_exchange_limit_ratio': 0.99
})
},
leverage=1.0
)
assert 'id' in order
assert 'info' in order
@@ -221,17 +223,32 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr
with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side)
exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
order_types={},
side=side,
leverage=1.0
)
with pytest.raises(InvalidOrderException):
api_mock.create_order = MagicMock(
side_effect=ccxt.InvalidOrder("kraken Order would trigger immediately."))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side)
exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
order_types={},
side=side,
leverage=1.0
)
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken",
"stoploss", "create_order", retries=1,
pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side)
pair='ETH/BTC', amount=1, stop_price=220, order_types={},
side=side, leverage=1.0)
@pytest.mark.parametrize('side', ['buy', 'sell'])
@@ -245,7 +262,14 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker, side):
api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side)
order = exchange.stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
order_types={},
side=side,
leverage=1.0
)
assert 'id' in order
assert 'info' in order
@@ -307,15 +331,3 @@ def test_fill_leverage_brackets_kraken(default_conf, mocker):
'XLTCUSDT': [1],
'LTC/ETH': [1]
}
def test__set_leverage_kraken(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, id="kraken")
exchange._set_leverage(1)
assert 'leverage' not in exchange._params
exchange._set_leverage(3)
assert exchange._params['leverage'] == 3
exchange._set_leverage(1.0)
assert 'leverage' not in exchange._params
exchange._set_leverage(3.0)
assert exchange._params['leverage'] == 3

View File

@@ -22,9 +22,10 @@ twentyfive_hours = Decimal(25.0)
('kraken', 0.00025, five_hours, 0.045),
('kraken', 0.00025, twentyfive_hours, 0.12),
# FTX
# TODO-lev: - implement FTX tests
# ('ftx', Decimal(0.0005), ten_mins, 0.06),
# ('ftx', Decimal(0.0005), five_hours, 0.045),
('ftx', 0.0005, ten_mins, 0.00125),
('ftx', 0.00025, ten_mins, 0.000625),
('ftx', 0.00025, five_hours, 0.003125),
('ftx', 0.00025, twentyfive_hours, 0.015625),
])
def test_interest(exchange, interest_rate, hours, expected):
borrowed = Decimal(60.0)

View File

@@ -1,271 +0,0 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
from functools import reduce
from typing import Any, Callable, Dict, List
import talib.abstract as ta
from pandas import DataFrame
from skopt.space import Categorical, Dimension, Integer
import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.optimize.hyperopt_interface import IHyperOpt
class DefaultHyperOpt(IHyperOpt):
"""
Default hyperopt provided by the Freqtrade bot.
You can override it with your own Hyperopt
"""
@staticmethod
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Add several indicators needed for buy and sell strategies defined below.
"""
# ADX
dataframe['adx'] = ta.ADX(dataframe)
# MACD
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
# MFI
dataframe['mfi'] = ta.MFI(dataframe)
# RSI
dataframe['rsi'] = ta.RSI(dataframe)
# Stochastic Fast
stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd']
# Minus-DI
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_upperband'] = bollinger['upper']
# SAR
dataframe['sar'] = ta.SAR(dataframe)
return dataframe
@staticmethod
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
Define the buy strategy parameters to be used by Hyperopt.
"""
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Buy strategy Hyperopt will build and use.
"""
long_conditions = []
short_conditions = []
# GUARDS AND TRENDS
if 'mfi-enabled' in params and params['mfi-enabled']:
long_conditions.append(dataframe['mfi'] < params['mfi-value'])
short_conditions.append(dataframe['mfi'] > params['short-mfi-value'])
if 'fastd-enabled' in params and params['fastd-enabled']:
long_conditions.append(dataframe['fastd'] < params['fastd-value'])
short_conditions.append(dataframe['fastd'] > params['short-fastd-value'])
if 'adx-enabled' in params and params['adx-enabled']:
long_conditions.append(dataframe['adx'] > params['adx-value'])
short_conditions.append(dataframe['adx'] < params['short-adx-value'])
if 'rsi-enabled' in params and params['rsi-enabled']:
long_conditions.append(dataframe['rsi'] < params['rsi-value'])
short_conditions.append(dataframe['rsi'] > params['short-rsi-value'])
# TRIGGERS
if 'trigger' in params:
if params['trigger'] == 'boll':
long_conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
short_conditions.append(dataframe['close'] > dataframe['bb_upperband'])
if params['trigger'] == 'macd_cross_signal':
long_conditions.append(qtpylib.crossed_above(
dataframe['macd'],
dataframe['macdsignal']
))
short_conditions.append(qtpylib.crossed_below(
dataframe['macd'],
dataframe['macdsignal']
))
if params['trigger'] == 'sar_reversal':
long_conditions.append(qtpylib.crossed_above(
dataframe['close'],
dataframe['sar']
))
short_conditions.append(qtpylib.crossed_below(
dataframe['close'],
dataframe['sar']
))
if long_conditions:
dataframe.loc[
reduce(lambda x, y: x & y, long_conditions),
'buy'] = 1
if short_conditions:
dataframe.loc[
reduce(lambda x, y: x & y, short_conditions),
'enter_short'] = 1
return dataframe
return populate_buy_trend
@staticmethod
def indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching buy strategy parameters.
"""
return [
Integer(10, 25, name='mfi-value'),
Integer(15, 45, name='fastd-value'),
Integer(20, 50, name='adx-value'),
Integer(20, 40, name='rsi-value'),
Integer(75, 90, name='short-mfi-value'),
Integer(55, 85, name='short-fastd-value'),
Integer(50, 80, name='short-adx-value'),
Integer(60, 80, name='short-rsi-value'),
Categorical([True, False], name='mfi-enabled'),
Categorical([True, False], name='fastd-enabled'),
Categorical([True, False], name='adx-enabled'),
Categorical([True, False], name='rsi-enabled'),
Categorical(['boll', 'macd_cross_signal', 'sar_reversal'], name='trigger')
]
@staticmethod
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
Define the sell strategy parameters to be used by Hyperopt.
"""
def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Sell strategy Hyperopt will build and use.
"""
exit_long_conditions = []
exit_short_conditions = []
# GUARDS AND TRENDS
if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']:
exit_long_conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
exit_short_conditions.append(dataframe['mfi'] < params['exit-short-mfi-value'])
if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']:
exit_long_conditions.append(dataframe['fastd'] > params['sell-fastd-value'])
exit_short_conditions.append(dataframe['fastd'] < params['exit-short-fastd-value'])
if 'sell-adx-enabled' in params and params['sell-adx-enabled']:
exit_long_conditions.append(dataframe['adx'] < params['sell-adx-value'])
exit_short_conditions.append(dataframe['adx'] > params['exit-short-adx-value'])
if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']:
exit_long_conditions.append(dataframe['rsi'] > params['sell-rsi-value'])
exit_short_conditions.append(dataframe['rsi'] < params['exit-short-rsi-value'])
# TRIGGERS
if 'sell-trigger' in params:
if params['sell-trigger'] == 'sell-boll':
exit_long_conditions.append(dataframe['close'] > dataframe['bb_upperband'])
exit_short_conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
if params['sell-trigger'] == 'sell-macd_cross_signal':
exit_long_conditions.append(qtpylib.crossed_above(
dataframe['macdsignal'],
dataframe['macd']
))
exit_short_conditions.append(qtpylib.crossed_below(
dataframe['macdsignal'],
dataframe['macd']
))
if params['sell-trigger'] == 'sell-sar_reversal':
exit_long_conditions.append(qtpylib.crossed_above(
dataframe['sar'],
dataframe['close']
))
exit_short_conditions.append(qtpylib.crossed_below(
dataframe['sar'],
dataframe['close']
))
if exit_long_conditions:
dataframe.loc[
reduce(lambda x, y: x & y, exit_long_conditions),
'sell'] = 1
if exit_short_conditions:
dataframe.loc[
reduce(lambda x, y: x & y, exit_short_conditions),
'exit-short'] = 1
return dataframe
return populate_sell_trend
@staticmethod
def sell_indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching sell strategy parameters.
"""
return [
Integer(75, 100, name='sell-mfi-value'),
Integer(50, 100, name='sell-fastd-value'),
Integer(50, 100, name='sell-adx-value'),
Integer(60, 100, name='sell-rsi-value'),
Integer(1, 25, name='exit-short-mfi-value'),
Integer(1, 50, name='exit-short-fastd-value'),
Integer(1, 50, name='exit-short-adx-value'),
Integer(1, 40, name='exit-short-rsi-value'),
Categorical([True, False], name='sell-mfi-enabled'),
Categorical([True, False], name='sell-fastd-enabled'),
Categorical([True, False], name='sell-adx-enabled'),
Categorical([True, False], name='sell-rsi-enabled'),
Categorical(['sell-boll',
'sell-macd_cross_signal',
'sell-sar_reversal'],
name='sell-trigger')
]
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators. Should be a copy of same method from strategy.
Must align to populate_indicators in this file.
Only used when --spaces does not include buy space.
"""
dataframe.loc[
(
(dataframe['close'] < dataframe['bb_lowerband']) &
(dataframe['mfi'] < 16) &
(dataframe['adx'] > 25) &
(dataframe['rsi'] < 21)
),
'buy'] = 1
dataframe.loc[
(
(dataframe['close'] > dataframe['bb_upperband']) &
(dataframe['mfi'] < 84) &
(dataframe['adx'] > 75) &
(dataframe['rsi'] < 79)
),
'enter_short'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators. Should be a copy of same method from strategy.
Must align to populate_indicators in this file.
Only used when --spaces does not include sell space.
"""
dataframe.loc[
(
(qtpylib.crossed_above(
dataframe['macdsignal'], dataframe['macd']
)) &
(dataframe['fastd'] > 54)
),
'sell'] = 1
dataframe.loc[
(
(qtpylib.crossed_below(
dataframe['macdsignal'], dataframe['macd']
)) &
(dataframe['fastd'] < 46)
),
'exit_short'] = 1
return dataframe

View File

@@ -887,6 +887,10 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
assert hyperopt.backtesting.strategy.buy_rsi.value != 35
assert hyperopt.backtesting.strategy.sell_rsi.value != 74
hyperopt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: 'ET1'
with pytest.raises(OperationalException, match="Estimator ET1 not supported."):
hyperopt.get_optimizer([], 2)
def test_SKDecimal():
space = SKDecimal(1, 2, decimals=2)

View File

@@ -4,6 +4,7 @@ import time
from unittest.mock import MagicMock, PropertyMock
import pytest
import time_machine
from freqtrade.constants import AVAILABLE_PAIRLISTS
from freqtrade.exceptions import OperationalException
@@ -11,7 +12,8 @@ from freqtrade.persistence import Trade
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.plugins.pairlistmanager import PairListManager
from freqtrade.resolvers import PairListResolver
from tests.conftest import get_patched_exchange, get_patched_freqtradebot, log_has, log_has_re
from tests.conftest import (create_mock_trades, get_patched_exchange, get_patched_freqtradebot,
log_has, log_has_re)
@pytest.fixture(scope="function")
@@ -662,6 +664,31 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None:
assert log_has("PerformanceFilter is not available in this mode.", caplog)
@pytest.mark.usefixtures("init_persistence")
def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee) -> None:
whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC')
whitelist_conf['pairlists'] = [
{"method": "StaticPairList"},
{"method": "PerformanceFilter", "minutes": 60}
]
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
exchange = get_patched_exchange(mocker, whitelist_conf)
pm = PairListManager(exchange, whitelist_conf)
pm.refresh_pairlist()
assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC']
with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
create_mock_trades(fee)
pm.refresh_pairlist()
assert pm.whitelist == ['XRP/BTC', 'ETH/BTC', 'TKN/BTC']
# Move to "outside" of lookback window, so original sorting is restored.
t.move_to("2021-09-01 07:00:00 +00:00")
pm.refresh_pairlist()
assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC']
def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}]
@@ -815,32 +842,63 @@ def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tick
def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, ohlcv_history):
ohlcv_data = {
('ETH/BTC', '1d'): ohlcv_history,
('TKN/BTC', '1d'): ohlcv_history,
('LTC/BTC', '1d'): ohlcv_history,
}
mocker.patch.multiple('freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
get_tickers=tickers
)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data),
)
with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
ohlcv_data = {
('ETH/BTC', '1d'): ohlcv_history,
('TKN/BTC', '1d'): ohlcv_history,
('LTC/BTC', '1d'): ohlcv_history,
}
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
get_tickers=tickers,
refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data),
)
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_agefilter)
assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 0
freqtrade.pairlists.refresh_pairlist()
assert len(freqtrade.pairlists.whitelist) == 3
assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_agefilter)
assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 0
freqtrade.pairlists.refresh_pairlist()
assert len(freqtrade.pairlists.whitelist) == 3
assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0
previous_call_count = freqtrade.exchange.refresh_latest_ohlcv.call_count
freqtrade.pairlists.refresh_pairlist()
assert len(freqtrade.pairlists.whitelist) == 3
# Called once for XRP/BTC
assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count + 1
freqtrade.pairlists.refresh_pairlist()
assert len(freqtrade.pairlists.whitelist) == 3
# Call to XRP/BTC cached
assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 2
ohlcv_data = {
('ETH/BTC', '1d'): ohlcv_history,
('TKN/BTC', '1d'): ohlcv_history,
('LTC/BTC', '1d'): ohlcv_history,
('XRP/BTC', '1d'): ohlcv_history.iloc[[0]],
}
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data)
freqtrade.pairlists.refresh_pairlist()
assert len(freqtrade.pairlists.whitelist) == 3
assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 1
# Move to next day
t.move_to("2021-09-02 01:00:00 +00:00")
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data)
freqtrade.pairlists.refresh_pairlist()
assert len(freqtrade.pairlists.whitelist) == 3
assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 1
# Move another day with fresh mocks (now the pair is old enough)
t.move_to("2021-09-03 01:00:00 +00:00")
# Called once for XRP/BTC
ohlcv_data = {
('ETH/BTC', '1d'): ohlcv_history,
('TKN/BTC', '1d'): ohlcv_history,
('LTC/BTC', '1d'): ohlcv_history,
('XRP/BTC', '1d'): ohlcv_history,
}
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data)
freqtrade.pairlists.refresh_pairlist()
assert len(freqtrade.pairlists.whitelist) == 4
# Called once (only for XRP/BTC)
assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 1
def test_OffsetFilter_error(mocker, whitelist_conf) -> None:

View File

@@ -422,20 +422,22 @@ def test_api_stopbuy(botclient):
assert ftbot.config['max_open_trades'] == 0
def test_api_balance(botclient, mocker, rpc_balance):
def test_api_balance(botclient, mocker, rpc_balance, tickers):
ftbot, client = botclient
ftbot.config['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination',
side_effect=lambda a, b: f"{a}/{b}")
ftbot.wallets.update()
rc = client_get(client, f"{BASE_URI}/balance")
assert_response(rc)
assert "currencies" in rc.json()
assert len(rc.json()["currencies"]) == 5
assert rc.json()['currencies'][0] == {
response = rc.json()
assert "currencies" in response
assert len(response["currencies"]) == 5
assert response['currencies'][0] == {
'currency': 'BTC',
'free': 12.0,
'balance': 12.0,
@@ -443,6 +445,10 @@ def test_api_balance(botclient, mocker, rpc_balance):
'est_stake': 12.0,
'stake': 'BTC',
}
assert 'starting_capital' in response
assert 'starting_capital_fiat' in response
assert 'starting_capital_pct' in response
assert 'starting_capital_ratio' in response
def test_api_count(botclient, mocker, ticker, fee, markets):
@@ -1218,6 +1224,7 @@ def test_api_strategies(botclient):
assert_response(rc)
assert rc.json() == {'strategies': [
'HyperoptableStrategy',
'InformativeDecoratorTest',
'StrategyTestV2',
'TestStrategyLegacyV1'
]}

View File

@@ -576,6 +576,8 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None
'total': 100.0,
'symbol': 100.0,
'value': 1000.0,
'starting_capital': 1000,
'starting_capital_fiat': 1000,
})
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)

View File

@@ -0,0 +1,75 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
from pandas import DataFrame
from freqtrade.strategy import informative, merge_informative_pair
from freqtrade.strategy.interface import IStrategy
class InformativeDecoratorTest(IStrategy):
"""
Strategy used by tests freqtrade bot.
Please do not modify this strategy, it's intended for internal use only.
Please look at the SampleStrategy in the user_data/strategy directory
or strategy repository https://github.com/freqtrade/freqtrade-strategies
for samples and inspiration.
"""
INTERFACE_VERSION = 2
stoploss = -0.10
timeframe = '5m'
startup_candle_count: int = 20
def informative_pairs(self):
return [('BTC/USDT', '5m')]
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['buy'] = 0
return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['sell'] = 0
return dataframe
# Decorator stacking test.
@informative('30m')
@informative('1h')
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['rsi'] = 14
return dataframe
# Simple informative test.
@informative('1h', 'BTC/{stake}')
def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['rsi'] = 14
return dataframe
# Quote currency different from stake currency test.
@informative('1h', 'ETH/BTC')
def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['rsi'] = 14
return dataframe
# Formatting test.
@informative('30m', 'BTC/{stake}', '{column}_{BASE}_{QUOTE}_{base}_{quote}_{asset}_{timeframe}')
def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['rsi'] = 14
return dataframe
# Custom formatter test
@informative('30m', 'ETH/{stake}', fmt=lambda column, **kwargs: column + '_from_callable')
def populate_indicators_eth_30m(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['rsi'] = 14
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Strategy timeframe indicators for current pair.
dataframe['rsi'] = 14
# Informative pairs are available in this method.
dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h']
# Mixing manual informative pairs with decorators.
informative = self.dp.get_pair_dataframe('BTC/USDT', '5m')
informative['rsi'] = 14
dataframe = merge_informative_pair(dataframe, informative, self.timeframe, '5m', ffill=True)
return dataframe

View File

@@ -648,7 +648,7 @@ def test_is_informative_pairs_callback(default_conf):
strategy = StrategyResolver.load_strategy(default_conf)
# Should return empty
# Uses fallback to base implementation
assert [] == strategy.informative_pairs()
assert [] == strategy.gather_informative_pairs()
@pytest.mark.parametrize('error', [

View File

@@ -4,7 +4,9 @@ import numpy as np
import pandas as pd
import pytest
from freqtrade.strategy import merge_informative_pair, stoploss_from_open, timeframe_to_minutes
from freqtrade.data.dataprovider import DataProvider
from freqtrade.strategy import (merge_informative_pair, stoploss_from_absolute, stoploss_from_open,
timeframe_to_minutes)
def generate_test_data(timeframe: str, size: int):
@@ -132,3 +134,65 @@ def test_stoploss_from_open():
assert stoploss == 0
else:
assert isclose(stop_price, expected_stop_price, rel_tol=0.00001)
def test_stoploss_from_absolute():
assert stoploss_from_absolute(90, 100) == 1 - (90 / 100)
assert stoploss_from_absolute(100, 100) == 0
assert stoploss_from_absolute(110, 100) == 0
assert stoploss_from_absolute(100, 0) == 1
assert stoploss_from_absolute(0, 100) == 1
def test_informative_decorator(mocker, default_conf):
test_data_5m = generate_test_data('5m', 40)
test_data_30m = generate_test_data('30m', 40)
test_data_1h = generate_test_data('1h', 40)
data = {
('XRP/USDT', '5m'): test_data_5m,
('XRP/USDT', '30m'): test_data_30m,
('XRP/USDT', '1h'): test_data_1h,
('LTC/USDT', '5m'): test_data_5m,
('LTC/USDT', '30m'): test_data_30m,
('LTC/USDT', '1h'): test_data_1h,
('BTC/USDT', '30m'): test_data_30m,
('BTC/USDT', '5m'): test_data_5m,
('BTC/USDT', '1h'): test_data_1h,
('ETH/USDT', '1h'): test_data_1h,
('ETH/USDT', '30m'): test_data_30m,
('ETH/BTC', '1h'): test_data_1h,
}
from .strats.informative_decorator_strategy import InformativeDecoratorTest
default_conf['stake_currency'] = 'USDT'
strategy = InformativeDecoratorTest(config=default_conf)
strategy.dp = DataProvider({}, None, None)
mocker.patch.object(strategy.dp, 'current_whitelist', return_value=[
'XRP/USDT', 'LTC/USDT', 'BTC/USDT'
])
assert len(strategy._ft_informative) == 6 # Equal to number of decorators used
informative_pairs = [('XRP/USDT', '1h'), ('LTC/USDT', '1h'), ('XRP/USDT', '30m'),
('LTC/USDT', '30m'), ('BTC/USDT', '1h'), ('BTC/USDT', '30m'),
('BTC/USDT', '5m'), ('ETH/BTC', '1h'), ('ETH/USDT', '30m')]
for inf_pair in informative_pairs:
assert inf_pair in strategy.gather_informative_pairs()
def test_historic_ohlcv(pair, timeframe):
return data[(pair, timeframe or strategy.timeframe)].copy()
mocker.patch('freqtrade.data.dataprovider.DataProvider.historic_ohlcv',
side_effect=test_historic_ohlcv)
analyzed = strategy.advise_all_indicators(
{p: data[(p, strategy.timeframe)] for p in ('XRP/USDT', 'LTC/USDT')})
expected_columns = [
'rsi_1h', 'rsi_30m', # Stacked informative decorators
'btc_usdt_rsi_1h', # BTC 1h informative
'rsi_BTC_USDT_btc_usdt_BTC/USDT_30m', # Column formatting
'rsi_from_callable', # Custom column formatter
'eth_btc_rsi_1h', # Quote currency not matching stake currency
'rsi', 'rsi_less', # Non-informative columns
'rsi_5m', # Manual informative dataframe
]
for _, dataframe in analyzed.items():
for col in expected_columns:
assert col in dataframe.columns

View File

@@ -35,7 +35,7 @@ def test_search_all_strategies_no_failed():
directory = Path(__file__).parent / "strats"
strategies = StrategyResolver.search_all_objects(directory, enum_failed=False)
assert isinstance(strategies, list)
assert len(strategies) == 3
assert len(strategies) == 4
assert isinstance(strategies[0], dict)
@@ -43,10 +43,10 @@ def test_search_all_strategies_with_failed():
directory = Path(__file__).parent / "strats"
strategies = StrategyResolver.search_all_objects(directory, enum_failed=True)
assert isinstance(strategies, list)
assert len(strategies) == 4
assert len(strategies) == 5
# with enum_failed=True search_all_objects() shall find 2 good strategies
# and 1 which fails to load
assert len([x for x in strategies if x['class'] is not None]) == 3
assert len([x for x in strategies if x['class'] is not None]) == 4
assert len([x for x in strategies if x['class'] is None]) == 1

View File

@@ -98,11 +98,15 @@ def test_bot_cleanup(mocker, default_conf, caplog) -> None:
assert coo_mock.call_count == 1
def test_order_dict_dry_run(default_conf, mocker, caplog) -> None:
@pytest.mark.parametrize('runmode', [
RunMode.DRY_RUN,
RunMode.LIVE
])
def test_order_dict(default_conf, mocker, runmode, caplog) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
conf = default_conf.copy()
conf['runmode'] = RunMode.DRY_RUN
conf['runmode'] = runmode
conf['order_types'] = {
'buy': 'market',
'sell': 'limit',
@@ -112,45 +116,14 @@ def test_order_dict_dry_run(default_conf, mocker, caplog) -> None:
conf['bid_strategy']['price_side'] = 'ask'
freqtrade = FreqtradeBot(conf)
if runmode == RunMode.LIVE:
assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog)
assert freqtrade.strategy.order_types['stoploss_on_exchange']
caplog.clear()
# is left untouched
conf = default_conf.copy()
conf['runmode'] = RunMode.DRY_RUN
conf['order_types'] = {
'buy': 'market',
'sell': 'limit',
'stoploss': 'limit',
'stoploss_on_exchange': False,
}
freqtrade = FreqtradeBot(conf)
assert not freqtrade.strategy.order_types['stoploss_on_exchange']
assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog)
def test_order_dict_live(default_conf, mocker, caplog) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
conf = default_conf.copy()
conf['runmode'] = RunMode.LIVE
conf['order_types'] = {
'buy': 'market',
'sell': 'limit',
'stoploss': 'limit',
'stoploss_on_exchange': True,
}
conf['bid_strategy']['price_side'] = 'ask'
freqtrade = FreqtradeBot(conf)
assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog)
assert freqtrade.strategy.order_types['stoploss_on_exchange']
caplog.clear()
# is left untouched
conf = default_conf.copy()
conf['runmode'] = RunMode.LIVE
conf['runmode'] = runmode
conf['order_types'] = {
'buy': 'market',
'sell': 'limit',
@@ -239,8 +212,14 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None:
'LTC/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21
def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None:
@pytest.mark.parametrize('buy_price_mult,ignore_strat_sl', [
# Override stoploss
(0.79, False),
# Override strategy stoploss
(0.85, True)
])
def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker,
buy_price_mult, ignore_strat_sl, edge_conf) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
patch_edge(mocker)
@@ -254,9 +233,9 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=MagicMock(return_value={
'bid': buy_price * 0.79,
'ask': buy_price * 0.79,
'last': buy_price * 0.79
'bid': buy_price * buy_price_mult,
'ask': buy_price * buy_price_mult,
'last': buy_price * buy_price_mult,
}),
get_fee=fee,
)
@@ -273,46 +252,10 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf
#############################################
# stoploss shoud be hit
assert freqtrade.handle_trade(trade) is True
assert log_has('Exit for NEO/BTC detected. Reason: stop_loss', caplog)
assert trade.sell_reason == SellType.STOP_LOSS.value
def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee,
mocker, edge_conf) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
patch_edge(mocker)
edge_conf['max_open_trades'] = float('inf')
# Strategy stoploss is -0.1 but Edge imposes a stoploss at -0.2
# Thus, if price falls 15%, stoploss should not be triggered
#
# mocking the ticker: price is falling ...
buy_price = limit_buy_order['price']
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=MagicMock(return_value={
'bid': buy_price * 0.85,
'ask': buy_price * 0.85,
'last': buy_price * 0.85
}),
get_fee=fee,
)
#############################################
# Create a trade with "limit_buy_order" price
freqtrade = FreqtradeBot(edge_conf)
freqtrade.active_pair_whitelist = ['NEO/BTC']
patch_get_signal(freqtrade)
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
freqtrade.enter_positions()
trade = Trade.query.first()
trade.update(limit_buy_order)
#############################################
# stoploss shoud not be hit
assert freqtrade.handle_trade(trade) is False
assert freqtrade.handle_trade(trade) is not ignore_strat_sl
if not ignore_strat_sl:
assert log_has('Exit for NEO/BTC detected. Reason: stop_loss', caplog)
assert trade.sell_reason == SellType.STOP_LOSS.value
def test_total_open_trades_stakes(mocker, default_conf, ticker, fee) -> None:
@@ -406,37 +349,16 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order,
@pytest.mark.parametrize("is_short", [False, True])
def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open,
limit_sell_order_open, fee, mocker, is_short) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
enter_mock = (
MagicMock(return_value=limit_sell_order_open)
if is_short else
MagicMock(return_value=limit_buy_order_open)
)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
create_order=enter_mock,
get_fee=fee,
)
default_conf['stake_amount'] = 0.0005
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade.create_trade('ETH/BTC')
rate, amount = enter_mock.call_args[1]['rate'], enter_mock.call_args[1]['amount']
assert rate * amount <= default_conf['stake_amount']
# TODO-lev: paramatrize and convert to USDT
# @pytest.mark.parametrize("stake_amount,leverage", [
# "buy, sell"
# ])
def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order_open,
fee, mocker, caplog) -> None:
@pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [
(0.0005, True, True, 99),
(0.000000005, True, False, 99),
(0, False, True, 99),
(UNLIMITED_STAKE_AMOUNT, False, True, 0),
])
def test_create_trade_minimal_amount(
default_conf, ticker, limit_buy_order_open, fee, mocker,
stake_amount, create, amount_enough, max_open_trades, caplog, is_short
) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
buy_mock = MagicMock(return_value=limit_buy_order_open)
@@ -446,58 +368,33 @@ def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_ord
create_order=buy_mock,
get_fee=fee,
)
default_conf['max_open_trades'] = max_open_trades
freqtrade = FreqtradeBot(default_conf)
freqtrade.config['stake_amount'] = 0.000000005
freqtrade.config['stake_amount'] = stake_amount
patch_get_signal(freqtrade)
assert freqtrade.create_trade('ETH/BTC')
assert log_has_re(r"Stake amount for pair .* is too small.*", caplog)
def test_create_trade_zero_stake_amount(default_conf, ticker, limit_buy_order_open,
fee, mocker) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
buy_mock = MagicMock(return_value=limit_buy_order_open)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
create_order=buy_mock,
get_fee=fee,
)
freqtrade = FreqtradeBot(default_conf)
freqtrade.config['stake_amount'] = 0
patch_get_signal(freqtrade)
assert not freqtrade.create_trade('ETH/BTC')
def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open,
fee, mocker) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
create_order=MagicMock(return_value=limit_buy_order_open),
get_fee=fee,
)
default_conf['max_open_trades'] = 0
default_conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
assert not freqtrade.create_trade('ETH/BTC')
assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0
if create:
assert freqtrade.create_trade('ETH/BTC')
if amount_enough:
rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount']
assert rate * amount <= default_conf['stake_amount']
else:
assert log_has_re(
r"Stake amount for pair .* is too small.*",
caplog
)
else:
assert not freqtrade.create_trade('ETH/BTC')
if not max_open_trades:
assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0
@pytest.mark.parametrize('whitelist,positions', [
(["ETH/BTC"], 1), # No pairs left
([], 0), # No pairs in whitelist
])
def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee,
mocker, caplog) -> None:
whitelist, positions, mocker, caplog) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch.multiple(
@@ -506,36 +403,20 @@ def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_ope
create_order=MagicMock(return_value=limit_buy_order_open),
get_fee=fee,
)
default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"]
default_conf['exchange']['pair_whitelist'] = whitelist
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
n = freqtrade.enter_positions()
assert n == 1
assert not log_has_re(r"No currency pair in active pair whitelist.*", caplog)
n = freqtrade.enter_positions()
assert n == 0
assert log_has_re(r"No currency pair in active pair whitelist.*", caplog)
def test_enter_positions_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee,
mocker, caplog) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
create_order=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
)
default_conf['exchange']['pair_whitelist'] = []
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
n = freqtrade.enter_positions()
assert n == 0
assert log_has("Active pair whitelist is empty.", caplog)
assert n == positions
if positions:
assert not log_has_re(r"No currency pair in active pair whitelist.*", caplog)
n = freqtrade.enter_positions()
assert n == 0
assert log_has_re(r"No currency pair in active pair whitelist.*", caplog)
else:
assert n == 0
assert log_has("Active pair whitelist is empty.", caplog)
@pytest.mark.usefixtures("init_persistence")
@@ -953,7 +834,7 @@ def test_execute_entry(mocker, default_conf, fee, limit_buy_order, limit_sell_or
# Fail to get price...
mocker.patch('freqtrade.exchange.Exchange.get_rate', MagicMock(return_value=0.0))
with pytest.raises(PricingError, match=f"Could not determine {'sell' if is_short else 'buy'} price."):
with pytest.raises(PricingError, match=f"Could not determine {enter_side(is_short)} price."):
freqtrade.execute_entry(pair, stake_amount, is_short=is_short)
# In case of custom entry price
@@ -1338,6 +1219,7 @@ def test_create_stoploss_order_insufficient_funds(
@pytest.mark.usefixtures("init_persistence")
def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, is_short,
limit_buy_order, limit_sell_order) -> None:
# TODO-lev: test for short
# When trailing stoploss is set
enter_order = limit_sell_order if is_short else limit_buy_order
exit_order = limit_buy_order if is_short else limit_sell_order
@@ -1437,7 +1319,8 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, is_shor
pair='ETH/BTC',
order_types=freqtrade.strategy.order_types,
stop_price=0.00002346 * 0.95,
side="sell"
side="sell",
leverage=1.0
)
# price fell below stoploss, so dry-run sells trade.
@@ -1540,6 +1423,7 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, is_s
exit_order = limit_buy_order if is_short else limit_sell_order
# When trailing stoploss is set
# TODO-lev: test for short
stoploss = MagicMock(return_value={'id': 13434334})
patch_RPCManager(mocker)
mocker.patch.multiple(
@@ -1635,7 +1519,8 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, is_s
pair='ETH/BTC',
order_types=freqtrade.strategy.order_types,
stop_price=0.00002346 * 0.96,
side="sell"
side="sell",
leverage=1.0
)
# price fell below stoploss, so dry-run sells trade.
@@ -1764,34 +1649,32 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, is
pair='NEO/BTC',
order_types=freqtrade.strategy.order_types,
stop_price=0.00002346 * 0.99,
side="sell"
side="sell",
leverage=1.0
)
def test_enter_positions(mocker, default_conf, caplog) -> None:
@pytest.mark.parametrize('return_value,side_effect,log_message', [
(False, None, 'Found no enter signals for whitelisted currencies. Trying again...'),
(None, DependencyException, 'Unable to create trade for ETH/BTC: ')
])
def test_enter_positions(mocker, default_conf, return_value, side_effect,
log_message, caplog) -> None:
caplog.set_level(logging.DEBUG)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
mock_ct = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade',
MagicMock(return_value=False))
n = freqtrade.enter_positions()
assert n == 0
assert log_has('Found no enter signals for whitelisted currencies. Trying again...', caplog)
# create_trade should be called once for every pair in the whitelist.
assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist'])
def test_enter_positions_exception(mocker, default_conf, caplog) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf)
mock_ct = mocker.patch(
'freqtrade.freqtradebot.FreqtradeBot.create_trade',
MagicMock(side_effect=DependencyException)
MagicMock(
return_value=return_value,
side_effect=side_effect
)
)
n = freqtrade.enter_positions()
assert n == 0
assert log_has(log_message, caplog)
# create_trade should be called once for every pair in the whitelist.
assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist'])
assert log_has('Unable to create trade for ETH/BTC: ', caplog)
@pytest.mark.parametrize("is_short", [False, True])
@@ -1896,9 +1779,15 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order,
@pytest.mark.parametrize("is_short", [False, True])
@pytest.mark.parametrize('initial_amount,has_rounding_fee', [
(90.99181073 + 1e-14, True),
(8.0, False)
])
def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee,
limit_sell_order, is_short, mocker):
mocker, initial_amount, has_rounding_fee,
limit_sell_order, is_short, caplog):
order = limit_sell_order if is_short else limit_buy_order
trades_for_order[0]['amount'] = initial_amount
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
# fetch_order should not be called!!
mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError))
@@ -1919,38 +1808,9 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_
)
freqtrade.update_trade_state(trade, '123456', order)
assert trade.amount != amount
assert trade.amount == order['amount']
@pytest.mark.parametrize("is_short", [False, True])
def test_update_trade_state_withorderdict_rounding_fee(
default_conf, trades_for_order, fee, limit_buy_order, limit_sell_order,
mocker, caplog, is_short
):
order = limit_sell_order if is_short else limit_buy_order
trades_for_order[0]['amount'] = order['amount'] + 1e-14
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
# fetch_order should not be called!!
mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError))
patch_exchange(mocker)
amount = sum(x['amount'] for x in trades_for_order)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
trade = Trade(
pair='LTC/ETH',
amount=amount,
exchange='binance',
open_rate=0.245441,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_order_id='123456',
is_open=True,
open_date=arrow.utcnow().datetime,
is_short=is_short
)
freqtrade.update_trade_state(trade, '123456', order)
assert trade.amount != amount
assert trade.amount == order['amount']
assert log_has_re(r'Applying fee on amount for .*', caplog)
assert trade.amount == limit_buy_order['amount']
if has_rounding_fee:
assert log_has_re(r'Applying fee on amount for .*', caplog)
@pytest.mark.parametrize("is_short", [False, True])
@@ -3354,16 +3214,28 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf, ticker, fee,
@pytest.mark.parametrize("is_short", [False, True])
def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy_order_open,
is_short, fee, mocker) -> None:
@pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,sell_type', [
# Enable profit
(True, 0.00001172, 0.00001173, False, True, SellType.SELL_SIGNAL.value),
# Disable profit
(False, 0.00002172, 0.00002173, True, False, SellType.SELL_SIGNAL.value),
# Enable loss
# * Shouldn't this be SellType.STOP_LOSS.value
(True, 0.00000172, 0.00000173, False, False, None),
# Disable loss
(False, 0.00000172, 0.00000173, True, False, SellType.SELL_SIGNAL.value),
])
def test_sell_profit_only(
default_conf, limit_buy_order, limit_buy_order_open, is_short,
fee, mocker, profit_only, bid, ask, handle_first, handle_second, sell_type) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=MagicMock(return_value={
'bid': 0.00001172,
'ask': 0.00001173,
'last': 0.00001172
'bid': bid,
'ask': ask,
'last': bid
}),
create_order=MagicMock(side_effect=[
limit_buy_order_open,
@@ -3373,131 +3245,29 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy
)
default_conf.update({
'use_sell_signal': True,
'sell_profit_only': True,
'sell_profit_only': profit_only,
'sell_profit_offset': 0.1,
})
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
if sell_type == SellType.SELL_SIGNAL.value:
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
else:
freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple(
sell_type=SellType.NONE))
freqtrade.enter_positions()
trade = Trade.query.first()
trade.update(limit_buy_order)
freqtrade.wallets.update()
patch_get_signal(freqtrade, enter_long=False, exit_long=True)
assert freqtrade.handle_trade(trade) is False
assert freqtrade.handle_trade(trade) is handle_first
freqtrade.strategy.sell_profit_offset = 0.0
assert freqtrade.handle_trade(trade) is True
if handle_second:
freqtrade.strategy.sell_profit_offset = 0.0
assert freqtrade.handle_trade(trade) is True
assert trade.sell_reason == SellType.SELL_SIGNAL.value
@pytest.mark.parametrize("is_short", [False, True])
def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, limit_buy_order_open,
is_short, fee, mocker) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=MagicMock(return_value={
'bid': 0.00002172,
'ask': 0.00002173,
'last': 0.00002172
}),
create_order=MagicMock(side_effect=[
limit_buy_order_open,
{'id': 1234553382},
]),
get_fee=fee,
)
default_conf.update({
'use_sell_signal': True,
'sell_profit_only': False,
})
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
freqtrade.enter_positions()
trade = Trade.query.first()
trade.update(limit_buy_order)
freqtrade.wallets.update()
patch_get_signal(freqtrade, enter_long=False, exit_long=True)
assert freqtrade.handle_trade(trade) is True
assert trade.sell_reason == SellType.SELL_SIGNAL.value
@pytest.mark.parametrize("is_short", [False, True])
def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_order_open,
is_short, fee, mocker) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=MagicMock(return_value={
'bid': 0.00000172,
'ask': 0.00000173,
'last': 0.00000172
}),
create_order=MagicMock(side_effect=[
limit_buy_order_open,
{'id': 1234553382},
]),
get_fee=fee,
)
default_conf.update({
'use_sell_signal': True,
'sell_profit_only': True,
})
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple(
sell_type=SellType.NONE))
freqtrade.enter_positions()
trade = Trade.query.first()
trade.update(limit_buy_order)
patch_get_signal(freqtrade, enter_long=False, exit_long=True)
assert freqtrade.handle_trade(trade) is False
@pytest.mark.parametrize("is_short", [False, True])
def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, limit_buy_order_open,
is_short, fee, mocker) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=MagicMock(return_value={
'bid': 0.0000172,
'ask': 0.0000173,
'last': 0.0000172
}),
create_order=MagicMock(side_effect=[
limit_buy_order_open,
{'id': 1234553382},
]),
get_fee=fee,
)
default_conf.update({
'use_sell_signal': True,
'sell_profit_only': False,
})
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
freqtrade.enter_positions()
trade = Trade.query.first()
trade.update(limit_buy_order)
freqtrade.wallets.update()
patch_get_signal(freqtrade, enter_long=False, exit_long=True)
assert freqtrade.handle_trade(trade) is True
assert trade.sell_reason == SellType.SELL_SIGNAL.value
assert trade.sell_reason == sell_type
@pytest.mark.parametrize("is_short", [False, True])
@@ -3536,11 +3306,15 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_
assert trade.amount != amnt
def test__safe_exit_amount(default_conf, fee, caplog, mocker):
@pytest.mark.parametrize('amount_wallet,has_err', [
(95.29, False),
(91.29, True)
])
def test__safe_exit_amount(default_conf, fee, caplog, mocker, amount_wallet, has_err):
patch_RPCManager(mocker)
patch_exchange(mocker)
amount = 95.33
amount_wallet = 95.29
amount_wallet = amount_wallet
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet))
wallet_update = mocker.patch('freqtrade.wallets.Wallets.update')
trade = Trade(
@@ -3554,37 +3328,19 @@ def test__safe_exit_amount(default_conf, fee, caplog, mocker):
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
wallet_update.reset_mock()
assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet
assert log_has_re(r'.*Falling back to wallet-amount.', caplog)
assert wallet_update.call_count == 1
caplog.clear()
wallet_update.reset_mock()
assert freqtrade._safe_exit_amount(trade.pair, amount_wallet) == amount_wallet
assert not log_has_re(r'.*Falling back to wallet-amount.', caplog)
assert wallet_update.call_count == 1
def test__safe_exit_amount_error(default_conf, fee, caplog, mocker):
patch_RPCManager(mocker)
patch_exchange(mocker)
amount = 95.33
amount_wallet = 91.29
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet))
trade = Trade(
pair='LTC/ETH',
amount=amount,
exchange='binance',
open_rate=0.245441,
open_order_id="123456",
fee_open=fee.return_value,
fee_close=fee.return_value,
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
with pytest.raises(DependencyException, match=r"Not enough amount to exit."):
assert freqtrade._safe_exit_amount(trade.pair, trade.amount)
if has_err:
with pytest.raises(DependencyException, match=r"Not enough amount to exit trade."):
assert freqtrade._safe_exit_amount(trade.pair, trade.amount)
else:
wallet_update.reset_mock()
assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet
assert log_has_re(r'.*Falling back to wallet-amount.', caplog)
assert wallet_update.call_count == 1
caplog.clear()
wallet_update.reset_mock()
assert freqtrade._safe_exit_amount(trade.pair, amount_wallet) == amount_wallet
assert not log_has_re(r'.*Falling back to wallet-amount.', caplog)
assert wallet_update.call_count == 1
@pytest.mark.parametrize("is_short", [False, True])
@@ -3847,8 +3603,8 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, limit_buy_orde
def test_tsl_only_offset_reached(default_conf, limit_buy_order_usdt, limit_buy_order_usdt_open,
fee, is_short, limit_sell_order_usdt,
limit_sell_order_usdt_open, caplog, mocker) -> None:
limit_order = limit_sell_usdt_order if is_short else limit_buy_order_usdt
limit_order_open = limit_sell_order_usdt_open if is_short else limit_buy_order_open_usdt
limit_order = limit_sell_order_usdt if is_short else limit_buy_order_usdt
limit_order_open = limit_sell_order_usdt_open if is_short else limit_buy_order_usdt_open
enter_price = limit_order['price']
# enter_price: 2.0
@@ -3885,9 +3641,9 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order_usdt, limit_buy_o
# Raise ticker above buy price
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
MagicMock(return_value={
'bid': buy_price + 0.0000004,
'ask': buy_price + 0.0000004,
'last': buy_price + 0.0000004
'bid': enter_price + 0.0000004,
'ask': enter_price + 0.0000004,
'last': enter_price + 0.0000004
}))
# stop-loss should not be adjusted as offset is not reached yet
@@ -3900,9 +3656,9 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order_usdt, limit_buy_o
# price rises above the offset (rises 12% when the offset is 5.5%)
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
MagicMock(return_value={
'bid': buy_price + 0.0000014,
'ask': buy_price + 0.0000014,
'last': buy_price + 0.0000014
'bid': enter_price + 0.0000014,
'ask': enter_price + 0.0000014,
'last': enter_price + 0.0000014
}))
assert freqtrade.handle_trade(trade) is False
@@ -4384,50 +4140,37 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o
assert trade is None
def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2) -> None:
@pytest.mark.parametrize('exception_thrown,ask,last,order_book_top,order_book', [
(False, 0.045, 0.046, 2, None),
(True, 0.042, 0.046, 1, {'bids': [[]], 'asks': [[]]})
])
def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, exception_thrown,
ask, last, order_book_top, order_book, caplog) -> None:
"""
test if function get_rate will return the order book price
instead of the ask rate
test if function get_rate will return the order book price instead of the ask rate
"""
patch_exchange(mocker)
ticker_mock = MagicMock(return_value={'ask': 0.045, 'last': 0.046})
ticker_mock = MagicMock(return_value={'ask': ask, 'last': last})
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_l2_order_book=order_book_l2,
fetch_l2_order_book=MagicMock(return_value=order_book) if order_book else order_book_l2,
fetch_ticker=ticker_mock,
)
default_conf['exchange']['name'] = 'binance'
default_conf['bid_strategy']['use_order_book'] = True
default_conf['bid_strategy']['order_book_top'] = 2
default_conf['bid_strategy']['order_book_top'] = order_book_top
default_conf['bid_strategy']['ask_last_balance'] = 0
default_conf['telegram']['enabled'] = False
freqtrade = FreqtradeBot(default_conf)
assert freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") == 0.043935
assert ticker_mock.call_count == 0
def test_order_book_bid_strategy_exception(mocker, default_conf, caplog) -> None:
patch_exchange(mocker)
ticker_mock = MagicMock(return_value={'ask': 0.042, 'last': 0.046})
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_l2_order_book=MagicMock(return_value={'bids': [[]], 'asks': [[]]}),
fetch_ticker=ticker_mock,
)
default_conf['exchange']['name'] = 'binance'
default_conf['bid_strategy']['use_order_book'] = True
default_conf['bid_strategy']['order_book_top'] = 1
default_conf['bid_strategy']['ask_last_balance'] = 0
default_conf['telegram']['enabled'] = False
freqtrade = FreqtradeBot(default_conf)
# orderbook shall be used even if tickers would be lower.
with pytest.raises(PricingError):
freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy")
assert log_has_re(r'Buy Price at location 1 from orderbook could not be determined.', caplog)
if exception_thrown:
with pytest.raises(PricingError):
freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy")
assert log_has_re(
r'Buy Price at location 1 from orderbook could not be determined.', caplog)
else:
assert freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") == 0.043935
assert ticker_mock.call_count == 0
def test_check_depth_of_market(default_conf, mocker, order_book_l2) -> None:

View File

@@ -0,0 +1,32 @@
import time_machine
from freqtrade.configuration import PeriodicCache
def test_ttl_cache():
with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
cache = PeriodicCache(5, ttl=60)
cache1h = PeriodicCache(5, ttl=3600)
assert cache.timer() == 1630472400.0
cache['a'] = 1235
cache1h['a'] = 555123
assert 'a' in cache
assert 'a' in cache1h
t.move_to("2021-09-01 05:00:59 +00:00")
assert 'a' in cache
assert 'a' in cache1h
# Cache expired
t.move_to("2021-09-01 05:01:00 +00:00")
assert 'a' not in cache
assert 'a' in cache1h
t.move_to("2021-09-01 05:59:59 +00:00")
assert 'a' in cache1h
t.move_to("2021-09-01 06:00:00 +00:00")
assert 'a' not in cache1h