merge develop into feat/shuffle_after_split

This commit is contained in:
robcaulk
2023-02-16 18:46:01 +01:00
203 changed files with 12376 additions and 8494 deletions

View File

@@ -746,9 +746,7 @@ def test_download_data_no_exchange(mocker, caplog):
start_download_data(pargs)
def test_download_data_no_pairs(mocker, caplog):
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
def test_download_data_no_pairs(mocker):
mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data',
MagicMock(return_value=["ETH/BTC", "XRP/BTC"]))
@@ -770,8 +768,6 @@ def test_download_data_no_pairs(mocker, caplog):
def test_download_data_all_pairs(mocker, markets):
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data',
MagicMock(return_value=["ETH/BTC", "XRP/BTC"]))
patch_exchange(mocker)
@@ -1454,10 +1450,10 @@ def test_start_list_data(testdatadir, capsys):
start_list_data(pargs)
captured = capsys.readouterr()
assert "Found 5 pair / timeframe combinations." in captured.out
assert "\n| Pair | Timeframe | Type |\n" in captured.out
assert "\n| XRP/USDT | 1h | futures |\n" in captured.out
assert "\n| XRP/USDT | 1h, 8h | mark |\n" in captured.out
assert "Found 6 pair / timeframe combinations." in captured.out
assert "\n| Pair | Timeframe | Type |\n" in captured.out
assert "\n| XRP/USDT:USDT | 5m, 1h | futures |\n" in captured.out
assert "\n| XRP/USDT:USDT | 1h, 8h | mark |\n" in captured.out
args = [
"list-data",
@@ -1529,7 +1525,7 @@ def test_backtesting_show(mocker, testdatadir, capsys):
args = [
"backtesting-show",
"--export-filename",
f"{testdatadir / 'backtest_results/backtest-result_new.json'}",
f"{testdatadir / 'backtest_results/backtest-result.json'}",
"--show-pair-list"
]
pargs = get_args(args)

View File

@@ -241,7 +241,6 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
:return: FreqtradeBot
"""
patch_freqtradebot(mocker, config)
config['datadir'] = Path(config['datadir'])
return FreqtradeBot(config)
@@ -510,7 +509,7 @@ def get_default_conf(testdatadir):
"chat_id": "0",
"notification_settings": {},
},
"datadir": str(testdatadir),
"datadir": Path(testdatadir),
"initial_state": "running",
"db_url": "sqlite://",
"user_data_dir": Path("user_data"),
@@ -2606,6 +2605,8 @@ def open_trade():
ft_order_side='buy',
ft_pair=trade.pair,
ft_is_open=False,
ft_amount=trade.amount,
ft_price=trade.open_rate,
order_id='123456789',
status="closed",
symbol=trade.pair,
@@ -2642,6 +2643,8 @@ def open_trade_usdt():
ft_order_side='buy',
ft_pair=trade.pair,
ft_is_open=False,
ft_amount=trade.amount,
ft_price=trade.open_rate,
order_id='123456789',
status="closed",
symbol=trade.pair,
@@ -2659,6 +2662,8 @@ def open_trade_usdt():
ft_order_side='exit',
ft_pair=trade.pair,
ft_is_open=True,
ft_amount=trade.amount,
ft_price=trade.open_rate,
order_id='123456789_exit',
status="open",
symbol=trade.pair,
@@ -3103,7 +3108,7 @@ def funding_rate_history_octohourly():
@pytest.fixture(scope='function')
def leverage_tiers():
return {
"1000SHIB/USDT": [
"1000SHIB/USDT:USDT": [
{
'minNotional': 0,
'maxNotional': 50000,
@@ -3154,7 +3159,7 @@ def leverage_tiers():
'maintAmt': 654500.0
},
],
"1INCH/USDT": [
"1INCH/USDT:USDT": [
{
'minNotional': 0,
'maxNotional': 5000,
@@ -3198,7 +3203,7 @@ def leverage_tiers():
'maintAmt': 386940.0
},
],
"AAVE/USDT": [
"AAVE/USDT:USDT": [
{
'minNotional': 0,
'maxNotional': 5000,
@@ -3242,7 +3247,7 @@ def leverage_tiers():
'maintAmt': 386950.0
},
],
"ADA/BUSD": [
"ADA/BUSD:BUSD": [
{
"minNotional": 0,
"maxNotional": 100000,
@@ -3286,7 +3291,7 @@ def leverage_tiers():
"maintAmt": 1527500.0
},
],
'BNB/BUSD': [
'BNB/BUSD:BUSD': [
{
"minNotional": 0, # stake(before leverage) = 0
"maxNotional": 100000, # max stake(before leverage) = 5000
@@ -3330,7 +3335,7 @@ def leverage_tiers():
"maintAmt": 1527500.0
}
],
'BNB/USDT': [
'BNB/USDT:USDT': [
{
"minNotional": 0, # stake = 0.0
"maxNotional": 10000, # max_stake = 133.33333333333334
@@ -3395,7 +3400,7 @@ def leverage_tiers():
"maintAmt": 6233035.0
},
],
'BTC/USDT': [
'BTC/USDT:USDT': [
{
"minNotional": 0, # stake = 0.0
"maxNotional": 50000, # max_stake = 400.0
@@ -3467,7 +3472,7 @@ def leverage_tiers():
"maintAmt": 1.997038E8
},
],
"ZEC/USDT": [
"ZEC/USDT:USDT": [
{
'minNotional': 0,
'maxNotional': 50000,

View File

@@ -12,9 +12,11 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelis
get_latest_hyperopt_file, load_backtest_data,
load_backtest_metadata, load_trades, load_trades_from_db)
from freqtrade.data.history import load_data, load_pair_history
from freqtrade.data.metrics import (calculate_cagr, calculate_csum, calculate_market_change,
calculate_max_drawdown, calculate_underwater,
combine_dataframes_with_mean, create_cum_profit)
from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum,
calculate_expectancy, calculate_market_change,
calculate_max_drawdown, calculate_sharpe, calculate_sortino,
calculate_underwater, combine_dataframes_with_mean,
create_cum_profit)
from freqtrade.exceptions import OperationalException
from tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades
from tests.conftest_trades import MOCK_TRADE_COUNT
@@ -30,10 +32,10 @@ def test_get_latest_backtest_filename(testdatadir, mocker):
testdir_bt = testdatadir / "backtest_results"
res = get_latest_backtest_filename(testdir_bt)
assert res == 'backtest-result_new.json'
assert res == 'backtest-result.json'
res = get_latest_backtest_filename(str(testdir_bt))
assert res == 'backtest-result_new.json'
assert res == 'backtest-result.json'
mocker.patch("freqtrade.data.btanalysis.json_load", return_value={})
@@ -81,7 +83,7 @@ def test_load_backtest_data_old_format(testdatadir, mocker):
def test_load_backtest_data_new_format(testdatadir):
filename = testdatadir / "backtest_results/backtest-result_new.json"
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
assert isinstance(bt_data, DataFrame)
assert set(bt_data.columns) == set(BT_DATA_COLUMNS)
@@ -182,7 +184,7 @@ def test_extract_trades_of_period(testdatadir):
def test_analyze_trade_parallelism(testdatadir):
filename = testdatadir / "backtest_results/backtest-result_new.json"
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
res = analyze_trade_parallelism(bt_data, "5m")
@@ -256,7 +258,7 @@ def test_combine_dataframes_with_mean_no_data(testdatadir):
def test_create_cum_profit(testdatadir):
filename = testdatadir / "backtest_results/backtest-result_new.json"
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
timerange = TimeRange.parse_timerange("20180110-20180112")
@@ -268,11 +270,11 @@ def test_create_cum_profit(testdatadir):
"cum_profits", timeframe="5m")
assert "cum_profits" in cum_profits.columns
assert cum_profits.iloc[0]['cum_profits'] == 0
assert pytest.approx(cum_profits.iloc[-1]['cum_profits']) == 8.723007518796964e-06
assert pytest.approx(cum_profits.iloc[-1]['cum_profits']) == 9.0225563e-05
def test_create_cum_profit1(testdatadir):
filename = testdatadir / "backtest_results/backtest-result_new.json"
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
# Move close-time to "off" the candle, to make sure the logic still works
bt_data['close_date'] = bt_data.loc[:, 'close_date'] + DateOffset(seconds=20)
@@ -286,7 +288,7 @@ def test_create_cum_profit1(testdatadir):
"cum_profits", timeframe="5m")
assert "cum_profits" in cum_profits.columns
assert cum_profits.iloc[0]['cum_profits'] == 0
assert pytest.approx(cum_profits.iloc[-1]['cum_profits']) == 8.723007518796964e-06
assert pytest.approx(cum_profits.iloc[-1]['cum_profits']) == 9.0225563e-05
with pytest.raises(ValueError, match='Trade dataframe empty.'):
create_cum_profit(df.set_index('date'), bt_data[bt_data["pair"] == 'NOTAPAIR'],
@@ -294,18 +296,18 @@ def test_create_cum_profit1(testdatadir):
def test_calculate_max_drawdown(testdatadir):
filename = testdatadir / "backtest_results/backtest-result_new.json"
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
_, hdate, lowdate, hval, lval, drawdown = calculate_max_drawdown(
bt_data, value_col="profit_abs")
assert isinstance(drawdown, float)
assert pytest.approx(drawdown) == 0.12071099
assert pytest.approx(drawdown) == 0.29753914
assert isinstance(hdate, Timestamp)
assert isinstance(lowdate, Timestamp)
assert isinstance(hval, float)
assert isinstance(lval, float)
assert hdate == Timestamp('2018-01-25 01:30:00', tz='UTC')
assert lowdate == Timestamp('2018-01-25 03:50:00', tz='UTC')
assert hdate == Timestamp('2018-01-16 19:30:00', tz='UTC')
assert lowdate == Timestamp('2018-01-16 22:25:00', tz='UTC')
underwater = calculate_underwater(bt_data)
assert isinstance(underwater, DataFrame)
@@ -318,14 +320,15 @@ def test_calculate_max_drawdown(testdatadir):
def test_calculate_csum(testdatadir):
filename = testdatadir / "backtest_results/backtest-result_new.json"
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
csum_min, csum_max = calculate_csum(bt_data)
assert isinstance(csum_min, float)
assert isinstance(csum_max, float)
assert csum_min < 0.01
assert csum_max > 0.02
assert csum_min < csum_max
assert csum_min < 0.0001
assert csum_max > 0.0002
csum_min1, csum_max1 = calculate_csum(bt_data, 5)
assert csum_min1 == csum_min + 5
@@ -335,6 +338,69 @@ def test_calculate_csum(testdatadir):
csum_min, csum_max = calculate_csum(DataFrame())
def test_calculate_expectancy(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
expectancy = calculate_expectancy(DataFrame())
assert expectancy == 0.0
expectancy = calculate_expectancy(bt_data)
assert isinstance(expectancy, float)
assert pytest.approx(expectancy) == 0.07151374226574791
def test_calculate_sortino(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
sortino = calculate_sortino(DataFrame(), None, None, 0)
assert sortino == 0.0
sortino = calculate_sortino(
bt_data,
bt_data['open_date'].min(),
bt_data['close_date'].max(),
0.01,
)
assert isinstance(sortino, float)
assert pytest.approx(sortino) == 35.17722
def test_calculate_sharpe(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
sharpe = calculate_sharpe(DataFrame(), None, None, 0)
assert sharpe == 0.0
sharpe = calculate_sharpe(
bt_data,
bt_data['open_date'].min(),
bt_data['close_date'].max(),
0.01,
)
assert isinstance(sharpe, float)
assert pytest.approx(sharpe) == 44.5078669
def test_calculate_calmar(testdatadir):
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
calmar = calculate_calmar(DataFrame(), None, None, 0)
assert calmar == 0.0
calmar = calculate_calmar(
bt_data,
bt_data['open_date'].min(),
bt_data['close_date'].max(),
0.01,
)
assert isinstance(calmar, float)
assert pytest.approx(calmar) == 559.040508
@pytest.mark.parametrize('start,end,days, expected', [
(64900, 176000, 3 * 365, 0.3945),
(64900, 176000, 365, 1.7119),

View File

@@ -294,8 +294,8 @@ def test_convert_trades_format(default_conf, testdatadir, tmpdir):
@pytest.mark.parametrize('file_base,candletype', [
(['XRP_ETH-5m', 'XRP_ETH-1m'], CandleType.SPOT),
(['UNITTEST_USDT-1h-mark', 'XRP_USDT-1h-mark'], CandleType.MARK),
(['XRP_USDT-1h-futures'], CandleType.FUTURES),
(['UNITTEST_USDT_USDT-1h-mark', 'XRP_USDT_USDT-1h-mark'], CandleType.MARK),
(['XRP_USDT_USDT-1h-futures'], CandleType.FUTURES),
])
def test_convert_ohlcv_format(default_conf, testdatadir, tmpdir, file_base, candletype):
tmpdir1 = Path(tmpdir)
@@ -315,7 +315,10 @@ def test_convert_ohlcv_format(default_conf, testdatadir, tmpdir, file_base, cand
files_new.append(file_new)
default_conf['datadir'] = tmpdir1
default_conf['pairs'] = ['XRP_ETH', 'XRP_USDT', 'UNITTEST_USDT']
if candletype == CandleType.SPOT:
default_conf['pairs'] = ['XRP/ETH', 'XRP/USDT', 'UNITTEST/USDT']
else:
default_conf['pairs'] = ['XRP/ETH:ETH', 'XRP/USDT:USDT', 'UNITTEST/USDT:USDT']
default_conf['timeframes'] = ['1m', '5m', '1h']
assert not file_new.exists()

View File

@@ -33,10 +33,10 @@ def test_datahandler_ohlcv_get_pairs(testdatadir):
assert set(pairs) == {'UNITTEST/BTC'}
pairs = JsonDataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.MARK)
assert set(pairs) == {'UNITTEST/USDT', 'XRP/USDT'}
assert set(pairs) == {'UNITTEST/USDT:USDT', 'XRP/USDT:USDT'}
pairs = JsonGzDataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.FUTURES)
assert set(pairs) == {'XRP/USDT'}
assert set(pairs) == {'XRP/USDT:USDT'}
pairs = HDF5DataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.MARK)
assert set(pairs) == {'UNITTEST/USDT:USDT'}
@@ -104,11 +104,12 @@ def test_datahandler_ohlcv_get_available_data(testdatadir):
paircombs = JsonDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.FUTURES)
# Convert to set to avoid failures due to sorting
assert set(paircombs) == {
('UNITTEST/USDT', '1h', 'mark'),
('XRP/USDT', '1h', 'futures'),
('XRP/USDT', '1h', 'mark'),
('XRP/USDT', '8h', 'mark'),
('XRP/USDT', '8h', 'funding_rate'),
('UNITTEST/USDT:USDT', '1h', 'mark'),
('XRP/USDT:USDT', '5m', 'futures'),
('XRP/USDT:USDT', '1h', 'futures'),
('XRP/USDT:USDT', '1h', 'mark'),
('XRP/USDT:USDT', '8h', 'mark'),
('XRP/USDT:USDT', '8h', 'funding_rate'),
}
paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.SPOT)
@@ -142,7 +143,7 @@ def test_jsondatahandler_ohlcv_load(testdatadir, caplog):
df = dh.ohlcv_load('XRP/ETH', '5m', 'spot')
assert len(df) == 712
df_mark = dh.ohlcv_load('UNITTEST/USDT', '1h', candle_type="mark")
df_mark = dh.ohlcv_load('UNITTEST/USDT:USDT', '1h', candle_type="mark")
assert len(df_mark) == 100
df_no_mark = dh.ohlcv_load('UNITTEST/USDT', '1h', 'spot')
@@ -424,7 +425,7 @@ def test_hdf5datahandler_ohlcv_load_and_resave(
# Data goes from 2018-01-10 - 2018-01-30
('UNITTEST/BTC', '5m', 'spot', '', '2018-01-15', '2018-01-19'),
# Mark data goes from to 2021-11-15 2021-11-19
('UNITTEST/USDT', '1h', 'mark', '-mark', '2021-11-16', '2021-11-18'),
('UNITTEST/USDT:USDT', '1h', 'mark', '-mark', '2021-11-16', '2021-11-18'),
])
@pytest.mark.parametrize('datahandler', ['hdf5', 'feather', 'parquet'])
def test_generic_datahandler_ohlcv_load_and_resave(

View File

@@ -2,13 +2,13 @@ from datetime import datetime, timezone
from unittest.mock import MagicMock
import pytest
from pandas import DataFrame
from pandas import DataFrame, Timestamp
from freqtrade.data.dataprovider import DataProvider
from freqtrade.enums import CandleType, RunMode
from freqtrade.exceptions import ExchangeError, OperationalException
from freqtrade.plugins.pairlistmanager import PairListManager
from tests.conftest import get_patched_exchange
from tests.conftest import generate_test_data, get_patched_exchange
@pytest.mark.parametrize('candle_type', [
@@ -144,7 +144,7 @@ def test_available_pairs(mocker, default_conf, ohlcv_history):
assert dp.available_pairs == [("XRP/BTC", timeframe), ("UNITTEST/BTC", timeframe), ]
def test_producer_pairs(mocker, default_conf, ohlcv_history):
def test_producer_pairs(default_conf):
dataprovider = DataProvider(default_conf, None)
producer = "default"
@@ -161,9 +161,9 @@ def test_producer_pairs(mocker, default_conf, ohlcv_history):
assert dataprovider.get_producer_pairs("bad") == []
def test_get_producer_df(mocker, default_conf, ohlcv_history):
def test_get_producer_df(default_conf):
dataprovider = DataProvider(default_conf, None)
ohlcv_history = generate_test_data('5m', 150)
pair = 'BTC/USDT'
timeframe = default_conf['timeframe']
candle_type = CandleType.SPOT
@@ -221,7 +221,7 @@ def test_emit_df(mocker, default_conf, ohlcv_history):
assert send_mock.call_count == 0
def test_refresh(mocker, default_conf, ohlcv_history):
def test_refresh(mocker, default_conf):
refresh_mock = MagicMock()
mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock)
@@ -412,3 +412,85 @@ def test_dp_send_msg(default_conf):
dp = DataProvider(default_conf, None)
dp.send_msg(msg, always_send=True)
assert msg not in dp._msg_queue
def test_dp__add_external_df(default_conf_usdt):
timeframe = '1h'
default_conf_usdt["timeframe"] = timeframe
dp = DataProvider(default_conf_usdt, None)
df = generate_test_data(timeframe, 24, '2022-01-01 00:00:00+00:00')
last_analyzed = datetime.now(timezone.utc)
res = dp._add_external_df('ETH/USDT', df, last_analyzed, timeframe, CandleType.SPOT)
assert res[0] is False
# Why 1000 ??
assert res[1] == 1000
# Hard add dataframe
dp._replace_external_df('ETH/USDT', df, last_analyzed, timeframe, CandleType.SPOT)
# BTC is not stored yet
res = dp._add_external_df('BTC/USDT', df, last_analyzed, timeframe, CandleType.SPOT)
assert res[0] is False
df_res, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
assert len(df_res) == 24
# Add the same dataframe again - dataframe size shall not change.
res = dp._add_external_df('ETH/USDT', df, last_analyzed, timeframe, CandleType.SPOT)
assert res[0] is True
assert isinstance(res[1], int)
assert res[1] == 0
df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
assert len(df) == 24
# Add a new day.
df2 = generate_test_data(timeframe, 24, '2022-01-02 00:00:00+00:00')
res = dp._add_external_df('ETH/USDT', df2, last_analyzed, timeframe, CandleType.SPOT)
assert res[0] is True
assert isinstance(res[1], int)
assert res[1] == 0
df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
assert len(df) == 48
# Add a dataframe with a 12 hour offset - so 12 candles are overlapping, and 12 valid.
df3 = generate_test_data(timeframe, 24, '2022-01-02 12:00:00+00:00')
res = dp._add_external_df('ETH/USDT', df3, last_analyzed, timeframe, CandleType.SPOT)
assert res[0] is True
assert isinstance(res[1], int)
assert res[1] == 0
df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
# New length = 48 + 12 (since we have a 12 hour offset).
assert len(df) == 60
assert df.iloc[-1]['date'] == df3.iloc[-1]['date']
assert df.iloc[-1]['date'] == Timestamp('2022-01-03 11:00:00+00:00')
# Generate 1 new candle
df4 = generate_test_data(timeframe, 1, '2022-01-03 12:00:00+00:00')
res = dp._add_external_df('ETH/USDT', df4, last_analyzed, timeframe, CandleType.SPOT)
# assert res[0] is True
# assert res[1] == 0
df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
# New length = 61 + 1
assert len(df) == 61
assert df.iloc[-2]['date'] == Timestamp('2022-01-03 11:00:00+00:00')
assert df.iloc[-1]['date'] == Timestamp('2022-01-03 12:00:00+00:00')
# Gap in the data ...
df4 = generate_test_data(timeframe, 1, '2022-01-05 00:00:00+00:00')
res = dp._add_external_df('ETH/USDT', df4, last_analyzed, timeframe, CandleType.SPOT)
assert res[0] is False
# 36 hours - from 2022-01-03 12:00:00+00:00 to 2022-01-05 00:00:00+00:00
assert isinstance(res[1], int)
assert res[1] == 36
df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT)
# New length = 61 + 1
assert len(df) == 61
# Empty dataframe
df4 = generate_test_data(timeframe, 0, '2022-01-05 00:00:00+00:00')
res = dp._add_external_df('ETH/USDT', df4, last_analyzed, timeframe, CandleType.SPOT)
assert res[0] is False
# 36 hours - from 2022-01-03 12:00:00+00:00 to 2022-01-05 00:00:00+00:00
assert isinstance(res[1], int)
assert res[1] == 0

View File

@@ -190,6 +190,15 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, tmp
assert '1' in captured.out
assert '2.5' in captured.out
# test group 5
args = get_args(base_args + ['--analysis-groups', "5"])
start_analysis_entries_exits(args)
captured = capsys.readouterr()
assert 'exit_signal' in captured.out
assert 'roi' in captured.out
assert 'stop_loss' in captured.out
assert 'trailing_stop_loss' in captured.out
# test date filtering
args = get_args(base_args + ['--timerange', "20180129-20180130"])
start_analysis_entries_exits(args)

View File

@@ -78,11 +78,11 @@ def test_load_data_1min_timeframe(ohlcv_history, mocker, caplog, testdatadir) ->
def test_load_data_mark(ohlcv_history, mocker, caplog, testdatadir) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history)
file = testdatadir / 'futures/UNITTEST_USDT-1h-mark.json'
file = testdatadir / 'futures/UNITTEST_USDT_USDT-1h-mark.json'
load_data(datadir=testdatadir, timeframe='1h', pairs=['UNITTEST/BTC'], candle_type='mark')
assert file.is_file()
assert not log_has(
'Download history data for pair: "UNITTEST/USDT", interval: 1m '
'Download history data for pair: "UNITTEST/USDT:USDT", interval: 1m '
'and store in None.', caplog
)

View File

@@ -139,7 +139,7 @@ def test_adjust(mocker, edge_conf):
assert (edge.adjust(pairs) == ['E/F', 'C/D'])
def test_stoploss(mocker, edge_conf):
def test_edge_get_stoploss(mocker, edge_conf):
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
@@ -150,10 +150,10 @@ def test_stoploss(mocker, edge_conf):
}
))
assert edge.stoploss('E/F') == -0.01
assert edge.get_stoploss('E/F') == -0.01
def test_nonexisting_stoploss(mocker, edge_conf):
def test_nonexisting_get_stoploss(mocker, edge_conf):
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
@@ -162,7 +162,7 @@ def test_nonexisting_stoploss(mocker, edge_conf):
}
))
assert edge.stoploss('N/O') == -0.1
assert edge.get_stoploss('N/O') == -0.1
def test_edge_stake_amount(mocker, edge_conf):

View File

@@ -20,10 +20,10 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers
(0.99, 220 * 1.01, "buy"),
(0.98, 220 * 1.02, "buy"),
])
def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side, trademode):
def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expected, side, trademode):
api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
order_type = 'stop_loss_limit' if trademode == TradingMode.SPOT else 'limit'
order_type = 'stop_loss_limit' if trademode == TradingMode.SPOT else 'stop'
api_mock.create_order = MagicMock(return_value={
'id': order_id,
@@ -40,7 +40,7 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
with pytest.raises(OperationalException):
order = exchange.stoploss(
order = exchange.create_stoploss(
pair='ETH/BTC',
amount=1,
stop_price=190,
@@ -50,11 +50,11 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side
)
api_mock.create_order.reset_mock()
order_types = {'stoploss': 'limit'}
order_types = {'stoploss': 'limit', 'stoploss_price_type': 'mark'}
if limitratio is not None:
order_types.update({'stoploss_on_exchange_limit_ratio': limitratio})
order = exchange.stoploss(
order = exchange.create_stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
@@ -75,14 +75,14 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side
if trademode == TradingMode.SPOT:
params_dict = {'stopPrice': 220}
else:
params_dict = {'stopPrice': 220, 'reduceOnly': True}
params_dict = {'stopPrice': 220, 'reduceOnly': True, 'workingType': 'MARK_PRICE'}
assert api_mock.create_order.call_args_list[0][1]['params'] == params_dict
# 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, 'binance')
exchange.stoploss(
exchange.create_stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
@@ -94,7 +94,7 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side
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(
exchange.create_stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
@@ -104,12 +104,12 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side
)
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance",
"stoploss", "create_order", retries=1,
"create_stoploss", "create_order", retries=1,
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):
def test_create_stoploss_order_dry_run_binance(default_conf, mocker):
api_mock = MagicMock()
order_type = 'stop_loss_limit'
default_conf['dry_run'] = True
@@ -119,7 +119,7 @@ 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(
order = exchange.create_stoploss(
pair='ETH/BTC',
amount=1,
stop_price=190,
@@ -130,7 +130,7 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker):
api_mock.create_order.reset_mock()
order = exchange.stoploss(
order = exchange.create_stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
@@ -522,8 +522,15 @@ def test__set_leverage_binance(mocker, default_conf):
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)
default_conf['trading_mode'] = TradingMode.FUTURES
default_conf['margin_mode'] = MarginMode.ISOLATED
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
exchange._set_leverage(3.2, 'BTC/USDT:USDT')
assert api_mock.set_leverage.call_count == 1
# Leverage is rounded to 3.
assert api_mock.set_leverage.call_args_list[0][1]['leverage'] == 3
assert api_mock.set_leverage.call_args_list[0][1]['symbol'] == 'BTC/USDT:USDT'
ccxt_exceptionhandlers(
mocker,
@@ -557,7 +564,7 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog, c
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
pair = 'ETH/BTC'
respair, restf, restype, res = await exchange._async_get_historic_ohlcv(
respair, restf, restype, res, _ = await exchange._async_get_historic_ohlcv(
pair, "5m", 1500000000000, is_new_pair=False, candle_type=candle_type)
assert respair == pair
assert restf == '5m'
@@ -566,7 +573,7 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog, c
assert exchange._api_async.fetch_ohlcv.call_count > 400
# assert res == ohlcv
exchange._api_async.fetch_ohlcv.reset_mock()
_, _, _, res = await exchange._async_get_historic_ohlcv(
_, _, _, res, _ = await exchange._async_get_historic_ohlcv(
pair, "5m", 1500000000000, is_new_pair=True, candle_type=candle_type)
# Called twice - one "init" call - and one to get the actual data.
@@ -575,25 +582,13 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog, c
assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog)
@pytest.mark.parametrize("trading_mode,margin_mode,config", [
("spot", "", {}),
("margin", "cross", {"options": {"defaultType": "margin"}}),
("futures", "isolated", {"options": {"defaultType": "future"}}),
])
def test__ccxt_config(default_conf, mocker, trading_mode, margin_mode, config):
default_conf['trading_mode'] = trading_mode
default_conf['margin_mode'] = margin_mode
exchange = get_patched_exchange(mocker, default_conf, id="binance")
assert exchange._ccxt_config == config
@pytest.mark.parametrize('pair,nominal_value,mm_ratio,amt', [
("BNB/BUSD", 0.0, 0.025, 0),
("BNB/USDT", 100.0, 0.0065, 0),
("BTC/USDT", 170.30, 0.004, 0),
("BNB/BUSD", 999999.9, 0.1, 27500.0),
("BNB/USDT", 5000000.0, 0.15, 233035.0),
("BTC/USDT", 600000000, 0.5, 1.997038E8),
("BNB/BUSD:BUSD", 0.0, 0.025, 0),
("BNB/USDT:USDT", 100.0, 0.0065, 0),
("BTC/USDT:USDT", 170.30, 0.004, 0),
("BNB/BUSD:BUSD", 999999.9, 0.1, 27500.0),
("BNB/USDT:USDT", 5000000.0, 0.15, 233035.0),
("BTC/USDT:USDT", 600000000, 0.5, 1.997038E8),
])
def test_get_maintenance_ratio_and_amt_binance(
default_conf,

View File

@@ -0,0 +1,57 @@
from unittest.mock import MagicMock
from freqtrade.enums.marginmode import MarginMode
from freqtrade.enums.tradingmode import TradingMode
from freqtrade.exchange.exchange_utils import timeframe_to_msecs
from tests.conftest import get_mock_coro, get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers
def test_additional_exchange_init_bybit(default_conf, mocker):
default_conf['dry_run'] = False
default_conf['trading_mode'] = TradingMode.FUTURES
default_conf['margin_mode'] = MarginMode.ISOLATED
api_mock = MagicMock()
api_mock.set_position_mode = MagicMock(return_value={"dualSidePosition": False})
get_patched_exchange(mocker, default_conf, id="bybit", api_mock=api_mock)
assert api_mock.set_position_mode.call_count == 1
ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'bybit',
"additional_exchange_init", "set_position_mode")
async def test_bybit_fetch_funding_rate(default_conf, mocker):
default_conf['trading_mode'] = 'futures'
default_conf['margin_mode'] = 'isolated'
api_mock = MagicMock()
api_mock.fetch_funding_rate_history = get_mock_coro(return_value=[])
exchange = get_patched_exchange(mocker, default_conf, id='bybit', api_mock=api_mock)
limit = 200
# Test fetch_funding_rate_history (current data)
await exchange._fetch_funding_rate_history(
pair='BTC/USDT:USDT',
timeframe='4h',
limit=limit,
)
assert api_mock.fetch_funding_rate_history.call_count == 1
assert api_mock.fetch_funding_rate_history.call_args_list[0][0][0] == 'BTC/USDT:USDT'
kwargs = api_mock.fetch_funding_rate_history.call_args_list[0][1]
assert kwargs['params'] == {}
assert kwargs['since'] is None
api_mock.fetch_funding_rate_history.reset_mock()
since_ms = 1610000000000
since_ms_end = since_ms + (timeframe_to_msecs('4h') * limit)
# Test fetch_funding_rate_history (current data)
await exchange._fetch_funding_rate_history(
pair='BTC/USDT:USDT',
timeframe='4h',
limit=limit,
since_ms=since_ms,
)
assert api_mock.fetch_funding_rate_history.call_count == 1
assert api_mock.fetch_funding_rate_history.call_args_list[0][0][0] == 'BTC/USDT:USDT'
kwargs = api_mock.fetch_funding_rate_history.call_args_list[0][1]
assert kwargs['params'] == {'until': since_ms_end}
assert kwargs['since'] == since_ms

View File

@@ -8,16 +8,20 @@ suitable to run with freqtrade.
from copy import deepcopy
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Tuple
import pytest
from freqtrade.constants import Config
from freqtrade.enums import CandleType
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
from freqtrade.exchange.exchange import timeframe_to_msecs
from freqtrade.exchange.exchange import Exchange, timeframe_to_msecs
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from tests.conftest import get_default_conf_usdt
EXCHANGE_FIXTURE_TYPE = Tuple[Exchange, str]
# Exchanges that should be tested
EXCHANGES = {
'bittrex': {
@@ -28,15 +32,61 @@ EXCHANGES = {
'leverage_tiers_public': False,
'leverage_in_spot_market': False,
},
# 'binance': {
# 'pair': 'BTC/USDT',
# 'stake_currency': 'USDT',
# 'hasQuoteVolume': True,
# 'timeframe': '5m',
# 'futures': True,
# 'leverage_tiers_public': False,
# 'leverage_in_spot_market': False,
# },
'binance': {
'pair': 'BTC/USDT',
'stake_currency': 'USDT',
'use_ci_proxy': True,
'hasQuoteVolume': True,
'timeframe': '5m',
'futures': True,
'futures_pair': 'BTC/USDT:USDT',
'hasQuoteVolumeFutures': True,
'leverage_tiers_public': False,
'leverage_in_spot_market': False,
'sample_order': [{
"symbol": "SOLUSDT",
"orderId": 3551312894,
"orderListId": -1,
"clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba",
"transactTime": 1674493798550,
"price": "15.50000000",
"origQty": "1.10000000",
"executedQty": "0.00000000",
"cummulativeQuoteQty": "0.00000000",
"status": "NEW",
"timeInForce": "GTC",
"type": "LIMIT",
"side": "BUY",
"workingTime": 1674493798550,
"fills": [],
"selfTradePreventionMode": "NONE",
}]
},
'binanceus': {
'pair': 'BTC/USDT',
'stake_currency': 'USDT',
'hasQuoteVolume': True,
'timeframe': '5m',
'futures': False,
'sample_order': [{
"symbol": "SOLUSDT",
"orderId": 3551312894,
"orderListId": -1,
"clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba",
"transactTime": 1674493798550,
"price": "15.50000000",
"origQty": "1.10000000",
"executedQty": "0.00000000",
"cummulativeQuoteQty": "0.00000000",
"status": "NEW",
"timeInForce": "GTC",
"type": "LIMIT",
"side": "BUY",
"workingTime": 1674493798550,
"fills": [],
"selfTradePreventionMode": "NONE",
}]
},
'kraken': {
'pair': 'BTC/USDT',
'stake_currency': 'USDT',
@@ -52,18 +102,127 @@ EXCHANGES = {
'timeframe': '5m',
'leverage_tiers_public': False,
'leverage_in_spot_market': True,
'sample_order': [
{'id': '63d6742d0adc5570001d2bbf7'}, # create order
{
'id': '63d6742d0adc5570001d2bbf7',
'symbol': 'SOL-USDT',
'opType': 'DEAL',
'type': 'limit',
'side': 'buy',
'price': '15.5',
'size': '1.1',
'funds': '0',
'dealFunds': '17.05',
'dealSize': '1.1',
'fee': '0.000065252',
'feeCurrency': 'USDT',
'stp': '',
'stop': '',
'stopTriggered': False,
'stopPrice': '0',
'timeInForce': 'GTC',
'postOnly': False,
'hidden': False,
'iceberg': False,
'visibleSize': '0',
'cancelAfter': 0,
'channel': 'API',
'clientOid': '0a053870-11bf-41e5-be61-b272a4cb62e1',
'remark': None,
'tags': 'partner:ccxt',
'isActive': False,
'cancelExist': False,
'createdAt': 1674493798550,
'tradeType': 'TRADE'
}],
},
'gateio': {
'gate': {
'pair': 'BTC/USDT',
'stake_currency': 'USDT',
'hasQuoteVolume': True,
'timeframe': '5m',
'futures': True,
'futures_pair': 'BTC/USDT:USDT',
'hasQuoteVolumeFutures': True,
'leverage_tiers_public': True,
'leverage_in_spot_market': True,
'sample_order': [
{
"id": "276266139423",
"text": "apiv4",
"create_time": "1674493798",
"update_time": "1674493798",
"create_time_ms": "1674493798550",
"update_time_ms": "1674493798550",
"status": "closed",
"currency_pair": "SOL_USDT",
"type": "limit",
"account": "spot",
"side": "buy",
"amount": "1.1",
"price": "15.5",
"time_in_force": "gtc",
"iceberg": "0",
"left": "0",
"fill_price": "17.05",
"filled_total": "17.05",
"avg_deal_price": "15.5",
"fee": "0.0000018",
"fee_currency": "SOL",
"point_fee": "0",
"gt_fee": "0",
"gt_maker_fee": "0",
"gt_taker_fee": "0.0015",
"gt_discount": True,
"rebated_fee": "0",
"rebated_fee_currency": "USDT"
},
{
# market order
'id': '276401180529',
'text': 'apiv4',
'create_time': '1674493798',
'update_time': '1674493798',
'create_time_ms': '1674493798550',
'update_time_ms': '1674493798550',
'status': 'cancelled',
'currency_pair': 'SOL_USDT',
'type': 'market',
'account': 'spot',
'side': 'buy',
'amount': '17.05',
'price': '0',
'time_in_force': 'ioc',
'iceberg': '0',
'left': '0.0000000016228',
'fill_price': '17.05',
'filled_total': '17.05',
'avg_deal_price': '15.5',
'fee': '0',
'fee_currency': 'SOL',
'point_fee': '0.0199999999967544',
'gt_fee': '0',
'gt_maker_fee': '0',
'gt_taker_fee': '0',
'gt_discount': False,
'rebated_fee': '0',
'rebated_fee_currency': 'USDT'
}
],
},
'okx': {
'pair': 'BTC/USDT',
'stake_currency': 'USDT',
'hasQuoteVolume': True,
'timeframe': '5m',
'futures': True,
'futures_pair': 'BTC/USDT:USDT',
'hasQuoteVolumeFutures': False,
'leverage_tiers_public': True,
'leverage_in_spot_market': True,
},
'bybit': {
'pair': 'BTC/USDT',
'stake_currency': 'USDT',
'hasQuoteVolume': True,
@@ -72,10 +231,27 @@ EXCHANGES = {
'futures': True,
'leverage_tiers_public': True,
'leverage_in_spot_market': True,
'sample_order': [
{
"orderId": "1274754916287346280",
"orderLinkId": "1666798627015730",
"symbol": "SOLUSDT",
"createTime": "1674493798550",
"orderPrice": "15.5",
"orderQty": "1.1",
"orderType": "LIMIT",
"side": "BUY",
"status": "NEW",
"timeInForce": "GTC",
"accountId": "5555555",
"execQty": "0",
"orderCategory": "0"
}
]
},
'huobi': {
'pair': 'BTC/USDT',
'stake_currency': 'USDT',
'pair': 'ETH/BTC',
'stake_currency': 'BTC',
'hasQuoteVolume': True,
'timeframe': '5m',
'futures': False,
@@ -103,8 +279,27 @@ def exchange_conf():
return config
def set_test_proxy(config: Config, use_proxy: bool) -> Config:
# Set proxy to test in CI.
import os
if use_proxy and (proxy := os.environ.get('CI_WEB_PROXY')):
config1 = deepcopy(config)
config1['exchange']['ccxt_config'] = {
"aiohttp_proxy": proxy,
'proxies': {
'https': proxy,
'http': proxy,
}
}
return config1
return config
@pytest.fixture(params=EXCHANGES, scope="class")
def exchange(request, exchange_conf):
exchange_conf = set_test_proxy(
exchange_conf, EXCHANGES[request.param].get('use_ci_proxy', False))
exchange_conf['exchange']['name'] = request.param
exchange_conf['stake_currency'] = EXCHANGES[request.param]['stake_currency']
exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=True)
@@ -117,6 +312,8 @@ def exchange_futures(request, exchange_conf, class_mocker):
if not EXCHANGES[request.param].get('futures') is True:
yield None, request.param
else:
exchange_conf = set_test_proxy(
exchange_conf, EXCHANGES[request.param].get('use_ci_proxy', False))
exchange_conf = deepcopy(exchange_conf)
exchange_conf['exchange']['name'] = request.param
exchange_conf['trading_mode'] = 'futures'
@@ -128,6 +325,7 @@ def exchange_futures(request, exchange_conf, class_mocker):
class_mocker.patch('freqtrade.exchange.exchange.Exchange.fetch_trading_fees')
class_mocker.patch('freqtrade.exchange.okx.Okx.additional_exchange_init')
class_mocker.patch('freqtrade.exchange.binance.Binance.additional_exchange_init')
class_mocker.patch('freqtrade.exchange.bybit.Bybit.additional_exchange_init')
class_mocker.patch('freqtrade.exchange.exchange.Exchange.load_cached_leverage_tiers',
return_value=None)
class_mocker.patch('freqtrade.exchange.exchange.Exchange.cache_leverage_tiers')
@@ -141,34 +339,34 @@ def exchange_futures(request, exchange_conf, class_mocker):
@pytest.mark.longrun
class TestCCXTExchange():
def test_load_markets(self, exchange):
exchange, exchangename = exchange
def test_load_markets(self, exchange: EXCHANGE_FIXTURE_TYPE):
exch, exchangename = exchange
pair = EXCHANGES[exchangename]['pair']
markets = exchange.markets
markets = exch.markets
assert pair in markets
assert isinstance(markets[pair], dict)
assert exchange.market_is_spot(markets[pair])
assert exch.market_is_spot(markets[pair])
def test_has_validations(self, exchange):
def test_has_validations(self, exchange: EXCHANGE_FIXTURE_TYPE):
exchange, exchangename = exchange
exch, exchangename = exchange
exchange.validate_ordertypes({
exch.validate_ordertypes({
'entry': 'limit',
'exit': 'limit',
'stoploss': 'limit',
})
if exchangename == 'gateio':
# gateio doesn't have market orders on spot
if exchangename == 'gate':
# gate doesn't have market orders on spot
return
exchange.validate_ordertypes({
exch.validate_ordertypes({
'entry': 'market',
'exit': 'market',
'stoploss': 'market',
})
def test_load_markets_futures(self, exchange_futures):
def test_load_markets_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
exchange, exchangename = exchange_futures
if not exchange:
# exchange_futures only returns values for supported exchanges
@@ -181,11 +379,37 @@ class TestCCXTExchange():
assert exchange.market_is_future(markets[pair])
def test_ccxt_fetch_tickers(self, exchange):
exchange, exchangename = exchange
def test_ccxt_order_parse(self, exchange: EXCHANGE_FIXTURE_TYPE):
exch, exchange_name = exchange
if orders := EXCHANGES[exchange_name].get('sample_order'):
for order in orders:
po = exch._api.parse_order(order)
assert isinstance(po['id'], str)
assert po['id'] is not None
if len(order.keys()) < 5:
# Kucoin case
assert po['status'] == 'closed'
continue
assert po['timestamp'] == 1674493798550
assert isinstance(po['datetime'], str)
assert isinstance(po['timestamp'], int)
assert isinstance(po['price'], float)
assert po['price'] == 15.5
if po['average'] is not None:
assert isinstance(po['average'], float)
assert po['average'] == 15.5
assert po['symbol'] == 'SOL/USDT'
assert isinstance(po['amount'], float)
assert po['amount'] == 1.1
assert isinstance(po['status'], str)
else:
pytest.skip(f"No sample order available for exchange {exchange_name}")
def test_ccxt_fetch_tickers(self, exchange: EXCHANGE_FIXTURE_TYPE):
exch, exchangename = exchange
pair = EXCHANGES[exchangename]['pair']
tickers = exchange.get_tickers()
tickers = exch.get_tickers()
assert pair in tickers
assert 'ask' in tickers[pair]
assert tickers[pair]['ask'] is not None
@@ -195,11 +419,30 @@ class TestCCXTExchange():
if EXCHANGES[exchangename].get('hasQuoteVolume'):
assert tickers[pair]['quoteVolume'] is not None
def test_ccxt_fetch_ticker(self, exchange):
exchange, exchangename = exchange
def test_ccxt_fetch_tickers_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
exch, exchangename = exchange_futures
if not exch or exchangename in ('gate'):
# exchange_futures only returns values for supported exchanges
return
pair = EXCHANGES[exchangename]['pair']
pair = EXCHANGES[exchangename].get('futures_pair', pair)
tickers = exch.get_tickers()
assert pair in tickers
assert 'ask' in tickers[pair]
assert tickers[pair]['ask'] is not None
assert 'bid' in tickers[pair]
assert tickers[pair]['bid'] is not None
assert 'quoteVolume' in tickers[pair]
if EXCHANGES[exchangename].get('hasQuoteVolumeFutures'):
assert tickers[pair]['quoteVolume'] is not None
def test_ccxt_fetch_ticker(self, exchange: EXCHANGE_FIXTURE_TYPE):
exch, exchangename = exchange
pair = EXCHANGES[exchangename]['pair']
ticker = exchange.fetch_ticker(pair)
ticker = exch.fetch_ticker(pair)
assert 'ask' in ticker
assert ticker['ask'] is not None
assert 'bid' in ticker
@@ -208,21 +451,21 @@ class TestCCXTExchange():
if EXCHANGES[exchangename].get('hasQuoteVolume'):
assert ticker['quoteVolume'] is not None
def test_ccxt_fetch_l2_orderbook(self, exchange):
exchange, exchangename = exchange
def test_ccxt_fetch_l2_orderbook(self, exchange: EXCHANGE_FIXTURE_TYPE):
exch, exchangename = exchange
pair = EXCHANGES[exchangename]['pair']
l2 = exchange.fetch_l2_order_book(pair)
l2 = exch.fetch_l2_order_book(pair)
assert 'asks' in l2
assert 'bids' in l2
assert len(l2['asks']) >= 1
assert len(l2['bids']) >= 1
l2_limit_range = exchange._ft_has['l2_limit_range']
l2_limit_range_required = exchange._ft_has['l2_limit_range_required']
if exchangename == 'gateio':
# TODO: Gateio is unstable here at the moment, ignoring the limit partially.
l2_limit_range = exch._ft_has['l2_limit_range']
l2_limit_range_required = exch._ft_has['l2_limit_range_required']
if exchangename == 'gate':
# TODO: Gate is unstable here at the moment, ignoring the limit partially.
return
for val in [1, 2, 5, 25, 100]:
l2 = exchange.fetch_l2_order_book(pair, val)
l2 = exch.fetch_l2_order_book(pair, val)
if not l2_limit_range or val in l2_limit_range:
if val > 50:
# Orderbooks are not always this deep.
@@ -232,7 +475,7 @@ class TestCCXTExchange():
assert len(l2['asks']) == val
assert len(l2['bids']) == val
else:
next_limit = exchange.get_next_limit_in_list(
next_limit = exch.get_next_limit_in_list(
val, l2_limit_range, l2_limit_range_required)
if next_limit is None:
assert len(l2['asks']) > 100
@@ -245,23 +488,23 @@ class TestCCXTExchange():
assert len(l2['asks']) == next_limit
assert len(l2['asks']) == next_limit
def test_ccxt_fetch_ohlcv(self, exchange):
exchange, exchangename = exchange
def test_ccxt_fetch_ohlcv(self, exchange: EXCHANGE_FIXTURE_TYPE):
exch, exchangename = exchange
pair = EXCHANGES[exchangename]['pair']
timeframe = EXCHANGES[exchangename]['timeframe']
pair_tf = (pair, timeframe, CandleType.SPOT)
ohlcv = exchange.refresh_latest_ohlcv([pair_tf])
ohlcv = exch.refresh_latest_ohlcv([pair_tf])
assert isinstance(ohlcv, dict)
assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf))
# assert len(exchange.klines(pair_tf)) > 200
assert len(ohlcv[pair_tf]) == len(exch.klines(pair_tf))
# assert len(exch.klines(pair_tf)) > 200
# Assume 90% uptime ...
assert len(exchange.klines(pair_tf)) > exchange.ohlcv_candle_limit(
assert len(exch.klines(pair_tf)) > exch.ohlcv_candle_limit(
timeframe, CandleType.SPOT) * 0.90
# Check if last-timeframe is within the last 2 intervals
now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2))
assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now)
assert exch.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now)
def ccxt__async_get_candle_history(self, exchange, exchangename, pair, timeframe, candle_type):
@@ -289,17 +532,20 @@ class TestCCXTExchange():
assert len(candles) >= min(candle_count, candle_count1)
assert candles[0][0] == since_ms or (since_ms + timeframe_ms)
def test_ccxt__async_get_candle_history(self, exchange):
exchange, exchangename = exchange
# For some weired reason, this test returns random lengths for bittrex.
if not exchange._ft_has['ohlcv_has_history'] or exchangename in ('bittrex'):
return
def test_ccxt__async_get_candle_history(self, exchange: EXCHANGE_FIXTURE_TYPE):
exc, exchangename = exchange
if exchangename in ('bittrex'):
# For some weired reason, this test returns random lengths for bittrex.
pytest.skip("Exchange doesn't provide stable ohlcv history")
if not exc._ft_has['ohlcv_has_history']:
pytest.skip("Exchange does not support candle history")
pair = EXCHANGES[exchangename]['pair']
timeframe = EXCHANGES[exchangename]['timeframe']
self.ccxt__async_get_candle_history(
exchange, exchangename, pair, timeframe, CandleType.SPOT)
exc, exchangename, pair, timeframe, CandleType.SPOT)
def test_ccxt__async_get_candle_history_futures(self, exchange_futures):
def test_ccxt__async_get_candle_history_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
exchange, exchangename = exchange_futures
if not exchange:
# exchange_futures only returns values for supported exchanges
@@ -309,7 +555,7 @@ class TestCCXTExchange():
self.ccxt__async_get_candle_history(
exchange, exchangename, pair, timeframe, CandleType.FUTURES)
def test_ccxt_fetch_funding_rate_history(self, exchange_futures):
def test_ccxt_fetch_funding_rate_history(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
exchange, exchangename = exchange_futures
if not exchange:
# exchange_futures only returns values for supported exchanges
@@ -347,7 +593,7 @@ class TestCCXTExchange():
(rate['open'].min() != rate['open'].max())
)
def test_ccxt_fetch_mark_price_history(self, exchange_futures):
def test_ccxt_fetch_mark_price_history(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
exchange, exchangename = exchange_futures
if not exchange:
# exchange_futures only returns values for supported exchanges
@@ -371,7 +617,7 @@ class TestCCXTExchange():
assert mark_candles[mark_candles['date'] == prev_hour].iloc[0]['open'] != 0.0
assert mark_candles[mark_candles['date'] == this_hour].iloc[0]['open'] != 0.0
def test_ccxt__calculate_funding_fees(self, exchange_futures):
def test_ccxt__calculate_funding_fees(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
exchange, exchangename = exchange_futures
if not exchange:
# exchange_futures only returns values for supported exchanges
@@ -387,16 +633,16 @@ class TestCCXTExchange():
# TODO: tests fetch_trades (?)
def test_ccxt_get_fee(self, exchange):
exchange, exchangename = exchange
def test_ccxt_get_fee(self, exchange: EXCHANGE_FIXTURE_TYPE):
exch, exchangename = exchange
pair = EXCHANGES[exchangename]['pair']
threshold = 0.01
assert 0 < exchange.get_fee(pair, 'limit', 'buy') < threshold
assert 0 < exchange.get_fee(pair, 'limit', 'sell') < threshold
assert 0 < exchange.get_fee(pair, 'market', 'buy') < threshold
assert 0 < exchange.get_fee(pair, 'market', 'sell') < threshold
assert 0 < exch.get_fee(pair, 'limit', 'buy') < threshold
assert 0 < exch.get_fee(pair, 'limit', 'sell') < threshold
assert 0 < exch.get_fee(pair, 'market', 'buy') < threshold
assert 0 < exch.get_fee(pair, 'market', 'sell') < threshold
def test_ccxt_get_max_leverage_spot(self, exchange):
def test_ccxt_get_max_leverage_spot(self, exchange: EXCHANGE_FIXTURE_TYPE):
spot, spot_name = exchange
if spot:
leverage_in_market_spot = EXCHANGES[spot_name].get('leverage_in_spot_market')
@@ -406,7 +652,7 @@ class TestCCXTExchange():
assert (isinstance(spot_leverage, float) or isinstance(spot_leverage, int))
assert spot_leverage >= 1.0
def test_ccxt_get_max_leverage_futures(self, exchange_futures):
def test_ccxt_get_max_leverage_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
futures, futures_name = exchange_futures
if futures:
leverage_tiers_public = EXCHANGES[futures_name].get('leverage_tiers_public')
@@ -419,7 +665,7 @@ class TestCCXTExchange():
assert (isinstance(futures_leverage, float) or isinstance(futures_leverage, int))
assert futures_leverage >= 1.0
def test_ccxt_get_contract_size(self, exchange_futures):
def test_ccxt_get_contract_size(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
futures, futures_name = exchange_futures
if futures:
futures_pair = EXCHANGES[futures_name].get(
@@ -430,7 +676,7 @@ class TestCCXTExchange():
assert (isinstance(contract_size, float) or isinstance(contract_size, int))
assert contract_size >= 0.0
def test_ccxt_load_leverage_tiers(self, exchange_futures):
def test_ccxt_load_leverage_tiers(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
futures, futures_name = exchange_futures
if futures and EXCHANGES[futures_name].get('leverage_tiers_public'):
leverage_tiers = futures.load_leverage_tiers()
@@ -463,7 +709,7 @@ class TestCCXTExchange():
oldminNotional = tier['minNotional']
oldmaxNotional = tier['maxNotional']
def test_ccxt_dry_run_liquidation_price(self, exchange_futures):
def test_ccxt_dry_run_liquidation_price(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
futures, futures_name = exchange_futures
if futures and EXCHANGES[futures_name].get('leverage_tiers_public'):
@@ -473,28 +719,30 @@ class TestCCXTExchange():
)
liquidation_price = futures.dry_run_liquidation_price(
futures_pair,
40000,
False,
100,
100,
100,
pair=futures_pair,
open_rate=40000,
is_short=False,
amount=100,
stake_amount=100,
leverage=5,
wallet_balance=100,
)
assert (isinstance(liquidation_price, float))
assert liquidation_price >= 0.0
liquidation_price = futures.dry_run_liquidation_price(
futures_pair,
40000,
False,
100,
100,
100,
pair=futures_pair,
open_rate=40000,
is_short=False,
amount=100,
stake_amount=100,
leverage=5,
wallet_balance=100,
)
assert (isinstance(liquidation_price, float))
assert liquidation_price >= 0.0
def test_ccxt_get_max_pair_stake_amount(self, exchange_futures):
def test_ccxt_get_max_pair_stake_amount(self, exchange_futures: EXCHANGE_FIXTURE_TYPE):
futures, futures_name = exchange_futures
if futures:
futures_pair = EXCHANGES[futures_name].get(

View File

@@ -27,7 +27,7 @@ from tests.conftest import (generate_test_data_raw, get_mock_coro, get_patched_e
# Make sure to always keep one exchange here which is NOT subclassed!!
EXCHANGES = ['bittrex', 'binance', 'kraken', 'gateio']
EXCHANGES = ['bittrex', 'binance', 'kraken', 'gate']
get_entry_rate_data = [
('other', 20, 19, 10, 0.0, 20), # Full ask side
@@ -1060,6 +1060,47 @@ def test_validate_ordertypes(default_conf, mocker):
Exchange(default_conf)
@pytest.mark.parametrize('exchange_name,stopadv, expected', [
('binance', 'last', True),
('binance', 'mark', True),
('binance', 'index', False),
('bybit', 'last', True),
('bybit', 'mark', True),
('bybit', 'index', True),
# ('okx', 'last', True),
# ('okx', 'mark', True),
# ('okx', 'index', True),
('gate', 'last', True),
('gate', 'mark', True),
('gate', 'index', True),
])
def test_validate_ordertypes_stop_advanced(default_conf, mocker, exchange_name, stopadv, expected):
api_mock = MagicMock()
default_conf['trading_mode'] = TradingMode.FUTURES
default_conf['margin_mode'] = MarginMode.ISOLATED
type(api_mock).has = PropertyMock(return_value={'createMarketOrder': True})
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
default_conf['order_types'] = {
'entry': 'limit',
'exit': 'limit',
'stoploss': 'limit',
'stoploss_on_exchange': True,
'stoploss_price_type': stopadv,
}
if expected:
ExchangeResolver.load_exchange(exchange_name, default_conf)
else:
with pytest.raises(OperationalException,
match=r'On exchange stoploss price type is not supported for .*'):
ExchangeResolver.load_exchange(exchange_name, default_conf)
def test_validate_order_types_not_in_config(default_conf, mocker):
api_mock = MagicMock()
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
@@ -1742,7 +1783,7 @@ def test_fetch_trading_fees(default_conf, mocker):
'maker': 0.0,
'taker': 0.0005}
}
exchange_name = 'gateio'
exchange_name = 'gate'
default_conf['dry_run'] = False
default_conf['trading_mode'] = TradingMode.FUTURES
default_conf['margin_mode'] = MarginMode.ISOLATED
@@ -1955,7 +1996,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_
pair = 'ETH/BTC'
async def mock_candle_hist(pair, timeframe, candle_type, since_ms):
return pair, timeframe, candle_type, ohlcv
return pair, timeframe, candle_type, ohlcv, True
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
# one_call calculation * 1.8 should do 2 calls
@@ -1988,62 +2029,6 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_
assert log_has_re(r"Async code raised an exception: .*", caplog)
@pytest.mark.parametrize("exchange_name", EXCHANGES)
@pytest.mark.parametrize('candle_type', ['mark', ''])
def test_get_historic_ohlcv_as_df(default_conf, mocker, exchange_name, candle_type):
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
ohlcv = [
[
arrow.utcnow().int_timestamp * 1000, # unix timestamp ms
1, # open
2, # high
3, # low
4, # close
5, # volume (in quote currency)
],
[
arrow.utcnow().shift(minutes=5).int_timestamp * 1000, # unix timestamp ms
1, # open
2, # high
3, # low
4, # close
5, # volume (in quote currency)
],
[
arrow.utcnow().shift(minutes=10).int_timestamp * 1000, # unix timestamp ms
1, # open
2, # high
3, # low
4, # close
5, # volume (in quote currency)
]
]
pair = 'ETH/BTC'
async def mock_candle_hist(pair, timeframe, candle_type, since_ms):
return pair, timeframe, candle_type, ohlcv
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
# one_call calculation * 1.8 should do 2 calls
since = 5 * 60 * exchange.ohlcv_candle_limit('5m', CandleType.SPOT) * 1.8
ret = exchange.get_historic_ohlcv_as_df(
pair,
"5m",
int((arrow.utcnow().int_timestamp - since) * 1000),
candle_type=candle_type
)
assert exchange._async_get_candle_history.call_count == 2
# Returns twice the above OHLCV data
assert len(ret) == 2
assert isinstance(ret, DataFrame)
assert 'date' in ret.columns
assert 'open' in ret.columns
assert 'close' in ret.columns
assert 'high' in ret.columns
@pytest.mark.asyncio
@pytest.mark.parametrize("exchange_name", EXCHANGES)
@pytest.mark.parametrize('candle_type', [CandleType.MARK, CandleType.SPOT])
@@ -2063,7 +2048,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
pair = 'ETH/USDT'
respair, restf, _, res = await exchange._async_get_historic_ohlcv(
respair, restf, _, res, _ = await exchange._async_get_historic_ohlcv(
pair, "5m", 1500000000000, candle_type=candle_type, is_new_pair=False)
assert respair == pair
assert restf == '5m'
@@ -2074,7 +2059,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
exchange._api_async.fetch_ohlcv.reset_mock()
end_ts = 1_500_500_000_000
start_ts = 1_500_000_000_000
respair, restf, _, res = await exchange._async_get_historic_ohlcv(
respair, restf, _, res, _ = await exchange._async_get_historic_ohlcv(
pair, "5m", since_ms=start_ts, candle_type=candle_type, is_new_pair=False,
until_ms=end_ts
)
@@ -2306,7 +2291,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
pair = 'ETH/BTC'
res = await exchange._async_get_candle_history(pair, "5m", CandleType.SPOT)
assert type(res) is tuple
assert len(res) == 4
assert len(res) == 5
assert res[0] == pair
assert res[1] == "5m"
assert res[2] == CandleType.SPOT
@@ -2393,7 +2378,7 @@ async def test__async_get_candle_history_empty(default_conf, mocker, caplog):
pair = 'ETH/BTC'
res = await exchange._async_get_candle_history(pair, "5m", CandleType.SPOT)
assert type(res) is tuple
assert len(res) == 4
assert len(res) == 5
assert res[0] == pair
assert res[1] == "5m"
assert res[2] == CandleType.SPOT
@@ -3162,24 +3147,24 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name):
def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name):
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', return_value={'for': 123})
mocker.patch('freqtrade.exchange.Gateio.fetch_stoploss_order', return_value={'for': 123})
mocker.patch('freqtrade.exchange.Gate.fetch_stoploss_order', return_value={'for': 123})
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
res = {'fee': {}, 'status': 'canceled', 'amount': 1234}
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', return_value=res)
mocker.patch('freqtrade.exchange.Gateio.cancel_stoploss_order', return_value=res)
mocker.patch('freqtrade.exchange.Gate.cancel_stoploss_order', return_value=res)
co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555)
assert co == res
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', return_value='canceled')
mocker.patch('freqtrade.exchange.Gateio.cancel_stoploss_order', return_value='canceled')
mocker.patch('freqtrade.exchange.Gate.cancel_stoploss_order', return_value='canceled')
# Fall back to fetch_stoploss_order
co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555)
assert co == {'for': 123}
exc = InvalidOrderException("")
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', side_effect=exc)
mocker.patch('freqtrade.exchange.Gateio.fetch_stoploss_order', side_effect=exc)
mocker.patch('freqtrade.exchange.Gate.fetch_stoploss_order', side_effect=exc)
co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555)
assert co['amount'] == 555
assert co == {'fee': {}, 'status': 'canceled', 'amount': 555, 'info': {}}
@@ -3187,7 +3172,7 @@ def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name):
with pytest.raises(InvalidOrderException):
exc = InvalidOrderException("Did not find order")
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', side_effect=exc)
mocker.patch('freqtrade.exchange.Gateio.cancel_stoploss_order', side_effect=exc)
mocker.patch('freqtrade.exchange.Gate.cancel_stoploss_order', side_effect=exc)
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=123)
@@ -3395,7 +3380,7 @@ 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(
exchange.create_stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
@@ -3967,14 +3952,14 @@ def test_set_margin_mode(mocker, default_conf, margin_mode):
("bittrex", TradingMode.MARGIN, MarginMode.ISOLATED, True),
("bittrex", TradingMode.FUTURES, MarginMode.CROSS, True),
("bittrex", TradingMode.FUTURES, MarginMode.ISOLATED, True),
("gateio", TradingMode.MARGIN, MarginMode.ISOLATED, True),
("gate", TradingMode.MARGIN, MarginMode.ISOLATED, True),
("okx", TradingMode.SPOT, None, False),
("okx", TradingMode.MARGIN, MarginMode.CROSS, True),
("okx", TradingMode.MARGIN, MarginMode.ISOLATED, True),
("okx", TradingMode.FUTURES, MarginMode.CROSS, True),
("binance", TradingMode.FUTURES, MarginMode.ISOLATED, False),
("gateio", TradingMode.FUTURES, MarginMode.ISOLATED, False),
("gate", TradingMode.FUTURES, MarginMode.ISOLATED, False),
("okx", TradingMode.FUTURES, MarginMode.ISOLATED, False),
# * Remove once implemented
@@ -3982,16 +3967,16 @@ def test_set_margin_mode(mocker, default_conf, margin_mode):
("binance", TradingMode.FUTURES, MarginMode.CROSS, True),
("kraken", TradingMode.MARGIN, MarginMode.CROSS, True),
("kraken", TradingMode.FUTURES, MarginMode.CROSS, True),
("gateio", TradingMode.MARGIN, MarginMode.CROSS, True),
("gateio", TradingMode.FUTURES, MarginMode.CROSS, True),
("gate", TradingMode.MARGIN, MarginMode.CROSS, True),
("gate", TradingMode.FUTURES, MarginMode.CROSS, True),
# * Uncomment once implemented
# ("binance", TradingMode.MARGIN, MarginMode.CROSS, False),
# ("binance", TradingMode.FUTURES, MarginMode.CROSS, False),
# ("kraken", TradingMode.MARGIN, MarginMode.CROSS, False),
# ("kraken", TradingMode.FUTURES, MarginMode.CROSS, False),
# ("gateio", TradingMode.MARGIN, MarginMode.CROSS, False),
# ("gateio", TradingMode.FUTURES, MarginMode.CROSS, False),
# ("gate", TradingMode.MARGIN, MarginMode.CROSS, False),
# ("gate", TradingMode.FUTURES, MarginMode.CROSS, False),
])
def test_validate_trading_mode_and_margin_mode(
default_conf,
@@ -4013,13 +3998,10 @@ def test_validate_trading_mode_and_margin_mode(
@pytest.mark.parametrize("exchange_name,trading_mode,ccxt_config", [
("binance", "spot", {}),
("binance", "margin", {"options": {"defaultType": "margin"}}),
("binance", "futures", {"options": {"defaultType": "future"}}),
("bibox", "spot", {"has": {"fetchCurrencies": False}}),
("bibox", "margin", {"has": {"fetchCurrencies": False}, "options": {"defaultType": "margin"}}),
("bibox", "futures", {"has": {"fetchCurrencies": False}, "options": {"defaultType": "swap"}}),
("binance", "futures", {"options": {"defaultType": "swap"}}),
("bybit", "spot", {"options": {"defaultType": "spot"}}),
("bybit", "futures", {"options": {"defaultType": "linear"}}),
("gateio", "futures", {"options": {"defaultType": "swap"}}),
("bybit", "futures", {"options": {"defaultType": "swap"}}),
("gate", "futures", {"options": {"defaultType": "swap"}}),
("hitbtc", "futures", {"options": {"defaultType": "swap"}}),
("kraken", "futures", {"options": {"defaultType": "swap"}}),
("kucoin", "futures", {"options": {"defaultType": "swap"}}),
@@ -4050,7 +4032,7 @@ def test_get_max_leverage_from_margin(default_conf, mocker, pair, nominal_value,
default_conf['margin_mode'] = 'isolated'
api_mock = MagicMock()
type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': False})
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="gateio")
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="gate")
assert exchange.get_max_leverage(pair, nominal_value) == max_lev
@@ -4195,10 +4177,10 @@ def test_combine_funding_and_mark(
# ('kraken', "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.0012443999999999999),
# ('kraken', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, 0.0045759),
# ('kraken', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0008289),
('gateio', 0, 2, "2021-09-01 00:10:00", "2021-09-01 04:00:00", 30.0, 0.0),
('gateio', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999),
('gateio', 0, 2, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999),
('gateio', 1, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0002493),
('gate', 0, 2, "2021-09-01 00:10:00", "2021-09-01 04:00:00", 30.0, 0.0),
('gate', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999),
('gate', 0, 2, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999),
('gate', 1, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0002493),
('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0015235),
# TODO: Uncoment once _calculate_funding_fees can pas time_in_ratio to exchange._get_funding_fee
# ('kraken', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0024895),
@@ -4256,7 +4238,7 @@ def test__fetch_and_calculate_funding_fees(
d2 = datetime.strptime(f"{d2} +0000", '%Y-%m-%d %H:%M:%S %z')
funding_rate_history = {
'binance': funding_rate_history_octohourly,
'gateio': funding_rate_history_octohourly,
'gate': funding_rate_history_octohourly,
}[exchange][rate_start:rate_end]
api_mock = MagicMock()
api_mock.fetch_funding_rate_history = get_mock_coro(return_value=funding_rate_history)
@@ -4285,7 +4267,7 @@ def test__fetch_and_calculate_funding_fees(
@pytest.mark.parametrize('exchange,expected_fees', [
('binance', -0.0009140999999999999),
('gateio', -0.0009140999999999999),
('gate', -0.0009140999999999999),
])
def test__fetch_and_calculate_funding_fees_datetime_called(
mocker,
@@ -4426,7 +4408,7 @@ def test__order_contracts_to_amount(
'info': {},
},
{
# Realistic stoploss order on gateio.
# Realistic stoploss order on gate.
'id': '123456380',
'clientOrderId': '12345638203',
'timestamp': None,
@@ -4625,6 +4607,7 @@ def test_liquidation_price_is_none(
is_short=is_short,
amount=71200.81144,
stake_amount=open_rate * 71200.81144,
leverage=5,
wallet_balance=-56354.57,
mm_ex_1=0.10,
upnl_ex_1=0.0
@@ -4645,7 +4628,7 @@ def test_liquidation_price_is_none(
("binance", False, 'futures', 'cross', 1535443.01, 356512.508,
-448192.89, 16300.000, 109.488, 32481.980, 0.025, 26316.89)
])
def test_liquidation_price(
def test_liquidation_price_binance(
mocker, default_conf, exchange_name, open_rate, is_short, trading_mode,
margin_mode, wallet_balance, mm_ex_1, upnl_ex_1, maintenance_amt, amount, mm_ratio, expected
):
@@ -4663,6 +4646,7 @@ def test_liquidation_price(
upnl_ex_1=upnl_ex_1,
amount=amount,
stake_amount=open_rate * amount,
leverage=5,
), 2)) == expected
@@ -4957,22 +4941,22 @@ def test_get_maintenance_ratio_and_amt_exceptions(mocker, default_conf, leverage
OperationalException,
match='nominal value can not be lower than 0',
):
exchange.get_maintenance_ratio_and_amt('1000SHIB/USDT', -1)
exchange.get_maintenance_ratio_and_amt('1000SHIB/USDT:USDT', -1)
exchange._leverage_tiers = {}
with pytest.raises(
InvalidOrderException,
match="Maintenance margin rate for 1000SHIB/USDT is unavailable for",
match="Maintenance margin rate for 1000SHIB/USDT:USDT is unavailable for",
):
exchange.get_maintenance_ratio_and_amt('1000SHIB/USDT', 10000)
exchange.get_maintenance_ratio_and_amt('1000SHIB/USDT:USDT', 10000)
@pytest.mark.parametrize('pair,value,mmr,maintAmt', [
('ADA/BUSD', 500, 0.025, 0.0),
('ADA/BUSD', 20000000, 0.5, 1527500.0),
('ZEC/USDT', 500, 0.01, 0.0),
('ZEC/USDT', 20000000, 0.5, 654500.0),
('ADA/BUSD:BUSD', 500, 0.025, 0.0),
('ADA/BUSD:BUSD', 20000000, 0.5, 1527500.0),
('ZEC/USDT:USDT', 500, 0.01, 0.0),
('ZEC/USDT:USDT', 20000000, 0.5, 654500.0),
])
def test_get_maintenance_ratio_and_amt(
mocker,
@@ -5005,24 +4989,24 @@ def test_get_max_leverage_futures(default_conf, mocker, leverage_tiers):
exchange._leverage_tiers = leverage_tiers
assert exchange.get_max_leverage("BNB/BUSD", 1.0) == 20.0
assert exchange.get_max_leverage("BNB/USDT", 100.0) == 75.0
assert exchange.get_max_leverage("BTC/USDT", 170.30) == 125.0
assert pytest.approx(exchange.get_max_leverage("BNB/BUSD", 99999.9)) == 5.000005
assert pytest.approx(exchange.get_max_leverage("BNB/USDT", 1500)) == 33.333333333333333
assert exchange.get_max_leverage("BTC/USDT", 300000000) == 2.0
assert exchange.get_max_leverage("BTC/USDT", 600000000) == 1.0 # Last tier
assert exchange.get_max_leverage("BNB/BUSD:BUSD", 1.0) == 20.0
assert exchange.get_max_leverage("BNB/USDT:USDT", 100.0) == 75.0
assert exchange.get_max_leverage("BTC/USDT:USDT", 170.30) == 125.0
assert pytest.approx(exchange.get_max_leverage("BNB/BUSD:BUSD", 99999.9)) == 5.000005
assert pytest.approx(exchange.get_max_leverage("BNB/USDT:USDT", 1500)) == 33.333333333333333
assert exchange.get_max_leverage("BTC/USDT:USDT", 300000000) == 2.0
assert exchange.get_max_leverage("BTC/USDT:USDT", 600000000) == 1.0 # Last tier
assert exchange.get_max_leverage("SPONGE/USDT", 200) == 1.0 # Pair not in leverage_tiers
assert exchange.get_max_leverage("BTC/USDT", 0.0) == 125.0 # No stake amount
assert exchange.get_max_leverage("SPONGE/USDT:USDT", 200) == 1.0 # Pair not in leverage_tiers
assert exchange.get_max_leverage("BTC/USDT:USDT", 0.0) == 125.0 # No stake amount
with pytest.raises(
InvalidOrderException,
match=r'Amount 1000000000.01 too high for BTC/USDT'
match=r'Amount 1000000000.01 too high for BTC/USDT:USDT'
):
exchange.get_max_leverage("BTC/USDT", 1000000000.01)
exchange.get_max_leverage("BTC/USDT:USDT", 1000000000.01)
@pytest.mark.parametrize("exchange_name", ['bittrex', 'binance', 'kraken', 'gateio', 'okx'])
@pytest.mark.parametrize("exchange_name", ['bittrex', 'binance', 'kraken', 'gate', 'okx'])
def test__get_params(mocker, default_conf, exchange_name):
api_mock = MagicMock()
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
@@ -5084,6 +5068,7 @@ def test__get_params(mocker, default_conf, exchange_name):
def test_get_liquidation_price1(mocker, default_conf):
api_mock = MagicMock()
leverage = 9.97
positions = [
{
'info': {},
@@ -5096,7 +5081,7 @@ def test_get_liquidation_price1(mocker, default_conf):
'maintenanceMarginPercentage': 0.025,
'entryPrice': 18.884,
'notional': 15.1072,
'leverage': 9.97,
'leverage': leverage,
'unrealizedPnl': 0.0048,
'contracts': 8,
'contractSize': 0.1,
@@ -5126,6 +5111,7 @@ def test_get_liquidation_price1(mocker, default_conf):
is_short=False,
amount=0.8,
stake_amount=18.884 * 0.8,
leverage=leverage,
wallet_balance=18.884 * 0.8,
)
assert liq_price == 17.47
@@ -5138,6 +5124,7 @@ def test_get_liquidation_price1(mocker, default_conf):
is_short=False,
amount=0.8,
stake_amount=18.884 * 0.8,
leverage=leverage,
wallet_balance=18.884 * 0.8,
)
assert liq_price == 17.540699999999998
@@ -5150,6 +5137,7 @@ def test_get_liquidation_price1(mocker, default_conf):
is_short=False,
amount=0.8,
stake_amount=18.884 * 0.8,
leverage=leverage,
wallet_balance=18.884 * 0.8,
)
assert liq_price is None
@@ -5163,17 +5151,18 @@ def test_get_liquidation_price1(mocker, default_conf):
is_short=False,
amount=0.8,
stake_amount=18.884 * 0.8,
leverage=leverage,
wallet_balance=18.884 * 0.8,
)
@pytest.mark.parametrize('liquidation_buffer', [0.0, 0.05])
@pytest.mark.parametrize('liquidation_buffer', [0.0])
@pytest.mark.parametrize(
"is_short,trading_mode,exchange_name,margin_mode,leverage,open_rate,amount,expected_liq", [
(False, 'spot', 'binance', '', 5.0, 10.0, 1.0, None),
(True, 'spot', 'binance', '', 5.0, 10.0, 1.0, None),
(False, 'spot', 'gateio', '', 5.0, 10.0, 1.0, None),
(True, 'spot', 'gateio', '', 5.0, 10.0, 1.0, None),
(False, 'spot', 'gate', '', 5.0, 10.0, 1.0, None),
(True, 'spot', 'gate', '', 5.0, 10.0, 1.0, None),
(False, 'spot', 'okx', '', 5.0, 10.0, 1.0, None),
(True, 'spot', 'okx', '', 5.0, 10.0, 1.0, None),
# Binance, short
@@ -5186,16 +5175,26 @@ def test_get_liquidation_price1(mocker, default_conf):
(False, 'futures', 'binance', 'isolated', 5, 8, 1.0, 6.454545454545454),
(False, 'futures', 'binance', 'isolated', 3, 10, 1.0, 6.723905723905723),
(False, 'futures', 'binance', 'isolated', 5, 10, 0.6, 8.063973063973064),
# Gateio/okx, short
(True, 'futures', 'gateio', 'isolated', 5, 10, 1.0, 11.87413417771621),
(True, 'futures', 'gateio', 'isolated', 5, 10, 2.0, 11.87413417771621),
(True, 'futures', 'gateio', 'isolated', 3, 10, 1.0, 13.193482419684678),
(True, 'futures', 'gateio', 'isolated', 5, 8, 1.0, 9.499307342172967),
# Gate/okx, short
(True, 'futures', 'gate', 'isolated', 5, 10, 1.0, 11.87413417771621),
(True, 'futures', 'gate', 'isolated', 5, 10, 2.0, 11.87413417771621),
(True, 'futures', 'gate', 'isolated', 3, 10, 1.0, 13.193482419684678),
(True, 'futures', 'gate', 'isolated', 5, 8, 1.0, 9.499307342172967),
(True, 'futures', 'okx', 'isolated', 3, 10, 1.0, 13.193482419684678),
# Gateio/okx, long
(False, 'futures', 'gateio', 'isolated', 5.0, 10.0, 1.0, 8.085708510208207),
(False, 'futures', 'gateio', 'isolated', 3.0, 10.0, 1.0, 6.738090425173506),
# Gate/okx, long
(False, 'futures', 'gate', 'isolated', 5.0, 10.0, 1.0, 8.085708510208207),
(False, 'futures', 'gate', 'isolated', 3.0, 10.0, 1.0, 6.738090425173506),
(False, 'futures', 'okx', 'isolated', 3.0, 10.0, 1.0, 6.738090425173506),
# bybit, long
(False, 'futures', 'bybit', 'isolated', 1.0, 10.0, 1.0, 0.1),
(False, 'futures', 'bybit', 'isolated', 3.0, 10.0, 1.0, 6.7666666),
(False, 'futures', 'bybit', 'isolated', 5.0, 10.0, 1.0, 8.1),
(False, 'futures', 'bybit', 'isolated', 10.0, 10.0, 1.0, 9.1),
# bybit, short
(True, 'futures', 'bybit', 'isolated', 1.0, 10.0, 1.0, 19.9),
(True, 'futures', 'bybit', 'isolated', 3.0, 10.0, 1.0, 13.233333),
(True, 'futures', 'bybit', 'isolated', 5.0, 10.0, 1.0, 11.9),
(True, 'futures', 'bybit', 'isolated', 10.0, 10.0, 1.0, 10.9),
]
)
def test_get_liquidation_price(
@@ -5241,7 +5240,7 @@ def test_get_liquidation_price(
leverage = 5, open_rate = 10, amount = 0.6
((1.6 + 0.01) - (1 * 0.6 * 10)) / ((0.6 * 0.01) - (1 * 0.6)) = 7.39057239057239
Gateio/Okx, Short
Gate/Okx, Short
leverage = 5, open_rate = 10, amount = 1.0
(open_rate + (wallet_balance / position)) / (1 + (mm_ratio + taker_fee_rate))
(10 + (2 / 1.0)) / (1 + (0.01 + 0.0006)) = 11.87413417771621
@@ -5252,7 +5251,7 @@ def test_get_liquidation_price(
leverage = 5, open_rate = 8, amount = 1.0
(8 + (1.6 / 1.0)) / (1 + (0.01 + 0.0006)) = 9.499307342172967
Gateio/Okx, Long
Gate/Okx, Long
leverage = 5, open_rate = 10, amount = 1.0
(open_rate - (wallet_balance / position)) / (1 - (mm_ratio + taker_fee_rate))
(10 - (2 / 1)) / (1 - (0.01 + 0.0006)) = 8.085708510208207
@@ -5267,7 +5266,7 @@ def test_get_liquidation_price(
default_conf_usdt['trading_mode'] = trading_mode
default_conf_usdt['exchange']['name'] = exchange_name
default_conf_usdt['margin_mode'] = margin_mode
mocker.patch('freqtrade.exchange.Gateio.validate_ordertypes')
mocker.patch('freqtrade.exchange.Gate.validate_ordertypes')
exchange = get_patched_exchange(mocker, default_conf_usdt, id=exchange_name)
exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01))
@@ -5281,7 +5280,7 @@ def test_get_liquidation_price(
amount=amount,
stake_amount=amount * open_rate / leverage,
wallet_balance=amount * open_rate / leverage,
# leverage=leverage,
leverage=leverage,
is_short=is_short,
)
if expected_liq is None:
@@ -5319,7 +5318,7 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun
exchange.get_contract_size = MagicMock(return_value=contract_size)
api_mock.create_order.reset_mock()
order = exchange.stoploss(
order = exchange.create_stoploss(
pair='ETH/BTC',
amount=100,
stop_price=220,

View File

@@ -5,22 +5,22 @@ import pytest
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Gateio
from freqtrade.exchange import Gate
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from tests.conftest import get_patched_exchange
def test_validate_order_types_gateio(default_conf, mocker):
default_conf['exchange']['name'] = 'gateio'
def test_validate_order_types_gate(default_conf, mocker):
default_conf['exchange']['name'] = 'gate'
mocker.patch('freqtrade.exchange.Exchange._init_ccxt')
mocker.patch('freqtrade.exchange.Exchange._load_markets', return_value={})
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex')
exch = ExchangeResolver.load_exchange('gateio', default_conf, True)
assert isinstance(exch, Gateio)
mocker.patch('freqtrade.exchange.Exchange.name', 'Gate')
exch = ExchangeResolver.load_exchange('gate', default_conf, True)
assert isinstance(exch, Gate)
default_conf['order_types'] = {
'entry': 'market',
@@ -31,18 +31,18 @@ def test_validate_order_types_gateio(default_conf, mocker):
with pytest.raises(OperationalException,
match=r'Exchange .* does not support market orders.'):
ExchangeResolver.load_exchange('gateio', default_conf, True)
ExchangeResolver.load_exchange('gate', default_conf, True)
# market-orders supported on futures markets.
default_conf['trading_mode'] = 'futures'
default_conf['margin_mode'] = 'isolated'
ex = ExchangeResolver.load_exchange('gateio', default_conf, True)
ex = ExchangeResolver.load_exchange('gate', default_conf, True)
assert ex
@pytest.mark.usefixtures("init_persistence")
def test_fetch_stoploss_order_gateio(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, id='gateio')
def test_fetch_stoploss_order_gate(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, id='gate')
fetch_order_mock = MagicMock()
exchange.fetch_order = fetch_order_mock
@@ -56,7 +56,7 @@ def test_fetch_stoploss_order_gateio(default_conf, mocker):
default_conf['trading_mode'] = 'futures'
default_conf['margin_mode'] = 'isolated'
exchange = get_patched_exchange(mocker, default_conf, id='gateio')
exchange = get_patched_exchange(mocker, default_conf, id='gate')
exchange.fetch_order = MagicMock(return_value={
'status': 'closed',
@@ -73,8 +73,8 @@ def test_fetch_stoploss_order_gateio(default_conf, mocker):
assert exchange.fetch_order.call_args_list[1][1]['order_id'] == '222555'
def test_cancel_stoploss_order_gateio(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, id='gateio')
def test_cancel_stoploss_order_gate(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, id='gate')
cancel_order_mock = MagicMock()
exchange.cancel_order = cancel_order_mock
@@ -90,8 +90,8 @@ def test_cancel_stoploss_order_gateio(default_conf, mocker):
(1501, 1499, 1501, "sell"),
(1499, 1501, 1499, "buy")
])
def test_stoploss_adjust_gateio(mocker, default_conf, sl1, sl2, sl3, side):
exchange = get_patched_exchange(mocker, default_conf, id='gateio')
def test_stoploss_adjust_gate(mocker, default_conf, sl1, sl2, sl3, side):
exchange = get_patched_exchange(mocker, default_conf, id='gate')
order = {
'price': 1500,
'stopPrice': 1500,
@@ -104,7 +104,7 @@ def test_stoploss_adjust_gateio(mocker, default_conf, sl1, sl2, sl3, side):
('taker', 0.0005, 0.0001554325),
('maker', 0.0, 0.0),
])
def test_fetch_my_trades_gateio(mocker, default_conf, takerormaker, rate, cost):
def test_fetch_my_trades_gate(mocker, default_conf, takerormaker, rate, cost):
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
tick = {'ETH/USDT:USDT': {
'info': {'user_id': '',
@@ -134,7 +134,7 @@ def test_fetch_my_trades_gateio(mocker, default_conf, takerormaker, rate, cost):
'takerOrMaker': takerormaker,
'amount': 1, # 1 contract
}])
exchange = get_patched_exchange(mocker, default_conf, api_mock=api_mock, id='gateio')
exchange = get_patched_exchange(mocker, default_conf, api_mock=api_mock, id='gate')
exchange._trading_fees = tick
trades = exchange.get_trades_for_order('22255', 'ETH/USDT:USDT', datetime.now(timezone.utc))
trade = trades[0]

View File

@@ -14,7 +14,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers
(0.99, 220 * 0.99, "sell"),
(0.98, 220 * 0.98, "sell"),
])
def test_stoploss_order_huobi(default_conf, mocker, limitratio, expected, side):
def test_create_stoploss_order_huobi(default_conf, mocker, limitratio, expected, side):
api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
order_type = 'stop-limit'
@@ -32,15 +32,15 @@ def test_stoploss_order_huobi(default_conf, mocker, limitratio, expected, side):
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},
side=side,
leverage=1.0)
order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190,
order_types={'stoploss_on_exchange_limit_ratio': 1.05},
side=side,
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, leverage=1.0)
order = exchange.create_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
@@ -59,23 +59,23 @@ def test_stoploss_order_huobi(default_conf, mocker, limitratio, expected, side):
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={}, side=side, leverage=1.0)
exchange.create_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, leverage=1.0)
exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=220,
order_types={}, side=side, leverage=1.0)
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "huobi",
"stoploss", "create_order", retries=1,
"create_stoploss", "create_order", retries=1,
pair='ETH/BTC', amount=1, stop_price=220, order_types={},
side=side, leverage=1.0)
def test_stoploss_order_dry_run_huobi(default_conf, mocker):
def test_create_stoploss_order_dry_run_huobi(default_conf, mocker):
api_mock = MagicMock()
order_type = 'stop-limit'
default_conf['dry_run'] = True
@@ -85,14 +85,14 @@ def test_stoploss_order_dry_run_huobi(default_conf, mocker):
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},
side='sell', leverage=1.0)
order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190,
order_types={'stoploss_on_exchange_limit_ratio': 1.05},
side='sell', leverage=1.0)
api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
order_types={}, side='sell', leverage=1.0)
order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=220,
order_types={}, side='sell', leverage=1.0)
assert 'id' in order
assert 'info' in order

View File

@@ -179,7 +179,7 @@ def test_get_balances_prod(default_conf, mocker):
("sell", 217.8),
("buy", 222.2),
])
def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedprice):
def test_create_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedprice):
api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
@@ -196,7 +196,7 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
order = exchange.stoploss(
order = exchange.create_stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
@@ -230,7 +230,7 @@ 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(
exchange.create_stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
@@ -243,7 +243,7 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr
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(
exchange.create_stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,
@@ -253,13 +253,13 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr
)
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken",
"stoploss", "create_order", retries=1,
"create_stoploss", "create_order", retries=1,
pair='ETH/BTC', amount=1, stop_price=220, order_types={},
side=side, leverage=1.0)
@pytest.mark.parametrize('side', ['buy', 'sell'])
def test_stoploss_order_dry_run_kraken(default_conf, mocker, side):
def test_create_stoploss_order_dry_run_kraken(default_conf, mocker, side):
api_mock = MagicMock()
default_conf['dry_run'] = True
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
@@ -269,7 +269,7 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker, side):
api_mock.create_order.reset_mock()
order = exchange.stoploss(
order = exchange.create_stoploss(
pair='ETH/BTC',
amount=1,
stop_price=220,

View File

@@ -15,7 +15,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers
(0.99, 220 * 0.99, "sell"),
(0.98, 220 * 0.98, "sell"),
])
def test_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, side, order_type):
def test_create_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, side, order_type):
api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
@@ -32,18 +32,18 @@ def test_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, side,
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},
side=side, leverage=1.0)
order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190,
order_types={
'stoploss': order_type,
'stoploss_on_exchange_limit_ratio': 1.05},
side=side, leverage=1.0)
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, side=side, leverage=1.0)
order = exchange.create_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
@@ -67,18 +67,18 @@ def test_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, side,
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={}, side=side, leverage=1.0)
exchange.create_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("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={}, side=side, leverage=1.0)
exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=220,
order_types={}, side=side, leverage=1.0)
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kucoin",
"stoploss", "create_order", retries=1,
"create_stoploss", "create_order", retries=1,
pair='ETH/BTC', amount=1, stop_price=220, order_types={},
side=side, leverage=1.0)
@@ -93,15 +93,15 @@ def test_stoploss_order_dry_run_kucoin(default_conf, mocker):
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},
side='sell', leverage=1.0)
order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190,
order_types={'stoploss': 'limit',
'stoploss_on_exchange_limit_ratio': 1.05},
side='sell', leverage=1.0)
api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
order_types={}, side='sell', leverage=1.0)
order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=220,
order_types={}, side='sell', leverage=1.0)
assert 'id' in order
assert 'info' in order

View File

@@ -195,12 +195,12 @@ def test_get_max_pair_stake_amount_okx(default_conf, mocker, leverage_tiers):
exchange = get_patched_exchange(mocker, default_conf, id="okx")
exchange._leverage_tiers = leverage_tiers
assert exchange.get_max_pair_stake_amount('BNB/BUSD', 1.0) == 30000000
assert exchange.get_max_pair_stake_amount('BNB/USDT', 1.0) == 50000000
assert exchange.get_max_pair_stake_amount('BTC/USDT', 1.0) == 1000000000
assert exchange.get_max_pair_stake_amount('BTC/USDT', 1.0, 10.0) == 100000000
assert exchange.get_max_pair_stake_amount('BNB/BUSD:BUSD', 1.0) == 30000000
assert exchange.get_max_pair_stake_amount('BNB/USDT:USDT', 1.0) == 50000000
assert exchange.get_max_pair_stake_amount('BTC/USDT:USDT', 1.0) == 1000000000
assert exchange.get_max_pair_stake_amount('BTC/USDT:USDT', 1.0, 10.0) == 100000000
assert exchange.get_max_pair_stake_amount('TTT/USDT', 1.0) == float('inf') # Not in tiers
assert exchange.get_max_pair_stake_amount('TTT/USDT:USDT', 1.0) == float('inf') # Not in tiers
@pytest.mark.parametrize('mode,side,reduceonly,result', [

View File

@@ -82,7 +82,7 @@ def test_compute_distances(mocker, freqai_conf):
freqai = make_data_dictionary(mocker, freqai_conf)
freqai_conf['freqai']['feature_parameters'].update({"DI_threshold": 1})
avg_mean_dist = freqai.dk.compute_distances()
assert round(avg_mean_dist, 2) == 1.99
assert round(avg_mean_dist, 2) == 1.98
def test_use_SVM_to_remove_outliers_and_outlier_protection(mocker, freqai_conf, caplog):
@@ -90,7 +90,7 @@ def test_use_SVM_to_remove_outliers_and_outlier_protection(mocker, freqai_conf,
freqai_conf['freqai']['feature_parameters'].update({"outlier_protection_percentage": 0.1})
freqai.dk.use_SVM_to_remove_outliers(predict=False)
assert log_has_re(
"SVM detected 7.36%",
"SVM detected 7.83%",
caplog,
)

View File

@@ -27,21 +27,24 @@ def is_mac() -> bool:
return "Darwin" in machine
@pytest.mark.parametrize('model, pca, dbscan, float32, shuffle', [
('LightGBMRegressor', True, False, True, False),
('XGBoostRegressor', False, True, False, False),
('XGBoostRFRegressor', False, False, False, False),
('CatboostRegressor', False, False, False, True),
('ReinforcementLearner', False, True, False, False),
('ReinforcementLearner_multiproc', False, False, False, False),
('ReinforcementLearner_test_4ac', False, False, False, False)
@pytest.mark.parametrize('model, pca, dbscan, float32, can_short, shuffle', [
('LightGBMRegressor', True, False, True, True, False),
('XGBoostRegressor', False, True, False, True, False),
('XGBoostRFRegressor', False, False, False, True, False),
('CatboostRegressor', False, False, False, True, True),
('ReinforcementLearner', False, True, False, True, False),
('ReinforcementLearner_multiproc', False, False, False, True, False),
('ReinforcementLearner_test_3ac', False, False, False, False, False),
('ReinforcementLearner_test_3ac', False, False, False, True, False),
('ReinforcementLearner_test_4ac', False, False, False, True, False)
])
def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
dbscan, float32, shuffle):
dbscan, float32, can_short, shuffle):
if is_arm() and model == 'CatboostRegressor':
pytest.skip("CatBoost is not supported on ARM")
if is_mac() and 'Reinforcement' in model:
if is_mac() and not is_arm() and 'Reinforcement' in model:
pytest.skip("Reinforcement learning module not available on intel based Mac OS")
model_save_ext = 'joblib'
@@ -60,9 +63,6 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
freqai_conf['freqai']['feature_parameters'].update({"use_SVM_to_remove_outliers": True})
freqai_conf['freqai']['data_split_parameters'].update({'shuffle': True})
if 'test_4ac' in model:
freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models")
if 'ReinforcementLearner' in model:
model_save_ext = 'zip'
freqai_conf = make_rl_config(freqai_conf)
@@ -70,7 +70,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
freqai_conf['freqai']['feature_parameters'].update({"use_SVM_to_remove_outliers": True})
freqai_conf['freqai']['data_split_parameters'].update({'shuffle': True})
if 'test_4ac' in model:
if 'test_3ac' in model or 'test_4ac' in model:
freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models")
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
@@ -79,6 +79,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca,
strategy.freqai_info = freqai_conf.get("freqai", {})
freqai = strategy.freqai
freqai.live = True
freqai.can_short = can_short
freqai.dk = FreqaiDataKitchen(freqai_conf)
freqai.dk.set_paths('ADA/BTC', 10000)
timerange = TimeRange.parse_timerange("20180110-20180130")
@@ -223,6 +224,9 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog)
if 'test_4ac' in model:
freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models")
freqai_conf.get("freqai", {}).get("feature_parameters", {}).update(
{"indicator_periods_candles": [2]})
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
exchange = get_patched_exchange(mocker, freqai_conf)
strategy.dp = DataProvider(freqai_conf, exchange)
@@ -233,15 +237,14 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog)
timerange = TimeRange.parse_timerange("20180110-20180130")
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
sub_timerange = TimeRange.parse_timerange("20180110-20180130")
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
_, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
df = base_df[freqai_conf["timeframe"]]
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
df = freqai.cache_corr_pairlist_dfs(df, freqai.dk)
for i in range(5):
df[f'%-constant_{i}'] = i
metadata = {"pair": "LTC/BTC"}
freqai.start_backtesting(df, metadata, freqai.dk)
freqai.start_backtesting(df, metadata, freqai.dk, strategy)
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
assert len(model_folders) == num_files
@@ -262,6 +265,8 @@ def test_start_backtesting_subdaily_backtest_period(mocker, freqai_conf):
freqai_conf.update({"timerange": "20180120-20180124"})
freqai_conf.get("freqai", {}).update({"backtest_period_days": 0.5})
freqai_conf.get("freqai", {}).update({"save_backtest_models": True})
freqai_conf.get("freqai", {}).get("feature_parameters", {}).update(
{"indicator_periods_candles": [2]})
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
exchange = get_patched_exchange(mocker, freqai_conf)
strategy.dp = DataProvider(freqai_conf, exchange)
@@ -272,12 +277,11 @@ def test_start_backtesting_subdaily_backtest_period(mocker, freqai_conf):
timerange = TimeRange.parse_timerange("20180110-20180130")
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
sub_timerange = TimeRange.parse_timerange("20180110-20180130")
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
_, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
df = base_df[freqai_conf["timeframe"]]
metadata = {"pair": "LTC/BTC"}
freqai.start_backtesting(df, metadata, freqai.dk)
freqai.start_backtesting(df, metadata, freqai.dk, strategy)
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
assert len(model_folders) == 9
@@ -288,6 +292,8 @@ def test_start_backtesting_subdaily_backtest_period(mocker, freqai_conf):
def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
freqai_conf.update({"timerange": "20180120-20180130"})
freqai_conf.get("freqai", {}).update({"save_backtest_models": True})
freqai_conf.get("freqai", {}).get("feature_parameters", {}).update(
{"indicator_periods_candles": [2]})
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
exchange = get_patched_exchange(mocker, freqai_conf)
strategy.dp = DataProvider(freqai_conf, exchange)
@@ -297,15 +303,14 @@ def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
freqai.dk = FreqaiDataKitchen(freqai_conf)
timerange = TimeRange.parse_timerange("20180110-20180130")
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
sub_timerange = TimeRange.parse_timerange("20180110-20180130")
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
sub_timerange = TimeRange.parse_timerange("20180101-20180130")
_, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
df = base_df[freqai_conf["timeframe"]]
pair = "ADA/BTC"
metadata = {"pair": pair}
freqai.dk.pair = pair
freqai.start_backtesting(df, metadata, freqai.dk)
freqai.start_backtesting(df, metadata, freqai.dk, strategy)
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
assert len(model_folders) == 2
@@ -323,14 +328,13 @@ def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
timerange = TimeRange.parse_timerange("20180110-20180130")
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
sub_timerange = TimeRange.parse_timerange("20180110-20180130")
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
_, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
df = base_df[freqai_conf["timeframe"]]
pair = "ADA/BTC"
metadata = {"pair": pair}
freqai.dk.pair = pair
freqai.start_backtesting(df, metadata, freqai.dk)
freqai.start_backtesting(df, metadata, freqai.dk, strategy)
assert log_has_re(
"Found backtesting prediction file ",
@@ -340,7 +344,7 @@ def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
pair = "ETH/BTC"
metadata = {"pair": pair}
freqai.dk.pair = pair
freqai.start_backtesting(df, metadata, freqai.dk)
freqai.start_backtesting(df, metadata, freqai.dk, strategy)
path = (freqai.dd.full_path / freqai.dk.backtest_predictions_folder)
prediction_files = [x for x in path.iterdir() if x.is_file()]
@@ -374,57 +378,6 @@ def test_backtesting_fit_live_predictions(mocker, freqai_conf, caplog):
shutil.rmtree(Path(freqai.dk.full_path))
def test_follow_mode(mocker, freqai_conf):
freqai_conf.update({"timerange": "20180110-20180130"})
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
exchange = get_patched_exchange(mocker, freqai_conf)
strategy.dp = DataProvider(freqai_conf, exchange)
strategy.freqai_info = freqai_conf.get("freqai", {})
freqai = strategy.freqai
freqai.live = True
freqai.dk = FreqaiDataKitchen(freqai_conf)
timerange = TimeRange.parse_timerange("20180110-20180130")
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
metadata = {"pair": "ADA/BTC"}
freqai.dd.set_pair_dict_info(metadata)
data_load_timerange = TimeRange.parse_timerange("20180110-20180130")
new_timerange = TimeRange.parse_timerange("20180120-20180130")
freqai.extract_data_and_train_model(
new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange)
assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file()
assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").is_file()
assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").is_file()
assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").is_file()
# start the follower and ask it to predict on existing files
freqai_conf.get("freqai", {}).update({"follow_mode": "true"})
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
exchange = get_patched_exchange(mocker, freqai_conf)
strategy.dp = DataProvider(freqai_conf, exchange)
strategy.freqai_info = freqai_conf.get("freqai", {})
freqai = strategy.freqai
freqai.live = True
freqai.dk = FreqaiDataKitchen(freqai_conf, freqai.live)
timerange = TimeRange.parse_timerange("20180110-20180130")
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
df = strategy.dp.get_pair_dataframe('ADA/BTC', '5m')
freqai.dk.pair = "ADA/BTC"
freqai.start_live(df, metadata, strategy, freqai.dk)
assert len(freqai.dk.return_dataframe.index) == 5702
shutil.rmtree(Path(freqai.dk.full_path))
def test_principal_component_analysis(mocker, freqai_conf):
freqai_conf.update({"timerange": "20180110-20180130"})
freqai_conf.get("freqai", {}).get("feature_parameters", {}).update(

View File

@@ -0,0 +1,65 @@
import logging
import numpy as np
from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner
from freqtrade.freqai.RL.Base3ActionRLEnv import Actions, Base3ActionRLEnv, Positions
logger = logging.getLogger(__name__)
class ReinforcementLearner_test_3ac(ReinforcementLearner):
"""
User created Reinforcement Learning Model prediction model.
"""
class MyRLEnv(Base3ActionRLEnv):
"""
User can override any function in BaseRLEnv and gym.Env. Here the user
sets a custom reward based on profit and trade duration.
"""
def calculate_reward(self, action: int) -> float:
# first, penalize if the action is not valid
if not self._is_valid(action):
return -2
pnl = self.get_unrealized_profit()
rew = np.sign(pnl) * (pnl + 1)
factor = 100.
# reward agent for entering trades
if (action in (Actions.Buy.value, Actions.Sell.value)
and self._position == Positions.Neutral):
return 25
# discourage agent from not entering trades
if action == Actions.Neutral.value and self._position == Positions.Neutral:
return -1
max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300)
trade_duration = self._current_tick - self._last_trade_tick # type: ignore
if trade_duration <= max_trade_duration:
factor *= 1.5
elif trade_duration > max_trade_duration:
factor *= 0.5
# discourage sitting in position
if self._position in (Positions.Short, Positions.Long) and (
action == Actions.Neutral.value
or (action == Actions.Sell.value and self._position == Positions.Short)
or (action == Actions.Buy.value and self._position == Positions.Long)
):
return -1 * trade_duration / max_trade_duration
# close position
if (action == Actions.Buy.value and self._position == Positions.Short) or (
action == Actions.Sell.value and self._position == Positions.Long
):
if pnl > self.profit_aim * self.rr:
factor *= self.rl_config["model_reward_parameters"].get("win_reward_factor", 2)
return float(rew * factor)
return 0.

View File

@@ -1,5 +1,6 @@
import pytest
from freqtrade.exceptions import OperationalException
from freqtrade.leverage import interest
from freqtrade.util import FtPrecise
@@ -29,3 +30,13 @@ def test_interest(exchange, interest_rate, hours, expected):
rate=FtPrecise(interest_rate),
hours=hours
))) == expected
def test_interest_exception():
with pytest.raises(OperationalException, match=r"Leverage not available on .* with freqtrade"):
interest(
exchange_name='bitmex',
borrowed=FtPrecise(60.0),
rate=FtPrecise(0.0005),
hours=ten_mins
)

View File

@@ -48,8 +48,8 @@ def hyperopt_results():
return pd.DataFrame(
{
'pair': ['ETH/USDT', 'ETH/USDT', 'ETH/USDT', 'ETH/USDT'],
'profit_ratio': [-0.1, 0.2, -0.1, 0.3],
'profit_abs': [-0.2, 0.4, -0.2, 0.6],
'profit_ratio': [-0.1, 0.2, -0.12, 0.3],
'profit_abs': [-0.2, 0.4, -0.21, 0.6],
'trade_duration': [10, 30, 10, 10],
'amount': [0.1, 0.1, 0.1, 0.1],
'exit_reason': [ExitType.STOP_LOSS, ExitType.ROI, ExitType.STOP_LOSS, ExitType.ROI],

View File

@@ -919,6 +919,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer)
default_conf["trailing_stop_positive"] = data.trailing_stop_positive
default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset
default_conf["use_exit_signal"] = data.use_exit_signal
default_conf["max_open_trades"] = 10
mocker.patch("freqtrade.exchange.Exchange.get_fee", return_value=0.0)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
@@ -951,7 +952,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer)
processed=data_processed,
start_date=min_date,
end_date=max_date,
max_open_trades=10,
)
results = result['results']

View File

@@ -19,12 +19,12 @@ from freqtrade.data.btanalysis import BT_DATA_COLUMNS, evaluate_result_multi
from freqtrade.data.converter import clean_ohlcv_dataframe
from freqtrade.data.dataprovider import DataProvider
from freqtrade.data.history import get_timerange
from freqtrade.enums import ExitType, RunMode
from freqtrade.enums import CandleType, ExitType, RunMode
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exchange.exchange import timeframe_to_next_date
from freqtrade.optimize.backtest_caching import get_strategy_run_id
from freqtrade.optimize.backtesting import Backtesting
from freqtrade.persistence import LocalTrade
from freqtrade.persistence import LocalTrade, Trade
from freqtrade.resolvers import StrategyResolver
from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange,
patched_configuration_load_config_file)
@@ -96,7 +96,6 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'):
'processed': processed,
'start_date': min_date,
'end_date': max_date,
'max_open_trades': 10,
}
@@ -360,7 +359,6 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
PropertyMock(return_value=['UNITTEST/BTC']))
default_conf['timeframe'] = '1m'
default_conf['datadir'] = testdatadir
default_conf['export'] = 'signals'
default_conf['exportfilename'] = 'export.txt'
default_conf['timerange'] = '-1510694220'
@@ -396,7 +394,6 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) ->
PropertyMock(return_value=['UNITTEST/BTC']))
default_conf['timeframe'] = "1m"
default_conf['datadir'] = testdatadir
default_conf['export'] = 'none'
default_conf['timerange'] = '20180101-20180102'
@@ -417,7 +414,6 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) ->
PropertyMock(return_value=[]))
default_conf['timeframe'] = "1m"
default_conf['datadir'] = testdatadir
default_conf['export'] = 'none'
default_conf['timerange'] = '20180101-20180102'
@@ -451,7 +447,6 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.refresh_pairlist')
default_conf['ticker_interval'] = "1m"
default_conf['datadir'] = testdatadir
default_conf['export'] = 'none'
# Use stoploss from strategy
del default_conf['stoploss']
@@ -619,7 +614,7 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None:
assert trade is None
def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
def test_backtest__check_trade_exit(default_conf, fee, mocker) -> None:
default_conf['use_exit_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
@@ -665,7 +660,7 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
]
# No data available.
res = backtesting._get_exit_trade_entry(trade, row_sell, True)
res = backtesting._check_trade_exit(trade, row_sell)
assert res is not None
assert res.exit_reason == ExitType.ROI.value
assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc)
@@ -678,12 +673,14 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
[], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
'enter_short', 'exit_short', 'long_tag', 'short_tag', 'exit_tag'])
res = backtesting._get_exit_trade_entry(trade, row, True)
res = backtesting._check_trade_exit(trade, row)
assert res is None
def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
default_conf['use_exit_signal'] = False
default_conf['max_open_trades'] = 10
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
@@ -701,7 +698,6 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
processed=deepcopy(processed),
start_date=min_date,
end_date=max_date,
max_open_trades=10,
)
results = result['results']
assert not results.empty
@@ -710,6 +706,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
expected = pd.DataFrame(
{'pair': [pair, pair],
'stake_amount': [0.001, 0.001],
'max_stake_amount': [0.001, 0.001],
'amount': [0.00957442, 0.0097064],
'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime,
Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True
@@ -784,6 +781,8 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de
def custom_entry_price(proposed_rate, **kwargs):
return proposed_rate * 0.997
default_conf_usdt['max_open_trades'] = 10
backtesting = Backtesting(default_conf_usdt)
backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.populate_entry_trend = advise_entry
@@ -791,10 +790,10 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de
pair = 'XRP/ETH'
# Pick a timerange adapted to the pair we use to test
timerange = TimeRange.parse_timerange('20191010-20191013')
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['XRP/ETH'],
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=[pair],
timerange=timerange)
if use_detail:
data_1m = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['XRP/ETH'],
data_1m = history.load_data(datadir=testdatadir, timeframe='1m', pairs=[pair],
timerange=timerange)
backtesting.detail_data = data_1m
processed = backtesting.strategy.advise_all_indicators(data)
@@ -804,7 +803,6 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de
processed=deepcopy(processed),
start_date=min_date,
end_date=max_date,
max_open_trades=10,
)
results = result['results']
assert not results.empty
@@ -848,6 +846,164 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de
assert late_entry > 0
@pytest.mark.parametrize('use_detail', [True, False])
def test_backtest_one_detail_futures(
default_conf_usdt, fee, mocker, testdatadir, use_detail) -> None:
default_conf_usdt['use_exit_signal'] = False
default_conf_usdt['trading_mode'] = 'futures'
default_conf_usdt['margin_mode'] = 'isolated'
default_conf_usdt['candle_type_def'] = CandleType.FUTURES
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['XRP/USDT:USDT']))
mocker.patch("freqtrade.exchange.Exchange.get_maintenance_ratio_and_amt",
return_value=(0.01, 0.01))
default_conf_usdt['timeframe'] = '1h'
if use_detail:
default_conf_usdt['timeframe_detail'] = '5m'
patch_exchange(mocker)
def advise_entry(df, *args, **kwargs):
# Mock function to force several entries
df.loc[(df['rsi'] < 40), 'enter_long'] = 1
return df
def custom_entry_price(proposed_rate, **kwargs):
return proposed_rate * 0.997
default_conf_usdt['max_open_trades'] = 10
backtesting = Backtesting(default_conf_usdt)
backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.populate_entry_trend = advise_entry
backtesting.strategy.custom_entry_price = custom_entry_price
pair = 'XRP/USDT:USDT'
# Pick a timerange adapted to the pair we use to test
timerange = TimeRange.parse_timerange('20211117-20211119')
data = history.load_data(datadir=Path(testdatadir), timeframe='1h', pairs=[pair],
timerange=timerange, candle_type=CandleType.FUTURES)
backtesting.load_bt_data_detail()
processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed)
result = backtesting.backtest(
processed=deepcopy(processed),
start_date=min_date,
end_date=max_date,
)
results = result['results']
assert not results.empty
# Timeout settings from default_conf = entry: 10, exit: 30
assert len(results) == (5 if use_detail else 2)
assert 'orders' in results.columns
data_pair = processed[pair]
data_1m_pair = backtesting.detail_data[pair] if use_detail else pd.DataFrame()
late_entry = 0
for _, t in results.iterrows():
assert len(t['orders']) == 2
entryo = t['orders'][0]
entry_ts = datetime.fromtimestamp(entryo['order_filled_timestamp'] // 1000, tz=timezone.utc)
if entry_ts > t['open_date']:
late_entry += 1
# Get "entry fill" candle
ln = (data_1m_pair.loc[data_1m_pair["date"] == entry_ts]
if use_detail else data_pair.loc[data_pair["date"] == entry_ts])
# Check open trade rate aligns to open rate
assert not ln.empty
assert round(ln.iloc[0]["low"], 6) <= round(
t["open_rate"], 6) <= round(ln.iloc[0]["high"], 6)
# check close trade rate aligns to close rate or is between high and low
ln1 = data_pair.loc[data_pair["date"] == t["close_date"]]
if use_detail:
ln1_1m = data_1m_pair.loc[data_1m_pair["date"] == t["close_date"]]
assert not ln1.empty or not ln1_1m.empty
else:
assert not ln1.empty
ln2 = ln1_1m if ln1.empty else ln1
assert (round(ln2.iloc[0]["low"], 6) <= round(
t["close_rate"], 6) <= round(ln2.iloc[0]["high"], 6))
assert -0.0181 < Trade.trades[1].funding_fees < -0.01
# assert late_entry > 0
@pytest.mark.parametrize('use_detail', [True, False])
def test_backtest_one_detail_futures_funding_fees(
default_conf_usdt, fee, mocker, testdatadir, use_detail) -> None:
default_conf_usdt['use_exit_signal'] = False
default_conf_usdt['trading_mode'] = 'futures'
default_conf_usdt['margin_mode'] = 'isolated'
default_conf_usdt['candle_type_def'] = CandleType.FUTURES
default_conf_usdt['minimal_roi'] = {'0': 1}
default_conf_usdt['dry_run_wallet'] = 100000
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['XRP/USDT:USDT']))
mocker.patch("freqtrade.exchange.Exchange.get_maintenance_ratio_and_amt",
return_value=(0.01, 0.01))
default_conf_usdt['timeframe'] = '1h'
if use_detail:
default_conf_usdt['timeframe_detail'] = '5m'
patch_exchange(mocker)
def advise_entry(df, *args, **kwargs):
# Mock function to force several entries
df.loc[:, 'enter_long'] = 1
return df
def adjust_trade_position(trade, current_time, **kwargs):
if current_time > datetime(2021, 11, 18, 2, 0, 0, tzinfo=timezone.utc):
return None
return default_conf_usdt['stake_amount']
default_conf_usdt['max_open_trades'] = 1
backtesting = Backtesting(default_conf_usdt)
backtesting._set_strategy(backtesting.strategylist[0])
backtesting.strategy.populate_entry_trend = advise_entry
backtesting.strategy.adjust_trade_position = adjust_trade_position
backtesting.strategy.leverage = lambda **kwargs: 1
backtesting.strategy.position_adjustment_enable = True
pair = 'XRP/USDT:USDT'
# Pick a timerange adapted to the pair we use to test
timerange = TimeRange.parse_timerange('20211117-20211119')
data = history.load_data(datadir=Path(testdatadir), timeframe='1h', pairs=[pair],
timerange=timerange, candle_type=CandleType.FUTURES)
backtesting.load_bt_data_detail()
processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed)
result = backtesting.backtest(
processed=deepcopy(processed),
start_date=min_date,
end_date=max_date,
)
results = result['results']
assert not results.empty
# Only one result - as we're not selling.
assert len(results) == 1
assert 'orders' in results.columns
for t in Trade.trades:
# At least 4 adjustment orders
assert t.nr_of_successful_entries >= 6
# Funding fees will vary depending on the number of adjustment orders
# That number is a lot higher with detail data.
assert -20 < t.funding_fees < -0.1
def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir) -> None:
# This strategy intentionally places unfillable orders.
default_conf['strategy'] = 'StrategyTestV3CustomEntryPrice'
@@ -858,6 +1014,7 @@ def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
patch_exchange(mocker)
default_conf['max_open_trades'] = 1
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
# Testing dataframe contains 11 candles. Expecting 10 timed out orders.
@@ -870,7 +1027,6 @@ def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir)
processed=deepcopy(data),
start_date=min_date,
end_date=max_date,
max_open_trades=1,
)
assert result['timedout_entry_orders'] == 10
@@ -878,6 +1034,7 @@ def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir)
def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None:
default_conf['use_exit_signal'] = False
default_conf['max_open_trades'] = 1
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
@@ -895,7 +1052,6 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
processed=processed,
start_date=min_date,
end_date=max_date,
max_open_trades=1,
)
assert not results['results'].empty
assert len(results['results']) == 1
@@ -903,6 +1059,8 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
def test_backtest_trim_no_data_left(default_conf, fee, mocker, testdatadir) -> None:
default_conf['use_exit_signal'] = False
default_conf['max_open_trades'] = 10
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
@@ -926,7 +1084,6 @@ def test_backtest_trim_no_data_left(default_conf, fee, mocker, testdatadir) -> N
processed=deepcopy(processed),
start_date=min_date,
end_date=max_date,
max_open_trades=10,
)
@@ -947,6 +1104,7 @@ def test_processed(default_conf, mocker, testdatadir) -> None:
def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadir) -> None:
default_conf['use_exit_signal'] = False
default_conf['max_open_trades'] = 10
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=100000)
@@ -980,7 +1138,6 @@ def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadi
processed=deepcopy(processed),
start_date=min_date,
end_date=max_date,
max_open_trades=10,
)
assert count == 5
@@ -997,6 +1154,7 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
default_conf['enable_protections'] = True
default_conf['timeframe'] = '1m'
default_conf['max_open_trades'] = 1
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
@@ -1023,7 +1181,6 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
processed=processed,
start_date=min_date,
end_date=max_date,
max_open_trades=1,
)
assert len(results['results']) == numres
@@ -1061,11 +1218,12 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir,
processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed)
assert isinstance(processed, dict)
backtesting.strategy.max_open_trades = 1
backtesting.config.update({'max_open_trades': 1})
results = backtesting.backtest(
processed=processed,
start_date=min_date,
end_date=max_date,
max_open_trades=1,
)
assert len(results['results']) == expected
@@ -1076,7 +1234,7 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir):
buy_value = 1
sell_value = 1
return _trend(dataframe, buy_value, sell_value)
default_conf['max_open_trades'] = 10
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
@@ -1093,6 +1251,7 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir):
sell_value = 1
return _trend(dataframe, buy_value, sell_value)
default_conf['max_open_trades'] = 10
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
@@ -1106,6 +1265,7 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf'))
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
default_conf['max_open_trades'] = 10
backtest_conf = _make_backtest_conf(mocker, conf=default_conf,
pair='UNITTEST/BTC', datadir=testdatadir)
default_conf['timeframe'] = '1m'
@@ -1164,6 +1324,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
if tres > 0:
data[pair] = data[pair][tres:].reset_index()
default_conf['timeframe'] = '5m'
default_conf['max_open_trades'] = 3
backtesting = Backtesting(default_conf)
backtesting._set_strategy(backtesting.strategylist[0])
@@ -1172,11 +1333,11 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed)
backtest_conf = {
'processed': deepcopy(processed),
'start_date': min_date,
'end_date': max_date,
'max_open_trades': 3,
}
results = backtesting.backtest(**backtest_conf)
@@ -1194,11 +1355,12 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
backtesting.dataprovider.get_analyzed_dataframe('NXT/BTC', '5m')[0]
) == len(data['NXT/BTC']) - 1 - backtesting.strategy.startup_candle_count
backtesting.strategy.max_open_trades = 1
backtesting.config.update({'max_open_trades': 1})
backtest_conf = {
'processed': deepcopy(processed),
'start_date': min_date,
'end_date': max_date,
'max_open_trades': 1,
}
results = backtesting.backtest(**backtest_conf)
assert len(evaluate_result_multi(results['results'], '5m', 1)) == 0
@@ -1459,7 +1621,7 @@ def test_backtest_start_futures_noliq(default_conf_usdt, mocker,
patch_exchange(mocker)
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['HULUMULU/USDT', 'XRP/USDT']))
PropertyMock(return_value=['HULUMULU/USDT', 'XRP/USDT:USDT']))
# mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
patched_configuration_load_config_file(mocker, default_conf_usdt)
@@ -1490,7 +1652,7 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
"strategy": CURRENT_TEST_STRATEGY,
})
patch_exchange(mocker)
result1 = pd.DataFrame({'pair': ['XRP/USDT', 'XRP/USDT'],
result1 = pd.DataFrame({'pair': ['XRP/USDT:USDT', 'XRP/USDT:USDT'],
'profit_ratio': [0.0, 0.0],
'profit_abs': [0.0, 0.0],
'open_date': pd.to_datetime(['2021-11-18 18:00:00',
@@ -1506,7 +1668,7 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
'close_rate': [0.104969, 0.103541],
'exit_reason': [ExitType.ROI, ExitType.ROI]
})
result2 = pd.DataFrame({'pair': ['XRP/USDT', 'XRP/USDT', 'XRP/USDT'],
result2 = pd.DataFrame({'pair': ['XRP/USDT:USDT', 'XRP/USDT:USDT', 'XRP/USDT:USDT'],
'profit_ratio': [0.03, 0.01, 0.1],
'profit_abs': [0.01, 0.02, 0.2],
'open_date': pd.to_datetime(['2021-11-19 18:00:00',
@@ -1551,7 +1713,7 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
}
])
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
PropertyMock(return_value=['XRP/USDT']))
PropertyMock(return_value=['XRP/USDT:USDT']))
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
patched_configuration_load_config_file(mocker, default_conf_usdt)
@@ -1574,8 +1736,8 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
'up to 2021-11-21 04:00:00 (4 days).',
'Backtesting with data from 2021-11-17 21:00:00 '
'up to 2021-11-21 04:00:00 (3 days).',
'XRP/USDT, funding_rate, 8h, data starts at 2021-11-18 00:00:00',
'XRP/USDT, mark, 8h, data starts at 2021-11-18 00:00:00',
'XRP/USDT:USDT, funding_rate, 8h, data starts at 2021-11-18 00:00:00',
'XRP/USDT:USDT, mark, 8h, data starts at 2021-11-18 00:00:00',
f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}',
]

View File

@@ -17,6 +17,7 @@ from tests.conftest import patch_exchange
def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> None:
default_conf['use_exit_signal'] = False
default_conf['max_open_trades'] = 10
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch('freqtrade.optimize.backtesting.amount_to_contract_precision',
lambda x, *args, **kwargs: round(x, 8))
@@ -41,7 +42,6 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) ->
processed=deepcopy(processed),
start_date=min_date,
end_date=max_date,
max_open_trades=10,
)
results = result['results']
assert not results.empty
@@ -50,6 +50,7 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) ->
expected = pd.DataFrame(
{'pair': [pair, pair],
'stake_amount': [500.0, 100.0],
'max_stake_amount': [500.0, 100],
'amount': [4806.87657523, 970.63960782],
'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime,
Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True

View File

@@ -1,5 +1,6 @@
# pragma pylint: disable=missing-docstring,W0212,C0103
from datetime import datetime, timedelta
from functools import wraps
from pathlib import Path
from unittest.mock import ANY, MagicMock, PropertyMock
@@ -7,6 +8,7 @@ import pandas as pd
import pytest
from arrow import Arrow
from filelock import Timeout
from skopt.space import Integer
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt
from freqtrade.data.history import load_data
@@ -292,6 +294,8 @@ def test_params_no_optimize_details(hyperopt) -> None:
assert res['roi']['0'] == 0.04
assert "stoploss" in res
assert res['stoploss']['stoploss'] == -0.1
assert "max_open_trades" in res
assert res['max_open_trades']['max_open_trades'] == 1
def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
@@ -334,8 +338,7 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
assert dumper2.call_count == 1
assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
assert hasattr(hyperopt, "max_open_trades")
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf['max_open_trades']
assert hasattr(hyperopt.backtesting, "_position_stacking")
@@ -474,6 +477,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
'trailing_stop_positive': 0.02,
'trailing_stop_positive_offset_p1': 0.05,
'trailing_only_offset_is_reached': False,
'max_open_trades': 3,
}
response_expected = {
'loss': 1.9147239021396234,
@@ -499,7 +503,9 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
'trailing': {'trailing_only_offset_is_reached': False,
'trailing_stop': True,
'trailing_stop_positive': 0.02,
'trailing_stop_positive_offset': 0.07}},
'trailing_stop_positive_offset': 0.07},
'max_open_trades': {'max_open_trades': 3}
},
'params_dict': optimizer_param,
'params_not_optimized': {'buy': {}, 'protection': {}, 'sell': {}},
'results_metrics': ANY,
@@ -548,7 +554,8 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
'buy': {'mfi-value': None},
'sell': {'sell-mfi-value': None},
'roi': {}, 'stoploss': {'stoploss': None},
'trailing': {'trailing_stop': None}
'trailing': {'trailing_stop': None},
'max_open_trades': {'max_open_trades': None}
},
'results_metrics': generate_result_metrics(),
}])
@@ -571,7 +578,7 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
out, err = capsys.readouterr()
result_str = (
'{"params":{"mfi-value":null,"sell-mfi-value":null},"minimal_roi"'
':{},"stoploss":null,"trailing_stop":null}'
':{},"stoploss":null,"trailing_stop":null,"max_open_trades":null}'
)
assert result_str in out # noqa: E501
# Should be called for historical candle data
@@ -702,8 +709,7 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non
assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
assert hasattr(hyperopt, "max_open_trades")
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf['max_open_trades']
assert hasattr(hyperopt.backtesting, "_position_stacking")
@@ -776,8 +782,7 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
assert dumper2.call_count == 1
assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
assert hasattr(hyperopt, "max_open_trades")
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf['max_open_trades']
assert hasattr(hyperopt.backtesting, "_position_stacking")
@@ -819,8 +824,7 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
assert dumper2.call_count == 1
assert hasattr(hyperopt.backtesting.strategy, "advise_exit")
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
assert hasattr(hyperopt, "max_open_trades")
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf['max_open_trades']
assert hasattr(hyperopt.backtesting, "_position_stacking")
@@ -874,6 +878,7 @@ 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
assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value == 30
assert hyperopt.backtesting.strategy.max_open_trades == 1
buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range
assert isinstance(buy_rsi_range, range)
# Range from 0 - 50 (inclusive)
@@ -884,6 +889,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value != 30
assert hyperopt.backtesting.strategy.buy_rsi.value != 35
assert hyperopt.backtesting.strategy.sell_rsi.value != 74
assert hyperopt.backtesting.strategy.max_open_trades != 1
hyperopt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: 'ET1'
with pytest.raises(OperationalException, match="Estimator ET1 not supported."):
@@ -984,3 +990,124 @@ def test_SKDecimal():
assert space.transform([2.0]) == [200]
assert space.transform([1.0]) == [100]
assert space.transform([1.5, 1.6]) == [150, 160]
def test_stake_amount_unlimited_max_open_trades(mocker, hyperopt_conf, tmpdir, fee) -> None:
# This test is to ensure that unlimited max_open_trades are ignored for the backtesting
# if we have an unlimited stake amount
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
(Path(tmpdir) / 'hyperopt_results').mkdir(parents=True)
hyperopt_conf.update({
'strategy': 'HyperoptableStrategy',
'user_data_dir': Path(tmpdir),
'hyperopt_random_state': 42,
'spaces': ['trades'],
'stake_amount': 'unlimited'
})
hyperopt = Hyperopt(hyperopt_conf)
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._get_params_dict',
return_value={
'max_open_trades': -1
})
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
assert hyperopt.backtesting.strategy.max_open_trades == 1
hyperopt.start()
assert hyperopt.backtesting.strategy.max_open_trades == 1
def test_max_open_trades_dump(mocker, hyperopt_conf, tmpdir, fee, capsys) -> None:
# This test is to ensure that after hyperopting, max_open_trades is never
# saved as inf in the output json params
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
(Path(tmpdir) / 'hyperopt_results').mkdir(parents=True)
hyperopt_conf.update({
'strategy': 'HyperoptableStrategy',
'user_data_dir': Path(tmpdir),
'hyperopt_random_state': 42,
'spaces': ['trades'],
})
hyperopt = Hyperopt(hyperopt_conf)
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._get_params_dict',
return_value={
'max_open_trades': -1
})
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
hyperopt.start()
out, err = capsys.readouterr()
assert 'max_open_trades = -1' in out
assert 'max_open_trades = inf' not in out
##############
hyperopt_conf.update({'print_json': True})
hyperopt = Hyperopt(hyperopt_conf)
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._get_params_dict',
return_value={
'max_open_trades': -1
})
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
hyperopt.start()
out, err = capsys.readouterr()
assert '"max_open_trades":-1' in out
def test_max_open_trades_consistency(mocker, hyperopt_conf, tmpdir, fee) -> None:
# This test is to ensure that max_open_trades is the same across all functions needing it
# after it has been changed from the hyperopt
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0)
(Path(tmpdir) / 'hyperopt_results').mkdir(parents=True)
hyperopt_conf.update({
'strategy': 'HyperoptableStrategy',
'user_data_dir': Path(tmpdir),
'hyperopt_random_state': 42,
'spaces': ['trades'],
'stake_amount': 'unlimited',
'dry_run_wallet': 8,
'available_capital': 8,
'dry_run': True,
'epochs': 1
})
hyperopt = Hyperopt(hyperopt_conf)
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
hyperopt.custom_hyperopt.max_open_trades_space = lambda: [
Integer(1, 10, name='max_open_trades')]
first_time_evaluated = False
def stake_amount_interceptor(func):
@wraps(func)
def wrapper(*args, **kwargs):
nonlocal first_time_evaluated
stake_amount = func(*args, **kwargs)
if first_time_evaluated is False:
assert stake_amount == 1
first_time_evaluated = True
return stake_amount
return wrapper
hyperopt.backtesting.wallets._calculate_unlimited_stake_amount = stake_amount_interceptor(
hyperopt.backtesting.wallets._calculate_unlimited_stake_amount)
hyperopt.start()
assert hyperopt.backtesting.strategy.max_open_trades == 8
assert hyperopt.config['max_open_trades'] == 8

View File

@@ -66,52 +66,58 @@ def test_load_previous_results2(mocker, testdatadir, caplog) -> None:
@pytest.mark.parametrize("spaces, expected_results", [
(['buy'],
{'buy': True, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False,
'protection': False}),
'protection': False, 'trades': False}),
(['sell'],
{'buy': False, 'sell': True, 'roi': False, 'stoploss': False, 'trailing': False,
'protection': False}),
'protection': False, 'trades': False}),
(['roi'],
{'buy': False, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False,
'protection': False}),
'protection': False, 'trades': False}),
(['stoploss'],
{'buy': False, 'sell': False, 'roi': False, 'stoploss': True, 'trailing': False,
'protection': False}),
'protection': False, 'trades': False}),
(['trailing'],
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': True,
'protection': False}),
'protection': False, 'trades': False}),
(['buy', 'sell', 'roi', 'stoploss'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
'protection': False}),
'protection': False, 'trades': False}),
(['buy', 'sell', 'roi', 'stoploss', 'trailing'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
'protection': False}),
'protection': False, 'trades': False}),
(['buy', 'roi'],
{'buy': True, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False,
'protection': False}),
'protection': False, 'trades': False}),
(['all'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
'protection': True}),
'protection': True, 'trades': True}),
(['default'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
'protection': False}),
'protection': False, 'trades': False}),
(['default', 'trailing'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
'protection': False}),
'protection': False, 'trades': False}),
(['all', 'buy'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
'protection': True}),
'protection': True, 'trades': True}),
(['default', 'buy'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
'protection': False}),
'protection': False, 'trades': False}),
(['all'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
'protection': True}),
'protection': True, 'trades': True}),
(['protection'],
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False,
'protection': True}),
'protection': True, 'trades': False}),
(['trades'],
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False,
'protection': False, 'trades': True}),
(['default', 'trades'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
'protection': False, 'trades': True}),
])
def test_has_space(hyperopt_conf, spaces, expected_results):
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection']:
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection', 'trades']:
hyperopt_conf.update({'spaces': spaces})
assert HyperoptTools.has_space(hyperopt_conf, s) == expected_results[s]
@@ -193,6 +199,9 @@ def test_export_params(tmpdir):
"346": 0.08499,
"507": 0.049,
"1595": 0
},
"max_open_trades": {
"max_open_trades": 5
}
},
"params_not_optimized": {
@@ -219,6 +228,7 @@ def test_export_params(tmpdir):
assert "roi" in content["params"]
assert "stoploss" in content["params"]
assert "trailing" in content["params"]
assert "max_open_trades" in content["params"]
def test_try_export_params(default_conf, tmpdir, caplog, mocker):
@@ -297,6 +307,9 @@ def test_params_print(capsys):
"trailing_stop_positive_offset": 0.1,
"trailing_only_offset_is_reached": True
},
"max_open_trades": {
"max_open_trades": 5
}
}
HyperoptTools._params_pretty_print(params, 'buy', 'No header', non_optimized)
@@ -327,6 +340,13 @@ def test_params_print(capsys):
assert re.search('trailing_stop_positive_offset = 0.1 # value loaded.*\n', captured.out)
assert re.search('trailing_only_offset_is_reached = True # value loaded.*\n', captured.out)
HyperoptTools._params_pretty_print(
params, 'max_open_trades', "Max Open Trades:", non_optimized)
captured = capsys.readouterr()
assert re.search("# Max Open Trades:", captured.out)
assert re.search('max_open_trades = 5 # value loaded.*\n', captured.out)
def test_hyperopt_serializer():

View File

@@ -308,7 +308,7 @@ def test_generate_pair_metrics():
def test_generate_daily_stats(testdatadir):
filename = testdatadir / "backtest_results/backtest-result_new.json"
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
res = generate_daily_stats(bt_data)
assert isinstance(res, dict)
@@ -328,7 +328,7 @@ def test_generate_daily_stats(testdatadir):
def test_generate_trading_stats(testdatadir):
filename = testdatadir / "backtest_results/backtest-result_new.json"
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
res = generate_trading_stats(bt_data)
assert isinstance(res, dict)
@@ -444,7 +444,7 @@ def test_generate_edge_table():
def test_generate_periodic_breakdown_stats(testdatadir):
filename = testdatadir / "backtest_results/backtest-result_new.json"
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename).to_dict(orient='records')
res = generate_periodic_breakdown_stats(bt_data, 'day')
@@ -472,7 +472,7 @@ def test__get_resample_from_period():
def test_show_sorted_pairlist(testdatadir, default_conf, capsys):
filename = testdatadir / "backtest_results/backtest-result_new.json"
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_stats(filename)
default_conf['backtest_show_pair_list'] = True

View File

@@ -0,0 +1,412 @@
# pragma pylint: disable=missing-docstring, C0103
import logging
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from sqlalchemy import create_engine, text
from freqtrade.constants import DEFAULT_DB_PROD_URL
from freqtrade.enums import TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.persistence import Trade, init_db
from freqtrade.persistence.migrations import get_last_sequence_ids, set_sequence_ids
from freqtrade.persistence.models import PairLock
from tests.conftest import log_has
spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURES
def test_init_create_session(default_conf):
# Check if init create a session
init_db(default_conf['db_url'])
assert hasattr(Trade, '_session')
assert 'scoped_session' in type(Trade._session).__name__
def test_init_custom_db_url(default_conf, tmpdir):
# Update path to a value other than default, but still in-memory
filename = f"{tmpdir}/freqtrade2_test.sqlite"
assert not Path(filename).is_file()
default_conf.update({'db_url': f'sqlite:///{filename}'})
init_db(default_conf['db_url'])
assert Path(filename).is_file()
r = Trade._session.execute(text("PRAGMA journal_mode"))
assert r.first() == ('wal',)
def test_init_invalid_db_url():
# Update path to a value other than default, but still in-memory
with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
init_db('unknown:///some.url')
with pytest.raises(OperationalException, match=r'Bad db-url.*For in-memory database, pl.*'):
init_db('sqlite:///')
def test_init_prod_db(default_conf, mocker):
default_conf.update({'dry_run': False})
default_conf.update({'db_url': DEFAULT_DB_PROD_URL})
create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock())
init_db(default_conf['db_url'])
assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
def test_init_dryrun_db(default_conf, tmpdir):
filename = f"{tmpdir}/freqtrade2_prod.sqlite"
assert not Path(filename).is_file()
default_conf.update({
'dry_run': True,
'db_url': f'sqlite:///{filename}'
})
init_db(default_conf['db_url'])
assert Path(filename).is_file()
def test_migrate_new(mocker, default_conf, fee, caplog):
"""
Test Database migration (starting with new pairformat)
"""
caplog.set_level(logging.DEBUG)
amount = 103.223
# Always create all columns apart from the last!
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
id INTEGER NOT NULL,
exchange VARCHAR NOT NULL,
pair VARCHAR NOT NULL,
is_open BOOLEAN NOT NULL,
fee FLOAT NOT NULL,
open_rate FLOAT,
close_rate FLOAT,
close_profit FLOAT,
stake_amount FLOAT NOT NULL,
amount FLOAT,
open_date DATETIME NOT NULL,
close_date DATETIME,
open_order_id VARCHAR,
stop_loss FLOAT,
initial_stop_loss FLOAT,
max_rate FLOAT,
sell_reason VARCHAR,
strategy VARCHAR,
ticker_interval INTEGER,
stoploss_order_id VARCHAR,
PRIMARY KEY (id),
CHECK (is_open IN (0, 1))
);"""
create_table_order = """CREATE TABLE orders (
id INTEGER NOT NULL,
ft_trade_id INTEGER,
ft_order_side VARCHAR(25) NOT NULL,
ft_pair VARCHAR(25) NOT NULL,
ft_is_open BOOLEAN NOT NULL,
order_id VARCHAR(255) NOT NULL,
status VARCHAR(255),
symbol VARCHAR(25),
order_type VARCHAR(50),
side VARCHAR(25),
price FLOAT,
amount FLOAT,
filled FLOAT,
remaining FLOAT,
cost FLOAT,
order_date DATETIME,
order_filled_date DATETIME,
order_update_date DATETIME,
PRIMARY KEY (id)
);"""
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
open_rate, stake_amount, amount, open_date,
stop_loss, initial_stop_loss, max_rate, ticker_interval,
open_order_id, stoploss_order_id)
VALUES ('binance', 'ETC/BTC', 1, {fee},
0.00258580, {stake}, {amount},
'2019-11-28 12:44:24.000000',
0.0, 0.0, 0.0, '5m',
'buy_order', 'dry_stop_order_id222')
""".format(fee=fee.return_value,
stake=default_conf.get("stake_amount"),
amount=amount
)
insert_orders = f"""
insert into orders (
ft_trade_id,
ft_order_side,
ft_pair,
ft_is_open,
order_id,
status,
symbol,
order_type,
side,
price,
amount,
filled,
remaining,
cost)
values (
1,
'buy',
'ETC/BTC',
0,
'dry_buy_order',
'closed',
'ETC/BTC',
'limit',
'buy',
0.00258580,
{amount},
{amount},
0,
{amount * 0.00258580}
),
(
1,
'buy',
'ETC/BTC',
1,
'dry_buy_order22',
'canceled',
'ETC/BTC',
'limit',
'buy',
0.00258580,
{amount},
{amount},
0,
{amount * 0.00258580}
),
(
1,
'stoploss',
'ETC/BTC',
1,
'dry_stop_order_id11X',
'canceled',
'ETC/BTC',
'limit',
'sell',
0.00258580,
{amount},
{amount},
0,
{amount * 0.00258580}
),
(
1,
'stoploss',
'ETC/BTC',
1,
'dry_stop_order_id222',
'open',
'ETC/BTC',
'limit',
'sell',
0.00258580,
{amount},
{amount},
0,
{amount * 0.00258580}
)
"""
engine = create_engine('sqlite://')
mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
# Create table using the old format
with engine.begin() as connection:
connection.execute(text(create_table_old))
connection.execute(text(create_table_order))
connection.execute(text("create index ix_trades_is_open on trades(is_open)"))
connection.execute(text("create index ix_trades_pair on trades(pair)"))
connection.execute(text(insert_table_old))
connection.execute(text(insert_orders))
# fake previous backup
connection.execute(text("create table trades_bak as select * from trades"))
connection.execute(text("create table trades_bak1 as select * from trades"))
# Run init to test migration
init_db(default_conf['db_url'])
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
trade = Trade.query.filter(Trade.id == 1).first()
assert trade.fee_open == fee.return_value
assert trade.fee_close == fee.return_value
assert trade.open_rate_requested is None
assert trade.close_rate_requested is None
assert trade.is_open == 1
assert trade.amount == amount
assert trade.amount_requested == amount
assert trade.stake_amount == default_conf.get("stake_amount")
assert trade.pair == "ETC/BTC"
assert trade.exchange == "binance"
assert trade.max_rate == 0.0
assert trade.min_rate is None
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
assert trade.exit_reason is None
assert trade.strategy is None
assert trade.timeframe == '5m'
assert trade.stoploss_order_id == 'dry_stop_order_id222'
assert trade.stoploss_last_update is None
assert log_has("trying trades_bak1", caplog)
assert log_has("trying trades_bak2", caplog)
assert log_has("Running database migration for trades - backup: trades_bak2, orders_bak0",
caplog)
assert log_has("Database migration finished.", caplog)
assert pytest.approx(trade.open_trade_value) == trade._calc_open_trade_value(
trade.amount, trade.open_rate)
assert trade.close_profit_abs is None
assert trade.stake_amount == trade.max_stake_amount
orders = trade.orders
assert len(orders) == 4
assert orders[0].order_id == 'dry_buy_order'
assert orders[0].ft_order_side == 'buy'
assert orders[-1].order_id == 'dry_stop_order_id222'
assert orders[-1].ft_order_side == 'stoploss'
assert orders[-1].ft_is_open is True
assert orders[1].order_id == 'dry_buy_order22'
assert orders[1].ft_order_side == 'buy'
assert orders[1].ft_is_open is False
assert orders[2].order_id == 'dry_stop_order_id11X'
assert orders[2].ft_order_side == 'stoploss'
assert orders[2].ft_is_open is False
def test_migrate_too_old(mocker, default_conf, fee, caplog):
"""
Test Database migration (starting with new pairformat)
"""
caplog.set_level(logging.DEBUG)
amount = 103.223
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
id INTEGER NOT NULL,
exchange VARCHAR NOT NULL,
pair VARCHAR NOT NULL,
is_open BOOLEAN NOT NULL,
fee_open FLOAT NOT NULL,
fee_close FLOAT NOT NULL,
open_rate FLOAT,
close_rate FLOAT,
close_profit FLOAT,
stake_amount FLOAT NOT NULL,
amount FLOAT,
open_date DATETIME NOT NULL,
close_date DATETIME,
open_order_id VARCHAR,
PRIMARY KEY (id),
CHECK (is_open IN (0, 1))
);"""
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close,
open_rate, stake_amount, amount, open_date)
VALUES ('binance', 'ETC/BTC', 1, {fee}, {fee},
0.00258580, {stake}, {amount},
'2019-11-28 12:44:24.000000')
""".format(fee=fee.return_value,
stake=default_conf.get("stake_amount"),
amount=amount
)
engine = create_engine('sqlite://')
mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
# Create table using the old format
with engine.begin() as connection:
connection.execute(text(create_table_old))
connection.execute(text(insert_table_old))
# Run init to test migration
with pytest.raises(OperationalException, match=r'Your database seems to be very old'):
init_db(default_conf['db_url'])
def test_migrate_get_last_sequence_ids():
engine = MagicMock()
engine.begin = MagicMock()
engine.name = 'postgresql'
get_last_sequence_ids(engine, 'trades_bak', 'orders_bak')
assert engine.begin.call_count == 2
engine.reset_mock()
engine.begin.reset_mock()
engine.name = 'somethingelse'
get_last_sequence_ids(engine, 'trades_bak', 'orders_bak')
assert engine.begin.call_count == 0
def test_migrate_set_sequence_ids():
engine = MagicMock()
engine.begin = MagicMock()
engine.name = 'postgresql'
set_sequence_ids(engine, 22, 55, 5)
assert engine.begin.call_count == 1
engine.reset_mock()
engine.begin.reset_mock()
engine.name = 'somethingelse'
set_sequence_ids(engine, 22, 55, 6)
assert engine.begin.call_count == 0
def test_migrate_pairlocks(mocker, default_conf, fee, caplog):
"""
Test Database migration (starting with new pairformat)
"""
caplog.set_level(logging.DEBUG)
# Always create all columns apart from the last!
create_table_old = """CREATE TABLE pairlocks (
id INTEGER NOT NULL,
pair VARCHAR(25) NOT NULL,
reason VARCHAR(255),
lock_time DATETIME NOT NULL,
lock_end_time DATETIME NOT NULL,
active BOOLEAN NOT NULL,
PRIMARY KEY (id)
)
"""
create_index1 = "CREATE INDEX ix_pairlocks_pair ON pairlocks (pair)"
create_index2 = "CREATE INDEX ix_pairlocks_lock_end_time ON pairlocks (lock_end_time)"
create_index3 = "CREATE INDEX ix_pairlocks_active ON pairlocks (active)"
insert_table_old = """INSERT INTO pairlocks (
id, pair, reason, lock_time, lock_end_time, active)
VALUES (1, 'ETH/BTC', 'Auto lock', '2021-07-12 18:41:03', '2021-07-11 18:45:00', 1)
"""
insert_table_old2 = """INSERT INTO pairlocks (
id, pair, reason, lock_time, lock_end_time, active)
VALUES (2, '*', 'Lock all', '2021-07-12 18:41:03', '2021-07-12 19:00:00', 1)
"""
engine = create_engine('sqlite://')
mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
# Create table using the old format
with engine.begin() as connection:
connection.execute(text(create_table_old))
connection.execute(text(insert_table_old))
connection.execute(text(insert_table_old2))
connection.execute(text(create_index1))
connection.execute(text(create_index2))
connection.execute(text(create_index3))
init_db(default_conf['db_url'])
assert len(PairLock.query.all()) == 2
assert len(PairLock.query.filter(PairLock.pair == '*').all()) == 1
pairlocks = PairLock.query.filter(PairLock.pair == 'ETH/BTC').all()
assert len(pairlocks) == 1
pairlocks[0].pair == 'ETH/BTC'
pairlocks[0].side == '*'

View File

@@ -1,78 +1,20 @@
# pragma pylint: disable=missing-docstring, C0103
import logging
from datetime import datetime, timedelta, timezone
from pathlib import Path
from types import FunctionType
from unittest.mock import MagicMock
import arrow
import pytest
from sqlalchemy import create_engine, text
from freqtrade.constants import DATETIME_PRINT_FORMAT, DEFAULT_DB_PROD_URL
from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.enums import TradingMode
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exceptions import DependencyException
from freqtrade.persistence import LocalTrade, Order, Trade, init_db
from freqtrade.persistence.migrations import get_last_sequence_ids, set_sequence_ids
from freqtrade.persistence.models import PairLock
from tests.conftest import create_mock_trades, create_mock_trades_with_leverage, log_has, log_has_re
spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURES
def test_init_create_session(default_conf):
# Check if init create a session
init_db(default_conf['db_url'])
assert hasattr(Trade, '_session')
assert 'scoped_session' in type(Trade._session).__name__
def test_init_custom_db_url(default_conf, tmpdir):
# Update path to a value other than default, but still in-memory
filename = f"{tmpdir}/freqtrade2_test.sqlite"
assert not Path(filename).is_file()
default_conf.update({'db_url': f'sqlite:///{filename}'})
init_db(default_conf['db_url'])
assert Path(filename).is_file()
r = Trade._session.execute(text("PRAGMA journal_mode"))
assert r.first() == ('wal',)
def test_init_invalid_db_url():
# Update path to a value other than default, but still in-memory
with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
init_db('unknown:///some.url')
with pytest.raises(OperationalException, match=r'Bad db-url.*For in-memory database, pl.*'):
init_db('sqlite:///')
def test_init_prod_db(default_conf, mocker):
default_conf.update({'dry_run': False})
default_conf.update({'db_url': DEFAULT_DB_PROD_URL})
create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock())
init_db(default_conf['db_url'])
assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
def test_init_dryrun_db(default_conf, tmpdir):
filename = f"{tmpdir}/freqtrade2_prod.sqlite"
assert not Path(filename).is_file()
default_conf.update({
'dry_run': True,
'db_url': f'sqlite:///{filename}'
})
init_db(default_conf['db_url'])
assert Path(filename).is_file()
@pytest.mark.parametrize('is_short', [False, True])
@pytest.mark.usefixtures("init_persistence")
def test_enter_exit_side(fee, is_short):
@@ -316,8 +258,7 @@ def test_interest(fee, exchange, is_short, lev, minutes, rate, interest,
(True, 3.0, 30.0, margin),
])
@pytest.mark.usefixtures("init_persistence")
def test_borrowed(limit_buy_order_usdt, limit_sell_order_usdt, fee,
caplog, is_short, lev, borrowed, trading_mode):
def test_borrowed(fee, is_short, lev, borrowed, trading_mode):
"""
10 minute limit trade on Binance/Kraken at 1x, 3x leverage
fee: 0.25% quote
@@ -1204,347 +1145,6 @@ def test_calc_profit(
trade.open_rate)) == round(profit_ratio, 8)
def test_migrate_new(mocker, default_conf, fee, caplog):
"""
Test Database migration (starting with new pairformat)
"""
caplog.set_level(logging.DEBUG)
amount = 103.223
# Always create all columns apart from the last!
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
id INTEGER NOT NULL,
exchange VARCHAR NOT NULL,
pair VARCHAR NOT NULL,
is_open BOOLEAN NOT NULL,
fee FLOAT NOT NULL,
open_rate FLOAT,
close_rate FLOAT,
close_profit FLOAT,
stake_amount FLOAT NOT NULL,
amount FLOAT,
open_date DATETIME NOT NULL,
close_date DATETIME,
open_order_id VARCHAR,
stop_loss FLOAT,
initial_stop_loss FLOAT,
max_rate FLOAT,
sell_reason VARCHAR,
strategy VARCHAR,
ticker_interval INTEGER,
stoploss_order_id VARCHAR,
PRIMARY KEY (id),
CHECK (is_open IN (0, 1))
);"""
create_table_order = """CREATE TABLE orders (
id INTEGER NOT NULL,
ft_trade_id INTEGER,
ft_order_side VARCHAR(25) NOT NULL,
ft_pair VARCHAR(25) NOT NULL,
ft_is_open BOOLEAN NOT NULL,
order_id VARCHAR(255) NOT NULL,
status VARCHAR(255),
symbol VARCHAR(25),
order_type VARCHAR(50),
side VARCHAR(25),
price FLOAT,
amount FLOAT,
filled FLOAT,
remaining FLOAT,
cost FLOAT,
order_date DATETIME,
order_filled_date DATETIME,
order_update_date DATETIME,
PRIMARY KEY (id)
);"""
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
open_rate, stake_amount, amount, open_date,
stop_loss, initial_stop_loss, max_rate, ticker_interval,
open_order_id, stoploss_order_id)
VALUES ('binance', 'ETC/BTC', 1, {fee},
0.00258580, {stake}, {amount},
'2019-11-28 12:44:24.000000',
0.0, 0.0, 0.0, '5m',
'buy_order', 'dry_stop_order_id222')
""".format(fee=fee.return_value,
stake=default_conf.get("stake_amount"),
amount=amount
)
insert_orders = f"""
insert into orders (
ft_trade_id,
ft_order_side,
ft_pair,
ft_is_open,
order_id,
status,
symbol,
order_type,
side,
price,
amount,
filled,
remaining,
cost)
values (
1,
'buy',
'ETC/BTC',
0,
'dry_buy_order',
'closed',
'ETC/BTC',
'limit',
'buy',
0.00258580,
{amount},
{amount},
0,
{amount * 0.00258580}
),
(
1,
'buy',
'ETC/BTC',
1,
'dry_buy_order22',
'canceled',
'ETC/BTC',
'limit',
'buy',
0.00258580,
{amount},
{amount},
0,
{amount * 0.00258580}
),
(
1,
'stoploss',
'ETC/BTC',
1,
'dry_stop_order_id11X',
'canceled',
'ETC/BTC',
'limit',
'sell',
0.00258580,
{amount},
{amount},
0,
{amount * 0.00258580}
),
(
1,
'stoploss',
'ETC/BTC',
1,
'dry_stop_order_id222',
'open',
'ETC/BTC',
'limit',
'sell',
0.00258580,
{amount},
{amount},
0,
{amount * 0.00258580}
)
"""
engine = create_engine('sqlite://')
mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
# Create table using the old format
with engine.begin() as connection:
connection.execute(text(create_table_old))
connection.execute(text(create_table_order))
connection.execute(text("create index ix_trades_is_open on trades(is_open)"))
connection.execute(text("create index ix_trades_pair on trades(pair)"))
connection.execute(text(insert_table_old))
connection.execute(text(insert_orders))
# fake previous backup
connection.execute(text("create table trades_bak as select * from trades"))
connection.execute(text("create table trades_bak1 as select * from trades"))
# Run init to test migration
init_db(default_conf['db_url'])
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
trade = Trade.query.filter(Trade.id == 1).first()
assert trade.fee_open == fee.return_value
assert trade.fee_close == fee.return_value
assert trade.open_rate_requested is None
assert trade.close_rate_requested is None
assert trade.is_open == 1
assert trade.amount == amount
assert trade.amount_requested == amount
assert trade.stake_amount == default_conf.get("stake_amount")
assert trade.pair == "ETC/BTC"
assert trade.exchange == "binance"
assert trade.max_rate == 0.0
assert trade.min_rate is None
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
assert trade.exit_reason is None
assert trade.strategy is None
assert trade.timeframe == '5m'
assert trade.stoploss_order_id == 'dry_stop_order_id222'
assert trade.stoploss_last_update is None
assert log_has("trying trades_bak1", caplog)
assert log_has("trying trades_bak2", caplog)
assert log_has("Running database migration for trades - backup: trades_bak2, orders_bak0",
caplog)
assert log_has("Database migration finished.", caplog)
assert pytest.approx(trade.open_trade_value) == trade._calc_open_trade_value(
trade.amount, trade.open_rate)
assert trade.close_profit_abs is None
orders = trade.orders
assert len(orders) == 4
assert orders[0].order_id == 'dry_buy_order'
assert orders[0].ft_order_side == 'buy'
assert orders[-1].order_id == 'dry_stop_order_id222'
assert orders[-1].ft_order_side == 'stoploss'
assert orders[-1].ft_is_open is True
assert orders[1].order_id == 'dry_buy_order22'
assert orders[1].ft_order_side == 'buy'
assert orders[1].ft_is_open is False
assert orders[2].order_id == 'dry_stop_order_id11X'
assert orders[2].ft_order_side == 'stoploss'
assert orders[2].ft_is_open is False
def test_migrate_too_old(mocker, default_conf, fee, caplog):
"""
Test Database migration (starting with new pairformat)
"""
caplog.set_level(logging.DEBUG)
amount = 103.223
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
id INTEGER NOT NULL,
exchange VARCHAR NOT NULL,
pair VARCHAR NOT NULL,
is_open BOOLEAN NOT NULL,
fee_open FLOAT NOT NULL,
fee_close FLOAT NOT NULL,
open_rate FLOAT,
close_rate FLOAT,
close_profit FLOAT,
stake_amount FLOAT NOT NULL,
amount FLOAT,
open_date DATETIME NOT NULL,
close_date DATETIME,
open_order_id VARCHAR,
PRIMARY KEY (id),
CHECK (is_open IN (0, 1))
);"""
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close,
open_rate, stake_amount, amount, open_date)
VALUES ('binance', 'ETC/BTC', 1, {fee}, {fee},
0.00258580, {stake}, {amount},
'2019-11-28 12:44:24.000000')
""".format(fee=fee.return_value,
stake=default_conf.get("stake_amount"),
amount=amount
)
engine = create_engine('sqlite://')
mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
# Create table using the old format
with engine.begin() as connection:
connection.execute(text(create_table_old))
connection.execute(text(insert_table_old))
# Run init to test migration
with pytest.raises(OperationalException, match=r'Your database seems to be very old'):
init_db(default_conf['db_url'])
def test_migrate_get_last_sequence_ids():
engine = MagicMock()
engine.begin = MagicMock()
engine.name = 'postgresql'
get_last_sequence_ids(engine, 'trades_bak', 'orders_bak')
assert engine.begin.call_count == 2
engine.reset_mock()
engine.begin.reset_mock()
engine.name = 'somethingelse'
get_last_sequence_ids(engine, 'trades_bak', 'orders_bak')
assert engine.begin.call_count == 0
def test_migrate_set_sequence_ids():
engine = MagicMock()
engine.begin = MagicMock()
engine.name = 'postgresql'
set_sequence_ids(engine, 22, 55, 5)
assert engine.begin.call_count == 1
engine.reset_mock()
engine.begin.reset_mock()
engine.name = 'somethingelse'
set_sequence_ids(engine, 22, 55, 6)
assert engine.begin.call_count == 0
def test_migrate_pairlocks(mocker, default_conf, fee, caplog):
"""
Test Database migration (starting with new pairformat)
"""
caplog.set_level(logging.DEBUG)
# Always create all columns apart from the last!
create_table_old = """CREATE TABLE pairlocks (
id INTEGER NOT NULL,
pair VARCHAR(25) NOT NULL,
reason VARCHAR(255),
lock_time DATETIME NOT NULL,
lock_end_time DATETIME NOT NULL,
active BOOLEAN NOT NULL,
PRIMARY KEY (id)
)
"""
create_index1 = "CREATE INDEX ix_pairlocks_pair ON pairlocks (pair)"
create_index2 = "CREATE INDEX ix_pairlocks_lock_end_time ON pairlocks (lock_end_time)"
create_index3 = "CREATE INDEX ix_pairlocks_active ON pairlocks (active)"
insert_table_old = """INSERT INTO pairlocks (
id, pair, reason, lock_time, lock_end_time, active)
VALUES (1, 'ETH/BTC', 'Auto lock', '2021-07-12 18:41:03', '2021-07-11 18:45:00', 1)
"""
insert_table_old2 = """INSERT INTO pairlocks (
id, pair, reason, lock_time, lock_end_time, active)
VALUES (2, '*', 'Lock all', '2021-07-12 18:41:03', '2021-07-12 19:00:00', 1)
"""
engine = create_engine('sqlite://')
mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
# Create table using the old format
with engine.begin() as connection:
connection.execute(text(create_table_old))
connection.execute(text(insert_table_old))
connection.execute(text(insert_table_old2))
connection.execute(text(create_index1))
connection.execute(text(create_index2))
connection.execute(text(create_index3))
init_db(default_conf['db_url'])
assert len(PairLock.query.all()) == 2
assert len(PairLock.query.filter(PairLock.pair == '*').all()) == 1
pairlocks = PairLock.query.filter(PairLock.pair == 'ETH/BTC').all()
assert len(pairlocks) == 1
pairlocks[0].pair == 'ETH/BTC'
pairlocks[0].side == '*'
def test_adjust_stop_loss(fee):
trade = Trade(
pair='ADA/USDT',
@@ -1758,6 +1358,7 @@ def test_to_json(fee):
'amount': 123.0,
'amount_requested': 123.0,
'stake_amount': 0.001,
'max_stake_amount': None,
'trade_duration': None,
'trade_duration_s': None,
'realized_profit': 0.0,
@@ -1767,7 +1368,6 @@ def test_to_json(fee):
'profit_ratio': None,
'profit_pct': None,
'profit_abs': None,
'sell_reason': None,
'exit_reason': None,
'exit_order_status': None,
'stop_loss_abs': None,
@@ -1782,7 +1382,6 @@ def test_to_json(fee):
'min_rate': None,
'max_rate': None,
'strategy': None,
'buy_tag': None,
'enter_tag': None,
'timeframe': None,
'exchange': 'binance',
@@ -1826,6 +1425,7 @@ def test_to_json(fee):
'amount': 100.0,
'amount_requested': 101.0,
'stake_amount': 0.001,
'max_stake_amount': None,
'trade_duration': 60,
'trade_duration_s': 3600,
'stop_loss_abs': None,
@@ -1857,11 +1457,9 @@ def test_to_json(fee):
'open_order_id': None,
'open_rate_requested': None,
'open_trade_value': 12.33075,
'sell_reason': None,
'exit_reason': None,
'exit_order_status': None,
'strategy': None,
'buy_tag': 'buys_signal_001',
'enter_tag': 'buys_signal_001',
'timeframe': None,
'exchange': 'binance',
@@ -2270,13 +1868,18 @@ def test_get_exit_order_count(fee, is_short):
@pytest.mark.usefixtures("init_persistence")
def test_update_order_from_ccxt(caplog):
def test_update_order_from_ccxt(caplog, time_machine):
start = datetime(2023, 1, 1, 4, tzinfo=timezone.utc)
time_machine.move_to(start, tick=False)
# Most basic order return (only has orderid)
o = Order.parse_from_ccxt_object({'id': '1234'}, 'ADA/USDT', 'buy')
o = Order.parse_from_ccxt_object({'id': '1234'}, 'ADA/USDT', 'buy', 20.01, 1234.6)
assert isinstance(o, Order)
assert o.ft_pair == 'ADA/USDT'
assert o.ft_order_side == 'buy'
assert o.order_id == '1234'
assert o.ft_price == 1234.6
assert o.ft_amount == 20.01
assert o.ft_is_open
ccxt_order = {
'id': '1234',
@@ -2290,13 +1893,15 @@ def test_update_order_from_ccxt(caplog):
'status': 'open',
'timestamp': 1599394315123
}
o = Order.parse_from_ccxt_object(ccxt_order, 'ADA/USDT', 'buy')
o = Order.parse_from_ccxt_object(ccxt_order, 'ADA/USDT', 'buy', 20.01, 1234.6)
assert isinstance(o, Order)
assert o.ft_pair == 'ADA/USDT'
assert o.ft_order_side == 'buy'
assert o.order_id == '1234'
assert o.order_type == 'limit'
assert o.price == 1234.5
assert o.ft_price == 1234.6
assert o.ft_amount == 20.01
assert o.filled == 9
assert o.remaining == 11
assert o.order_date is not None
@@ -2315,7 +1920,9 @@ def test_update_order_from_ccxt(caplog):
assert o.filled == 20.0
assert o.remaining == 0.0
assert not o.ft_is_open
assert o.order_filled_date is not None
assert o.order_filled_date == start
# Move time
time_machine.move_to(start + timedelta(hours=1), tick=False)
ccxt_order.update({'id': 'somethingelse'})
with pytest.raises(DependencyException, match=r"Order-id's don't match"):
@@ -2328,6 +1935,12 @@ def test_update_order_from_ccxt(caplog):
# Call regular update - shouldn't fail.
Order.update_orders([o], {'id': '1234'})
assert o.order_filled_date == start
# Fill order again - shouldn't update filled date
ccxt_order.update({'id': '1234'})
Order.update_orders([o], ccxt_order)
assert o.order_filled_date == start
@pytest.mark.usefixtures("init_persistence")
@@ -2941,6 +2554,8 @@ def test_recalc_trade_from_orders_dca(data) -> None:
ft_pair=trade.pair,
order_id=f"order_{order[0]}_{idx}",
ft_is_open=False,
ft_amount=amount,
ft_price=price,
status="closed",
symbol=trade.pair,
order_type="market",

View File

@@ -22,6 +22,11 @@ from tests.conftest import (create_mock_trades_usdt, get_patched_exchange, get_p
log_has, log_has_re, num_log_has)
# Exclude RemotePairList from tests.
# It has a mandatory parameter, and requires special handling, which happens in test_remotepairlist.
TESTABLE_PAIRLISTS = [p for p in AVAILABLE_PAIRLISTS if p not in ['RemotePairList']]
@pytest.fixture(scope="function")
def whitelist_conf(default_conf):
default_conf['stake_currency'] = 'BTC'
@@ -824,7 +829,7 @@ def test_pair_whitelist_not_supported_Spread(mocker, default_conf, tickers) -> N
get_patched_freqtradebot(mocker, default_conf)
@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS)
@pytest.mark.parametrize("pairlist", TESTABLE_PAIRLISTS)
def test_pairlist_class(mocker, whitelist_conf, markets, pairlist):
whitelist_conf['pairlists'][0]['method'] = pairlist
mocker.patch.multiple('freqtrade.exchange.Exchange',
@@ -839,7 +844,7 @@ def test_pairlist_class(mocker, whitelist_conf, markets, pairlist):
assert isinstance(freqtrade.pairlists.blacklist, list)
@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS)
@pytest.mark.parametrize("pairlist", TESTABLE_PAIRLISTS)
@pytest.mark.parametrize("whitelist,log_message", [
(['ETH/BTC', 'TKN/BTC'], ""),
# TRX/ETH not in markets
@@ -872,7 +877,7 @@ def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist
assert log_message in caplog.text
@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS)
@pytest.mark.parametrize("pairlist", TESTABLE_PAIRLISTS)
def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, pairlist, tickers):
whitelist_conf['pairlists'][0]['method'] = pairlist

View File

@@ -39,6 +39,8 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool,
order_id=f'{pair}-{trade.entry_side}-{trade.open_date}',
ft_is_open=False,
ft_pair=pair,
ft_amount=trade.amount,
ft_price=trade.open_rate,
amount=trade.amount,
filled=trade.amount,
remaining=0,
@@ -49,16 +51,19 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool,
side=trade.entry_side,
))
if not is_open:
close_price = open_rate * (2 - profit_rate if is_short else profit_rate)
trade.orders.append(Order(
ft_order_side=trade.exit_side,
order_id=f'{pair}-{trade.exit_side}-{trade.close_date}',
ft_is_open=False,
ft_pair=pair,
ft_amount=trade.amount,
ft_price=trade.open_rate,
amount=trade.amount,
filled=trade.amount,
remaining=0,
price=open_rate * (2 - profit_rate if is_short else profit_rate),
average=open_rate * (2 - profit_rate if is_short else profit_rate),
price=close_price,
average=close_price,
status="closed",
order_type="market",
side=trade.exit_side,
@@ -66,7 +71,7 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool,
trade.recalc_open_trade_value()
if not is_open:
trade.close(open_rate * (2 - profit_rate if is_short else profit_rate))
trade.close(close_price)
trade.exit_reason = exit_reason
Trade.query.session.add(trade)

View File

@@ -0,0 +1,185 @@
import json
from unittest.mock import MagicMock
import pytest
import requests
from freqtrade.exceptions import OperationalException
from freqtrade.plugins.pairlist.RemotePairList import RemotePairList
from freqtrade.plugins.pairlistmanager import PairListManager
from tests.conftest import get_patched_exchange, get_patched_freqtradebot, log_has
@pytest.fixture(scope="function")
def rpl_config(default_conf):
default_conf['stake_currency'] = 'USDT'
default_conf['exchange']['pair_whitelist'] = [
'ETH/USDT',
'BTC/USDT',
]
default_conf['exchange']['pair_blacklist'] = [
'BLK/USDT'
]
return default_conf
def test_gen_pairlist_with_local_file(mocker, rpl_config):
mock_file = MagicMock()
mock_file.read.return_value = '{"pairs": ["TKN/USDT","ETH/USDT","NANO/USDT"]}'
mocker.patch('freqtrade.plugins.pairlist.RemotePairList.open', return_value=mock_file)
mock_file_path = mocker.patch('freqtrade.plugins.pairlist.RemotePairList.Path')
mock_file_path.exists.return_value = True
jsonparse = json.loads(mock_file.read.return_value)
mocker.patch('freqtrade.plugins.pairlist.RemotePairList.json.load', return_value=jsonparse)
rpl_config['pairlists'] = [
{
"method": "RemotePairList",
'number_assets': 2,
'refresh_period': 1800,
'keep_pairlist_on_failure': True,
'pairlist_url': 'file:///pairlist.json',
'bearer_token': '',
'read_timeout': 60
}
]
exchange = get_patched_exchange(mocker, rpl_config)
pairlistmanager = PairListManager(exchange, rpl_config)
remote_pairlist = RemotePairList(exchange, pairlistmanager, rpl_config,
rpl_config['pairlists'][0], 0)
result = remote_pairlist.gen_pairlist([])
assert result == ['TKN/USDT', 'ETH/USDT']
def test_fetch_pairlist_mock_response_html(mocker, rpl_config):
mock_response = MagicMock()
mock_response.headers = {'content-type': 'text/html'}
rpl_config['pairlists'] = [
{
"method": "RemotePairList",
"pairlist_url": "http://example.com/pairlist",
"number_assets": 10,
"read_timeout": 10,
"keep_pairlist_on_failure": True,
}
]
exchange = get_patched_exchange(mocker, rpl_config)
pairlistmanager = PairListManager(exchange, rpl_config)
mocker.patch("freqtrade.plugins.pairlist.RemotePairList.requests.get",
return_value=mock_response)
remote_pairlist = RemotePairList(exchange, pairlistmanager, rpl_config,
rpl_config['pairlists'][0], 0)
with pytest.raises(OperationalException, match='RemotePairList is not of type JSON, abort.'):
remote_pairlist.fetch_pairlist()
def test_fetch_pairlist_timeout_keep_last_pairlist(mocker, rpl_config, caplog):
rpl_config['pairlists'] = [
{
"method": "RemotePairList",
"pairlist_url": "http://example.com/pairlist",
"number_assets": 10,
"read_timeout": 10,
"keep_pairlist_on_failure": True,
}
]
exchange = get_patched_exchange(mocker, rpl_config)
pairlistmanager = PairListManager(exchange, rpl_config)
mocker.patch("freqtrade.plugins.pairlist.RemotePairList.requests.get",
side_effect=requests.exceptions.RequestException)
remote_pairlist = RemotePairList(exchange, pairlistmanager, rpl_config,
rpl_config['pairlists'][0], 0)
remote_pairlist._last_pairlist = ["BTC/USDT", "ETH/USDT", "LTC/USDT"]
pairs, time_elapsed = remote_pairlist.fetch_pairlist()
assert log_has(f"Was not able to fetch pairlist from: {remote_pairlist._pairlist_url}", caplog)
assert log_has("Keeping last fetched pairlist", caplog)
assert pairs == ["BTC/USDT", "ETH/USDT", "LTC/USDT"]
def test_remote_pairlist_init_no_pairlist_url(mocker, rpl_config):
rpl_config['pairlists'] = [
{
"method": "RemotePairList",
"number_assets": 10,
"keep_pairlist_on_failure": True,
}
]
get_patched_exchange(mocker, rpl_config)
with pytest.raises(OperationalException, match=r'`pairlist_url` not specified.'
r' Please check your configuration for "pairlist.config.pairlist_url"'):
get_patched_freqtradebot(mocker, rpl_config)
def test_remote_pairlist_init_no_number_assets(mocker, rpl_config):
rpl_config['pairlists'] = [
{
"method": "RemotePairList",
"pairlist_url": "http://example.com/pairlist",
"keep_pairlist_on_failure": True,
}
]
get_patched_exchange(mocker, rpl_config)
with pytest.raises(OperationalException, match=r'`number_assets` not specified. '
'Please check your configuration for "pairlist.config.number_assets"'):
get_patched_freqtradebot(mocker, rpl_config)
def test_fetch_pairlist_mock_response_valid(mocker, rpl_config):
rpl_config['pairlists'] = [
{
"method": "RemotePairList",
"pairlist_url": "http://example.com/pairlist",
"number_assets": 10,
"refresh_period": 10,
"read_timeout": 10,
"keep_pairlist_on_failure": True,
}
]
mock_response = MagicMock()
mock_response.json.return_value = {
"pairs": ["ETH/USDT", "XRP/USDT", "LTC/USDT", "EOS/USDT"],
"refresh_period": 60
}
mock_response.headers = {
"content-type": "application/json"
}
mock_response.elapsed.total_seconds.return_value = 0.4
mocker.patch("freqtrade.plugins.pairlist.RemotePairList.requests.get",
return_value=mock_response)
exchange = get_patched_exchange(mocker, rpl_config)
pairlistmanager = PairListManager(exchange, rpl_config)
remote_pairlist = RemotePairList(exchange, pairlistmanager, rpl_config,
rpl_config['pairlists'][0], 0)
pairs, time_elapsed = remote_pairlist.fetch_pairlist()
assert pairs == ["ETH/USDT", "XRP/USDT", "LTC/USDT", "EOS/USDT"]
assert time_elapsed == 0.4
assert remote_pairlist._refresh_period == 60

View File

@@ -46,13 +46,11 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'open_rate_requested': ANY,
'open_trade_value': 0.0010025,
'close_rate_requested': ANY,
'sell_reason': ANY,
'exit_reason': ANY,
'exit_order_status': ANY,
'min_rate': ANY,
'max_rate': ANY,
'strategy': ANY,
'buy_tag': ANY,
'enter_tag': ANY,
'timeframe': 5,
'open_order_id': ANY,
@@ -64,6 +62,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'amount': 91.07468123,
'amount_requested': 91.07468124,
'stake_amount': 0.001,
'max_stake_amount': ANY,
'trade_duration': None,
'trade_duration_s': None,
'close_profit': None,

View File

@@ -1,8 +1,6 @@
"""
Unit test file for rpc/api_server.py
"""
import json
import logging
import time
from datetime import datetime, timedelta, timezone
@@ -68,22 +66,23 @@ def botclient(default_conf, mocker):
ApiServer.shutdown()
def client_post(client, url, data={}):
def client_post(client: TestClient, url, data={}):
return client.post(url,
content=data,
json=data,
headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS),
'Origin': 'http://example.com',
'content-type': 'application/json'
})
def client_get(client, url):
def client_get(client: TestClient, url):
# Add fake Origin to ensure CORS kicks in
return client.get(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS),
'Origin': 'http://example.com'})
def client_delete(client, url):
def client_delete(client: TestClient, url):
# Add fake Origin to ensure CORS kicks in
return client.delete(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS),
'Origin': 'http://example.com'})
@@ -561,7 +560,7 @@ def test_api_locks(botclient):
assert rc.json()['lock_count'] == 1
rc = client_post(client, f"{BASE_URI}/locks/delete",
data='{"pair": "XRP/BTC"}')
data={"pair": "XRP/BTC"})
assert_response(rc)
assert rc.json()['lock_count'] == 0
@@ -707,6 +706,46 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short):
assert_response(rc, 502)
@pytest.mark.parametrize('is_short', [True, False])
def test_api_delete_open_order(botclient, mocker, fee, markets, ticker, is_short):
ftbot, client = botclient
patch_get_signal(ftbot, enter_long=not is_short, enter_short=is_short)
stoploss_mock = MagicMock()
cancel_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
fetch_ticker=ticker,
cancel_order=cancel_mock,
cancel_stoploss_order=stoploss_mock,
)
rc = client_delete(client, f"{BASE_URI}/trades/10/open-order")
assert_response(rc, 502)
assert 'Invalid trade_id.' in rc.json()['error']
create_mock_trades(fee, is_short=is_short)
Trade.commit()
rc = client_delete(client, f"{BASE_URI}/trades/5/open-order")
assert_response(rc, 502)
assert 'No open order for trade_id' in rc.json()['error']
trade = Trade.get_trades([Trade.id == 6]).first()
mocker.patch('freqtrade.exchange.Exchange.fetch_order',
side_effect=ExchangeError)
rc = client_delete(client, f"{BASE_URI}/trades/6/open-order")
assert_response(rc, 502)
assert 'Order not found.' in rc.json()['error']
trade = Trade.get_trades([Trade.id == 6]).first()
mocker.patch('freqtrade.exchange.Exchange.fetch_order',
return_value=trade.orders[-1].to_ccxt_object())
rc = client_delete(client, f"{BASE_URI}/trades/6/open-order")
assert_response(rc)
assert cancel_mock.call_count == 1
def test_api_logs(botclient):
ftbot, client = botclient
rc = client_get(client, f"{BASE_URI}/logs")
@@ -985,6 +1024,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
'base_currency': 'ETH',
'quote_currency': 'BTC',
'stake_amount': 0.001,
'max_stake_amount': ANY,
'stop_loss_abs': ANY,
'stop_loss_pct': ANY,
'stop_loss_ratio': ANY,
@@ -1014,11 +1054,9 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
'open_order_id': open_order_id,
'open_rate_requested': ANY,
'open_trade_value': open_trade_value,
'sell_reason': None,
'exit_reason': None,
'exit_order_status': None,
'strategy': CURRENT_TEST_STRATEGY,
'buy_tag': None,
'enter_tag': None,
'timeframe': 5,
'exchange': 'binance',
@@ -1063,7 +1101,7 @@ def test_api_blacklist(botclient, mocker):
# Add ETH/BTC to blacklist
rc = client_post(client, f"{BASE_URI}/blacklist",
data='{"blacklist": ["ETH/BTC"]}')
data={"blacklist": ["ETH/BTC"]})
assert_response(rc)
assert rc.json() == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"],
"blacklist_expanded": ["ETH/BTC"],
@@ -1073,7 +1111,7 @@ def test_api_blacklist(botclient, mocker):
}
rc = client_post(client, f"{BASE_URI}/blacklist",
data='{"blacklist": ["XRP/.*"]}')
data={"blacklist": ["XRP/.*"]})
assert_response(rc)
assert rc.json() == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC", "XRP/.*"],
"blacklist_expanded": ["ETH/BTC", "XRP/BTC", "XRP/USDT"],
@@ -1135,7 +1173,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
ftbot, client = botclient
rc = client_post(client, f"{BASE_URI}/{endpoint}",
data='{"pair": "ETH/BTC"}')
data={"pair": "ETH/BTC"})
assert_response(rc, 502)
assert rc.json() == {"error": f"Error querying /api/v1/{endpoint}: Force_entry not enabled."}
@@ -1145,7 +1183,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
fbuy_mock = MagicMock(return_value=None)
mocker.patch("freqtrade.rpc.RPC._rpc_force_entry", fbuy_mock)
rc = client_post(client, f"{BASE_URI}/{endpoint}",
data='{"pair": "ETH/BTC"}')
data={"pair": "ETH/BTC"})
assert_response(rc)
assert rc.json() == {"status": "Error entering long trade for pair ETH/BTC."}
@@ -1172,7 +1210,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
mocker.patch("freqtrade.rpc.RPC._rpc_force_entry", fbuy_mock)
rc = client_post(client, f"{BASE_URI}/{endpoint}",
data='{"pair": "ETH/BTC"}')
data={"pair": "ETH/BTC"})
assert_response(rc)
assert rc.json() == {
'amount': 1.0,
@@ -1188,6 +1226,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
'base_currency': 'ETH',
'quote_currency': 'BTC',
'stake_amount': 1,
'max_stake_amount': ANY,
'stop_loss_abs': None,
'stop_loss_pct': None,
'stop_loss_ratio': None,
@@ -1218,11 +1257,9 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
'open_order_id': '123456',
'open_rate_requested': None,
'open_trade_value': 0.24605460,
'sell_reason': None,
'exit_reason': None,
'exit_order_status': None,
'strategy': CURRENT_TEST_STRATEGY,
'buy_tag': None,
'enter_tag': None,
'timeframe': 5,
'exchange': 'binance',
@@ -1248,7 +1285,7 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets):
patch_get_signal(ftbot)
rc = client_post(client, f"{BASE_URI}/forceexit",
data='{"tradeid": "1"}')
data={"tradeid": "1"})
assert_response(rc, 502)
assert rc.json() == {"error": "Error querying /api/v1/forceexit: invalid argument"}
Trade.query.session.rollback()
@@ -1257,7 +1294,7 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets):
trade = Trade.get_trades([Trade.id == 5]).first()
assert pytest.approx(trade.amount) == 123
rc = client_post(client, f"{BASE_URI}/forceexit",
data='{"tradeid": "5", "ordertype": "market", "amount": 23}')
data={"tradeid": "5", "ordertype": "market", "amount": 23})
assert_response(rc)
assert rc.json() == {'result': 'Created sell order for trade 5.'}
Trade.query.session.rollback()
@@ -1267,7 +1304,7 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets):
assert trade.is_open is True
rc = client_post(client, f"{BASE_URI}/forceexit",
data='{"tradeid": "5"}')
data={"tradeid": "5"})
assert_response(rc)
assert rc.json() == {'result': 'Created sell order for trade 5.'}
Trade.query.session.rollback()
@@ -1420,7 +1457,7 @@ def test_api_pair_history(botclient, ohlcv_history):
"No data for UNITTEST/BTC, 5m in 20200111-20200112 found.")
def test_api_plot_config(botclient):
def test_api_plot_config(botclient, mocker):
ftbot, client = botclient
rc = client_get(client, f"{BASE_URI}/plot_config")
@@ -1444,6 +1481,21 @@ def test_api_plot_config(botclient):
assert isinstance(rc.json()['main_plot'], dict)
assert isinstance(rc.json()['subplots'], dict)
rc = client_get(client, f"{BASE_URI}/plot_config?strategy=freqai_test_classifier")
assert_response(rc)
res = rc.json()
assert 'target_roi' in res['subplots']
assert 'do_predict' in res['subplots']
rc = client_get(client, f"{BASE_URI}/plot_config?strategy=HyperoptableStrategy")
assert_response(rc)
assert rc.json()['subplots'] == {}
mocker.patch('freqtrade.rpc.api_server.api_v1.get_rpc_optional', return_value=None)
rc = client_get(client, f"{BASE_URI}/plot_config")
assert_response(rc)
def test_api_strategies(botclient, tmpdir):
ftbot, client = botclient
@@ -1488,6 +1540,44 @@ def test_api_strategy(botclient):
assert_response(rc, 500)
def test_api_freqaimodels(botclient, tmpdir, mocker):
ftbot, client = botclient
ftbot.config['user_data_dir'] = Path(tmpdir)
mocker.patch(
"freqtrade.resolvers.freqaimodel_resolver.FreqaiModelResolver.search_all_objects",
return_value=[
{'name': 'LightGBMClassifier'},
{'name': 'LightGBMClassifierMultiTarget'},
{'name': 'LightGBMRegressor'},
{'name': 'LightGBMRegressorMultiTarget'},
{'name': 'ReinforcementLearner'},
{'name': 'ReinforcementLearner_multiproc'},
{'name': 'XGBoostClassifier'},
{'name': 'XGBoostRFClassifier'},
{'name': 'XGBoostRFRegressor'},
{'name': 'XGBoostRegressor'},
{'name': 'XGBoostRegressorMultiTarget'},
])
rc = client_get(client, f"{BASE_URI}/freqaimodels")
assert_response(rc)
assert rc.json() == {'freqaimodels': [
'LightGBMClassifier',
'LightGBMClassifierMultiTarget',
'LightGBMRegressor',
'LightGBMRegressorMultiTarget',
'ReinforcementLearner',
'ReinforcementLearner_multiproc',
'XGBoostClassifier',
'XGBoostRFClassifier',
'XGBoostRFRegressor',
'XGBoostRegressor',
'XGBoostRegressorMultiTarget'
]}
def test_list_available_pairs(botclient):
ftbot, client = botclient
@@ -1518,13 +1608,13 @@ def test_list_available_pairs(botclient):
client, f"{BASE_URI}/available_pairs?timeframe=1h")
assert_response(rc)
assert rc.json()['length'] == 1
assert rc.json()['pairs'] == ['XRP/USDT']
assert rc.json()['pairs'] == ['XRP/USDT:USDT']
rc = client_get(
client, f"{BASE_URI}/available_pairs?timeframe=1h&candletype=mark")
assert_response(rc)
assert rc.json()['length'] == 2
assert rc.json()['pairs'] == ['UNITTEST/USDT', 'XRP/USDT']
assert rc.json()['pairs'] == ['UNITTEST/USDT:USDT', 'XRP/USDT:USDT']
assert len(rc.json()['pair_interval']) == 2
@@ -1580,7 +1670,7 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
"dry_run_wallet": 1000,
"enable_protections": False
}
rc = client_post(client, f"{BASE_URI}/backtest", data=json.dumps(data))
rc = client_post(client, f"{BASE_URI}/backtest", data=data)
assert_response(rc)
result = rc.json()
@@ -1631,7 +1721,7 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
assert result['status'] == 'running'
# Post to backtest that's still running
rc = client_post(client, f"{BASE_URI}/backtest", data=json.dumps(data))
rc = client_post(client, f"{BASE_URI}/backtest", data=data)
assert_response(rc, 502)
result = rc.json()
assert 'Bot Background task already running' in result['error']
@@ -1639,7 +1729,7 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
ApiServer._bgtask_running = False
# Rerun backtest (should get previous result)
rc = client_post(client, f"{BASE_URI}/backtest", data=json.dumps(data))
rc = client_post(client, f"{BASE_URI}/backtest", data=data)
assert_response(rc)
result = rc.json()
assert log_has_re('Reusing result of previous backtest.*', caplog)
@@ -1648,7 +1738,7 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest_one_strategy',
side_effect=DependencyException())
rc = client_post(client, f"{BASE_URI}/backtest", data=json.dumps(data))
rc = client_post(client, f"{BASE_URI}/backtest", data=data)
assert log_has("Backtesting caused an error: ", caplog)
# Delete backtesting to avoid leakage since the backtest-object may stick around.
@@ -1662,7 +1752,7 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
# Disallow base64 strategies
data['strategy'] = "xx:cHJpbnQoImhlbGxvIHdvcmxkIik="
rc = client_post(client, f"{BASE_URI}/backtest", data=json.dumps(data))
rc = client_post(client, f"{BASE_URI}/backtest", data=data)
assert_response(rc, 500)
@@ -1671,7 +1761,7 @@ def test_api_backtest_history(botclient, mocker, testdatadir):
mocker.patch('freqtrade.data.btanalysis._get_backtest_files',
return_value=[
testdatadir / 'backtest_results/backtest-result_multistrat.json',
testdatadir / 'backtest_results/backtest-result_new.json'
testdatadir / 'backtest_results/backtest-result.json'
])
rc = client_get(client, f"{BASE_URI}/backtest/history")
@@ -1730,7 +1820,7 @@ def test_api_ws_subscribe(botclient, mocker):
assert sub_mock.call_count == 1
def test_api_ws_requests(botclient, mocker, caplog):
def test_api_ws_requests(botclient, caplog):
caplog.set_level(logging.DEBUG)
ftbot, client = botclient

View File

@@ -83,6 +83,7 @@ def test_emc_init(patched_emc):
def test_emc_handle_producer_message(patched_emc, caplog, ohlcv_history):
test_producer = {"name": "test", "url": "ws://test", "ws_token": "test"}
producer_name = test_producer['name']
invalid_msg = r"Invalid message .+"
caplog.set_level(logging.DEBUG)
@@ -94,7 +95,7 @@ def test_emc_handle_producer_message(patched_emc, caplog, ohlcv_history):
assert log_has(
f"Consumed message from `{producer_name}` of type `RPCMessageType.WHITELIST`", caplog)
# Test handle analyzed_df message
# Test handle analyzed_df single candle message
df_message = {
"type": "analyzed_df",
"data": {
@@ -106,8 +107,7 @@ def test_emc_handle_producer_message(patched_emc, caplog, ohlcv_history):
patched_emc.handle_producer_message(test_producer, df_message)
assert log_has(f"Received message of type `analyzed_df` from `{producer_name}`", caplog)
assert log_has(
f"Consumed message from `{producer_name}` of type `RPCMessageType.ANALYZED_DF`", caplog)
assert log_has_re(r"Holes in data or no existing df,.+", caplog)
# Test unhandled message
unhandled_message = {"type": "status", "data": "RUNNING"}
@@ -120,7 +120,8 @@ def test_emc_handle_producer_message(patched_emc, caplog, ohlcv_history):
malformed_message = {"type": "whitelist", "data": {"pair": "BTC/USDT"}}
patched_emc.handle_producer_message(test_producer, malformed_message)
assert log_has_re(r"Invalid message .+", caplog)
assert log_has_re(invalid_msg, caplog)
caplog.clear()
malformed_message = {
"type": "analyzed_df",
@@ -133,13 +134,30 @@ def test_emc_handle_producer_message(patched_emc, caplog, ohlcv_history):
patched_emc.handle_producer_message(test_producer, malformed_message)
assert log_has(f"Received message of type `analyzed_df` from `{producer_name}`", caplog)
assert log_has_re(r"Invalid message .+", caplog)
assert log_has_re(invalid_msg, caplog)
caplog.clear()
# Empty dataframe
malformed_message = {
"type": "analyzed_df",
"data": {
"key": ("BTC/USDT", "5m", "spot"),
"df": ohlcv_history.loc[ohlcv_history['open'] < 0],
"la": datetime.now(timezone.utc)
}
}
patched_emc.handle_producer_message(test_producer, malformed_message)
assert log_has(f"Received message of type `analyzed_df` from `{producer_name}`", caplog)
assert not log_has_re(invalid_msg, caplog)
assert log_has_re(r"Received Empty Dataframe for.+", caplog)
caplog.clear()
malformed_message = {"some": "stuff"}
patched_emc.handle_producer_message(test_producer, malformed_message)
assert log_has_re(r"Invalid message .+", caplog)
assert log_has_re(invalid_msg, caplog)
caplog.clear()
caplog.clear()
malformed_message = {"type": "whitelist", "data": None}
@@ -183,7 +201,7 @@ async def test_emc_create_connection_success(default_conf, caplog, mocker):
async with websockets.serve(eat, _TEST_WS_HOST, _TEST_WS_PORT):
await emc._create_connection(test_producer, lock)
assert log_has_re(r"Producer connection success.+", caplog)
assert log_has_re(r"Connected to channel.+", caplog)
finally:
emc.shutdown()
@@ -212,7 +230,8 @@ async def test_emc_create_connection_invalid_url(default_conf, caplog, mocker, h
dp = DataProvider(default_conf, None, None, None)
# Handle start explicitly to avoid messing with threading in tests
mocker.patch("freqtrade.rpc.external_message_consumer.ExternalMessageConsumer.start",)
mocker.patch("freqtrade.rpc.external_message_consumer.ExternalMessageConsumer.start")
mocker.patch("freqtrade.rpc.api_server.ws.channel.create_channel")
emc = ExternalMessageConsumer(default_conf, dp)
try:
@@ -248,7 +267,7 @@ async def test_emc_create_connection_error(default_conf, caplog, mocker):
emc = ExternalMessageConsumer(default_conf, dp)
try:
await asyncio.sleep(0.01)
await asyncio.sleep(0.05)
assert log_has("Unexpected error has occurred:", caplog)
finally:
emc.shutdown()
@@ -390,7 +409,9 @@ async def test_emc_receive_messages_timeout(default_conf, caplog, mocker):
try:
change_running(emc)
loop.call_soon(functools.partial(change_running, emc=emc))
await emc._receive_messages(TestChannel(), test_producer, lock)
with pytest.raises(asyncio.TimeoutError):
await emc._receive_messages(TestChannel(), test_producer, lock)
assert log_has_re(r"Ping error.+", caplog)
finally:

View File

@@ -99,7 +99,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
"['balance'], ['start'], ['stop'], "
"['forcesell', 'forceexit', 'fx'], ['forcebuy', 'forcelong'], ['forceshort'], "
"['trades'], ['delete'], ['performance'], "
"['trades'], ['delete'], ['coo', 'cancel_open_order'], ['performance'], "
"['buys', 'entries'], ['sells', 'exits'], ['mix_tags'], "
"['stats'], ['daily'], ['weekly'], ['monthly'], "
"['count'], ['locks'], ['unlock', 'delete_locks'], "
@@ -253,6 +253,8 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
ft_order_side='buy',
ft_pair=trade.pair,
ft_is_open=False,
ft_amount=trade.amount,
ft_price=trade.open_rate,
status="closed",
symbol=trade.pair,
order_type="market",
@@ -1676,6 +1678,40 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short):
assert "Please make sure to take care of this asset" in msg_mock.call_args_list[0][0][0]
@pytest.mark.parametrize('is_short', [True, False])
def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short, ticker):
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
)
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
context = MagicMock()
context.args = []
telegram._cancel_open_order(update=update, context=context)
assert "Trade-id not set." in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
create_mock_trades(fee, is_short=is_short)
context = MagicMock()
context.args = [5]
telegram._cancel_open_order(update=update, context=context)
assert "No open order for trade_id" in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
trade = Trade.get_trades([Trade.id == 6]).first()
mocker.patch('freqtrade.exchange.Exchange.fetch_order',
return_value=trade.orders[-1].to_ccxt_object())
context = MagicMock()
context.args = [6]
telegram._cancel_open_order(update=update, context=context)
assert msg_mock.call_count == 1
assert "Open order canceled." in msg_mock.call_args_list[0][0][0]
def test_help_handle(default_conf, update, mocker) -> None:
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)

View File

@@ -1,11 +1,11 @@
import logging
from functools import reduce
from typing import Dict
import pandas as pd
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import IStrategy, merge_informative_pair
from freqtrade.strategy import IStrategy
logger = logging.getLogger(__name__)
@@ -22,52 +22,40 @@ class freqai_rl_test_strat(IStrategy):
process_only_new_candles = True
stoploss = -0.05
use_exit_signal = True
startup_candle_count: int = 30
startup_candle_count: int = 300
can_short = False
def populate_any_indicators(
self, pair, df, tf, informative=None, set_generalized_indicators=False
):
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
metadata: Dict, **kwargs):
if informative is None:
informative = self.dp.get_pair_dataframe(pair, tf)
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
# first loop is automatically duplicating indicators for time periods
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
return dataframe
t = int(t)
informative[f"%-{pair}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
# The following columns are necessary for RL models.
informative[f"%-{pair}raw_close"] = informative["close"]
informative[f"%-{pair}raw_open"] = informative["open"]
informative[f"%-{pair}raw_high"] = informative["high"]
informative[f"%-{pair}raw_low"] = informative["low"]
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-raw_volume"] = dataframe["volume"]
indicators = [col for col in informative if col.startswith("%")]
# This loop duplicates and shifts all indicators to add a sense of recency to data
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
if n == 0:
continue
informative_shift = informative[indicators].shift(n)
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
informative = pd.concat((informative, informative_shift), axis=1)
return dataframe
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
skip_columns = [
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
]
df = df.drop(columns=skip_columns)
def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
# Add generalized indicators here (because in live, it will call this
# function to populate indicators during training). Notice how we ensure not to
# add them multiple times
if set_generalized_indicators:
# For RL, there are no direct targets to set. This is filler (neutral)
# until the agent sends an action.
df["&-action"] = 0
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
return df
dataframe["%-raw_close"] = dataframe["close"]
dataframe["%-raw_open"] = dataframe["open"]
dataframe["%-raw_high"] = dataframe["high"]
dataframe["%-raw_low"] = dataframe["low"]
return dataframe
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
dataframe["&-action"] = 0
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

View File

@@ -1,12 +1,12 @@
import logging
from functools import reduce
from typing import Dict
import numpy as np
import pandas as pd
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, merge_informative_pair
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy
logger = logging.getLogger(__name__)
@@ -57,55 +57,36 @@ class freqai_test_classifier(IStrategy):
informative_pairs.append((pair, tf))
return informative_pairs
def populate_any_indicators(
self, pair, df, tf, informative=None, set_generalized_indicators=False
):
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
metadata: Dict, **kwargs):
coin = pair.split('/')[0]
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
if informative is None:
informative = self.dp.get_pair_dataframe(pair, tf)
return dataframe
# first loop is automatically duplicating indicators for time periods
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
t = int(t)
informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t)
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-raw_volume"] = dataframe["volume"]
dataframe["%-raw_price"] = dataframe["close"]
informative[f"%-{coin}pct-change"] = informative["close"].pct_change()
informative[f"%-{coin}raw_volume"] = informative["volume"]
informative[f"%-{coin}raw_price"] = informative["close"]
return dataframe
indicators = [col for col in informative if col.startswith("%")]
# This loop duplicates and shifts all indicators to add a sense of recency to data
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
if n == 0:
continue
informative_shift = informative[indicators].shift(n)
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
informative = pd.concat((informative, informative_shift), axis=1)
def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
skip_columns = [
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
]
df = df.drop(columns=skip_columns)
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
# Add generalized indicators here (because in live, it will call this
# function to populate indicators during training). Notice how we ensure not to
# add them multiple times
if set_generalized_indicators:
df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7
df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25
return dataframe
# user adds targets here by prepending them with &- (see convention below)
# If user wishes to use multiple targets, a multioutput prediction model
# needs to be used such as templates/CatboostPredictionMultiModel.py
df['&s-up_or_down'] = np.where(df["close"].shift(-100) > df["close"], 'up', 'down')
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
return df
dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) >
dataframe["close"], 'up', 'down')
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

View File

@@ -1,12 +1,12 @@
import logging
from functools import reduce
from typing import Dict
import numpy as np
import pandas as pd
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, merge_informative_pair
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy
logger = logging.getLogger(__name__)
@@ -44,59 +44,39 @@ class freqai_test_multimodel_classifier_strat(IStrategy):
)
max_roi_time_long = IntParameter(0, 800, default=400, space="sell", optimize=False, load=True)
def populate_any_indicators(
self, pair, df, tf, informative=None, set_generalized_indicators=False
):
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
metadata: Dict, **kwargs):
coin = pair.split('/')[0]
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
if informative is None:
informative = self.dp.get_pair_dataframe(pair, tf)
return dataframe
# first loop is automatically duplicating indicators for time periods
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
t = int(t)
informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t)
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-raw_volume"] = dataframe["volume"]
dataframe["%-raw_price"] = dataframe["close"]
informative[f"%-{coin}pct-change"] = informative["close"].pct_change()
informative[f"%-{coin}raw_volume"] = informative["volume"]
informative[f"%-{coin}raw_price"] = informative["close"]
return dataframe
indicators = [col for col in informative if col.startswith("%")]
# This loop duplicates and shifts all indicators to add a sense of recency to data
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
if n == 0:
continue
informative_shift = informative[indicators].shift(n)
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
informative = pd.concat((informative, informative_shift), axis=1)
def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
skip_columns = [
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
]
df = df.drop(columns=skip_columns)
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
# Add generalized indicators here (because in live, it will call this
# function to populate indicators during training). Notice how we ensure not to
# add them multiple times
if set_generalized_indicators:
df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7
df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25
return dataframe
# user adds targets here by prepending them with &- (see convention below)
# If user wishes to use multiple targets, a multioutput prediction model
# needs to be used such as templates/CatboostPredictionMultiModel.py
df['&s-up_or_down'] = np.where(df["close"].shift(-50) >
df["close"], 'up', 'down')
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
df['&s-up_or_down2'] = np.where(df["close"].shift(-50) >
df["close"], 'up2', 'down2')
dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-50) >
dataframe["close"], 'up', 'down')
return df
dataframe['&s-up_or_down2'] = np.where(dataframe["close"].shift(-50) >
dataframe["close"], 'up2', 'down2')
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

View File

@@ -1,11 +1,11 @@
import logging
from functools import reduce
from typing import Dict
import pandas as pd
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, merge_informative_pair
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy
logger = logging.getLogger(__name__)
@@ -43,74 +43,54 @@ class freqai_test_multimodel_strat(IStrategy):
)
max_roi_time_long = IntParameter(0, 800, default=400, space="sell", optimize=False, load=True)
def populate_any_indicators(
self, pair, df, tf, informative=None, set_generalized_indicators=False
):
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
metadata: Dict, **kwargs):
coin = pair.split('/')[0]
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
if informative is None:
informative = self.dp.get_pair_dataframe(pair, tf)
return dataframe
# first loop is automatically duplicating indicators for time periods
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
t = int(t)
informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t)
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-raw_volume"] = dataframe["volume"]
dataframe["%-raw_price"] = dataframe["close"]
informative[f"%-{coin}pct-change"] = informative["close"].pct_change()
informative[f"%-{coin}raw_volume"] = informative["volume"]
informative[f"%-{coin}raw_price"] = informative["close"]
return dataframe
indicators = [col for col in informative if col.startswith("%")]
# This loop duplicates and shifts all indicators to add a sense of recency to data
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
if n == 0:
continue
informative_shift = informative[indicators].shift(n)
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
informative = pd.concat((informative, informative_shift), axis=1)
def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
skip_columns = [
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
]
df = df.drop(columns=skip_columns)
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
# Add generalized indicators here (because in live, it will call this
# function to populate indicators during training). Notice how we ensure not to
# add them multiple times
if set_generalized_indicators:
df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7
df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25
return dataframe
# user adds targets here by prepending them with &- (see convention below)
# If user wishes to use multiple targets, a multioutput prediction model
# needs to be used such as templates/CatboostPredictionMultiModel.py
df["&-s_close"] = (
df["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.mean()
/ df["close"]
- 1
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
dataframe["&-s_close"] = (
dataframe["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.mean()
/ dataframe["close"]
- 1
)
df["&-s_range"] = (
df["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.max()
-
df["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.min()
)
dataframe["&-s_range"] = (
dataframe["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.max()
-
dataframe["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.min()
)
return df
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

View File

@@ -1,11 +1,11 @@
import logging
from functools import reduce
from typing import Dict
import pandas as pd
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, merge_informative_pair
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy
logger = logging.getLogger(__name__)
@@ -43,62 +43,42 @@ class freqai_test_strat(IStrategy):
)
max_roi_time_long = IntParameter(0, 800, default=400, space="sell", optimize=False, load=True)
def populate_any_indicators(
self, pair, df, tf, informative=None, set_generalized_indicators=False
):
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
metadata: Dict, **kwargs):
coin = pair.split('/')[0]
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
if informative is None:
informative = self.dp.get_pair_dataframe(pair, tf)
return dataframe
# first loop is automatically duplicating indicators for time periods
for t in self.freqai_info["feature_parameters"]["indicator_periods_candles"]:
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
t = int(t)
informative[f"%-{coin}rsi-period_{t}"] = ta.RSI(informative, timeperiod=t)
informative[f"%-{coin}mfi-period_{t}"] = ta.MFI(informative, timeperiod=t)
informative[f"%-{coin}adx-period_{t}"] = ta.ADX(informative, window=t)
dataframe["%-pct-change"] = dataframe["close"].pct_change()
dataframe["%-raw_volume"] = dataframe["volume"]
dataframe["%-raw_price"] = dataframe["close"]
informative[f"%-{coin}pct-change"] = informative["close"].pct_change()
informative[f"%-{coin}raw_volume"] = informative["volume"]
informative[f"%-{coin}raw_price"] = informative["close"]
return dataframe
indicators = [col for col in informative if col.startswith("%")]
# This loop duplicates and shifts all indicators to add a sense of recency to data
for n in range(self.freqai_info["feature_parameters"]["include_shifted_candles"] + 1):
if n == 0:
continue
informative_shift = informative[indicators].shift(n)
informative_shift = informative_shift.add_suffix("_shift-" + str(n))
informative = pd.concat((informative, informative_shift), axis=1)
def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
df = merge_informative_pair(df, informative, self.config["timeframe"], tf, ffill=True)
skip_columns = [
(s + "_" + tf) for s in ["date", "open", "high", "low", "close", "volume"]
]
df = df.drop(columns=skip_columns)
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
# Add generalized indicators here (because in live, it will call this
# function to populate indicators during training). Notice how we ensure not to
# add them multiple times
if set_generalized_indicators:
df["%-day_of_week"] = (df["date"].dt.dayofweek + 1) / 7
df["%-hour_of_day"] = (df["date"].dt.hour + 1) / 25
return dataframe
# user adds targets here by prepending them with &- (see convention below)
# If user wishes to use multiple targets, a multioutput prediction model
# needs to be used such as templates/CatboostPredictionMultiModel.py
df["&-s_close"] = (
df["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.mean()
/ df["close"]
- 1
def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
dataframe["&-s_close"] = (
dataframe["close"]
.shift(-self.freqai_info["feature_parameters"]["label_period_candles"])
.rolling(self.freqai_info["feature_parameters"]["label_period_candles"])
.mean()
/ dataframe["close"]
- 1
)
return df
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

View File

@@ -34,6 +34,11 @@ class HyperoptableStrategy(StrategyTestV3):
protection_enabled = BooleanParameter(default=True)
protection_cooldown_lookback = IntParameter([0, 50], default=30)
# Invalid plot config ...
plot_config = {
"main_plot": {},
}
@property
def protections(self):
prot = []

View File

@@ -30,6 +30,9 @@ class StrategyTestV3(IStrategy):
"0": 0.04
}
# Optimal max_open_trades for the strategy
max_open_trades = -1
# Optimal stoploss designed for the strategy
stoploss = -0.10

View File

@@ -452,8 +452,8 @@ def test_min_roi_reached3(default_conf, fee) -> None:
(0.05, 0.9, ExitType.NONE, None, False, True, 0.09, 0.9, ExitType.NONE,
lambda **kwargs: None),
])
def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, liq, trailing, custom,
profit2, adjusted2, expected2, custom_stop) -> None:
def test_ft_stoploss_reached(default_conf, fee, profit, adjusted, expected, liq, trailing, custom,
profit2, adjusted2, expected2, custom_stop) -> None:
strategy = StrategyResolver.load_strategy(default_conf)
trade = Trade(
@@ -477,9 +477,9 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, liq, t
now = arrow.utcnow().datetime
current_rate = trade.open_rate * (1 + profit)
sl_flag = strategy.stop_loss_reached(current_rate=current_rate, trade=trade,
current_time=now, current_profit=profit,
force_stoploss=0, high=None)
sl_flag = strategy.ft_stoploss_reached(current_rate=current_rate, trade=trade,
current_time=now, current_profit=profit,
force_stoploss=0, high=None)
assert isinstance(sl_flag, ExitCheckTuple)
assert sl_flag.exit_type == expected
if expected == ExitType.NONE:
@@ -489,9 +489,9 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, liq, t
assert round(trade.stop_loss, 2) == adjusted
current_rate2 = trade.open_rate * (1 + profit2)
sl_flag = strategy.stop_loss_reached(current_rate=current_rate2, trade=trade,
current_time=now, current_profit=profit2,
force_stoploss=0, high=None)
sl_flag = strategy.ft_stoploss_reached(current_rate=current_rate2, trade=trade,
current_time=now, current_profit=profit2,
force_stoploss=0, high=None)
assert sl_flag.exit_type == expected2
if expected2 == ExitType.NONE:
assert sl_flag.exit_flag is False
@@ -579,7 +579,7 @@ def test_should_sell(default_conf, fee) -> None:
assert res == [ExitCheckTuple(exit_type=ExitType.ROI)]
strategy.min_roi_reached = MagicMock(return_value=True)
strategy.stop_loss_reached = MagicMock(
strategy.ft_stoploss_reached = MagicMock(
return_value=ExitCheckTuple(exit_type=ExitType.STOP_LOSS))
res = strategy.should_exit(trade, 1, now,
@@ -603,7 +603,7 @@ def test_should_sell(default_conf, fee) -> None:
ExitCheckTuple(exit_type=ExitType.ROI),
]
strategy.stop_loss_reached = MagicMock(
strategy.ft_stoploss_reached = MagicMock(
return_value=ExitCheckTuple(exit_type=ExitType.TRAILING_STOP_LOSS))
# Regular exit signal
res = strategy.should_exit(trade, 1, now,

View File

@@ -6,6 +6,7 @@ from pathlib import Path
import pytest
from pandas import DataFrame
from freqtrade.configuration import Configuration
from freqtrade.exceptions import OperationalException
from freqtrade.resolvers import StrategyResolver
from freqtrade.strategy.interface import IStrategy
@@ -175,6 +176,18 @@ def test_strategy_override_stoploss(caplog, default_conf):
assert log_has("Override strategy 'stoploss' with value in config file: -0.5.", caplog)
def test_strategy_override_max_open_trades(caplog, default_conf):
caplog.set_level(logging.INFO)
default_conf.update({
'strategy': CURRENT_TEST_STRATEGY,
'max_open_trades': 7
})
strategy = StrategyResolver.load_strategy(default_conf)
assert strategy.max_open_trades == 7
assert log_has("Override strategy 'max_open_trades' with value in config file: 7.", caplog)
def test_strategy_override_trailing_stop(caplog, default_conf):
caplog.set_level(logging.INFO)
default_conf.update({
@@ -349,6 +362,38 @@ def test_strategy_override_use_exit_profit_only(caplog, default_conf):
assert log_has("Override strategy 'exit_profit_only' with value in config file: True.", caplog)
def test_strategy_max_open_trades_infinity_from_strategy(caplog, default_conf):
caplog.set_level(logging.INFO)
default_conf.update({
'strategy': CURRENT_TEST_STRATEGY,
})
del default_conf['max_open_trades']
strategy = StrategyResolver.load_strategy(default_conf)
# this test assumes -1 set to 'max_open_trades' in CURRENT_TEST_STRATEGY
assert strategy.max_open_trades == float('inf')
assert default_conf['max_open_trades'] == float('inf')
def test_strategy_max_open_trades_infinity_from_config(caplog, default_conf, mocker):
caplog.set_level(logging.INFO)
default_conf.update({
'strategy': CURRENT_TEST_STRATEGY,
'max_open_trades': -1,
'exchange': 'binance'
})
configuration = Configuration(args=default_conf)
parsed_config = configuration.get_config()
assert parsed_config['max_open_trades'] == float('inf')
strategy = StrategyResolver.load_strategy(parsed_config)
assert strategy.max_open_trades == float('inf')
@ pytest.mark.filterwarnings("ignore:deprecated")
def test_missing_implements(default_conf, caplog):
@@ -438,3 +483,19 @@ def test_strategy_interface_versioning(dataframe_1m, default_conf):
assert isinstance(exitdf, DataFrame)
assert 'sell' not in exitdf
assert 'exit_long' in exitdf
def test_strategy_ft_load_params_from_file(mocker, default_conf):
default_conf.update({'strategy': 'StrategyTestV2'})
del default_conf['max_open_trades']
mocker.patch('freqtrade.strategy.hyper.HyperStrategyMixin.load_params_from_file',
return_value={
'params': {
'max_open_trades': {
'max_open_trades': -1
}
}
})
strategy = StrategyResolver.load_strategy(default_conf)
assert strategy.max_open_trades == float('inf')
assert strategy.config['max_open_trades'] == float('inf')

58
tests/test_binance_mig.py Normal file
View File

@@ -0,0 +1,58 @@
import shutil
from pathlib import Path
import pytest
from freqtrade.persistence import Trade
from freqtrade.util.binance_mig import migrate_binance_futures_data, migrate_binance_futures_names
from tests.conftest import create_mock_trades_usdt, log_has
def test_binance_mig_data_conversion(default_conf_usdt, tmpdir, testdatadir):
# call doing nothing (spot mode)
migrate_binance_futures_data(default_conf_usdt)
default_conf_usdt['trading_mode'] = 'futures'
pair_old = 'XRP_USDT'
pair_unified = 'XRP_USDT_USDT'
futures_src = testdatadir / 'futures'
futures_dst = tmpdir / 'futures'
futures_dst.mkdir()
files = [
'-1h-mark.json',
'-1h-futures.json',
'-8h-funding_rate.json',
'-8h-mark.json',
]
# Copy files to tmpdir and rename to old naming
for file in files:
fn_after = futures_dst / f'{pair_old}{file}'
shutil.copy(futures_src / f'{pair_unified}{file}', fn_after)
default_conf_usdt['datadir'] = Path(tmpdir)
# Migrate files to unified namings
migrate_binance_futures_data(default_conf_usdt)
for file in files:
fn_after = futures_dst / f'{pair_unified}{file}'
assert fn_after.exists()
@pytest.mark.usefixtures("init_persistence")
def test_binance_mig_db_conversion(default_conf_usdt, fee, caplog):
# Does nothing in spot mode
migrate_binance_futures_names(default_conf_usdt)
create_mock_trades_usdt(fee, None)
for t in Trade.get_trades():
t.trading_mode = 'FUTURES'
t.exchange = 'binance'
Trade.commit()
default_conf_usdt['trading_mode'] = 'futures'
migrate_binance_futures_names(default_conf_usdt)
assert log_has('Migrating binance futures pairs in database.', caplog)

View File

@@ -58,6 +58,7 @@ def test_load_config_incorrect_stake_amount(default_conf) -> None:
def test_load_config_file(default_conf, mocker, caplog) -> None:
del default_conf['user_data_dir']
default_conf['datadir'] = str(default_conf['datadir'])
file_mock = mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
@@ -69,6 +70,7 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
def test_load_config_file_error(default_conf, mocker, caplog) -> None:
del default_conf['user_data_dir']
default_conf['datadir'] = str(default_conf['datadir'])
filedata = json.dumps(default_conf).replace(
'"stake_amount": 0.001,', '"stake_amount": .001,')
mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open(read_data=filedata))
@@ -80,6 +82,7 @@ def test_load_config_file_error(default_conf, mocker, caplog) -> None:
def test_load_config_file_error_range(default_conf, mocker, caplog) -> None:
del default_conf['user_data_dir']
default_conf['datadir'] = str(default_conf['datadir'])
filedata = json.dumps(default_conf).replace(
'"stake_amount": 0.001,', '"stake_amount": .001,')
mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata))
@@ -238,6 +241,7 @@ def test_print_config(default_conf, mocker, caplog) -> None:
conf1 = deepcopy(default_conf)
# Delete non-json elements from default_conf
del conf1['user_data_dir']
conf1['datadir'] = str(conf1['datadir'])
config_files = [conf1]
configsmock = MagicMock(side_effect=config_files)

View File

@@ -737,20 +737,22 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker)
@pytest.mark.parametrize("is_short,trading_mode,exchange_name,margin_mode,liq_buffer,liq_price", [
(False, 'spot', 'binance', None, 0.0, None),
(True, 'spot', 'binance', None, 0.0, None),
(False, 'spot', 'gateio', None, 0.0, None),
(True, 'spot', 'gateio', None, 0.0, None),
(False, 'spot', 'gate', None, 0.0, None),
(True, 'spot', 'gate', None, 0.0, None),
(False, 'spot', 'okx', None, 0.0, None),
(True, 'spot', 'okx', None, 0.0, None),
(True, 'futures', 'binance', 'isolated', 0.0, 11.88151815181518),
(False, 'futures', 'binance', 'isolated', 0.0, 8.080471380471382),
(True, 'futures', 'gateio', 'isolated', 0.0, 11.87413417771621),
(False, 'futures', 'gateio', 'isolated', 0.0, 8.085708510208207),
(True, 'futures', 'gate', 'isolated', 0.0, 11.87413417771621),
(False, 'futures', 'gate', 'isolated', 0.0, 8.085708510208207),
(True, 'futures', 'binance', 'isolated', 0.05, 11.7874422442244),
(False, 'futures', 'binance', 'isolated', 0.05, 8.17644781144781),
(True, 'futures', 'gateio', 'isolated', 0.05, 11.7804274688304),
(False, 'futures', 'gateio', 'isolated', 0.05, 8.181423084697796),
(True, 'futures', 'gate', 'isolated', 0.05, 11.7804274688304),
(False, 'futures', 'gate', 'isolated', 0.05, 8.181423084697796),
(True, 'futures', 'okx', 'isolated', 0.0, 11.87413417771621),
(False, 'futures', 'okx', 'isolated', 0.0, 8.085708510208207),
(True, 'futures', 'bybit', 'isolated', 0.0, 11.9),
(False, 'futures', 'bybit', 'isolated', 0.0, 8.1),
])
def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
limit_order_open, is_short, trading_mode,
@@ -766,11 +768,11 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position))
((2 + 0.01) - (1 * 1 * 10)) / ((1 * 0.01) - (1 * 1)) = 8.070707070707071
exchange_name = gateio/okx, is_short = true
exchange_name = gate/okx, is_short = true
(open_rate + (wallet_balance / position)) / (1 + (mm_ratio + taker_fee_rate))
(10 + (2 / 1)) / (1 + (0.01 + 0.0006)) = 11.87413417771621
exchange_name = gateio/okx, is_short = false
exchange_name = gate/okx, is_short = false
(open_rate - (wallet_balance / position)) / (1 - (mm_ratio + taker_fee_rate))
(10 - (2 / 1)) / (1 - (0.01 + 0.0006)) = 8.085708510208207
"""
@@ -783,7 +785,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
default_conf_usdt['exchange']['name'] = exchange_name
if margin_mode:
default_conf_usdt['margin_mode'] = margin_mode
mocker.patch('freqtrade.exchange.Gateio.validate_ordertypes')
mocker.patch('freqtrade.exchange.Gate.validate_ordertypes')
patch_RPCManager(mocker)
patch_exchange(mocker, id=exchange_name)
freqtrade = FreqtradeBot(default_conf_usdt)
@@ -1068,7 +1070,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_sho
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
stoploss = MagicMock(return_value={'id': 13434334})
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
mocker.patch('freqtrade.exchange.Binance.create_stoploss', stoploss)
freqtrade = FreqtradeBot(default_conf_usdt)
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
@@ -1107,7 +1109,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
exit_order,
]),
get_fee=fee,
stoploss=stoploss
create_stoploss=stoploss
)
freqtrade = FreqtradeBot(default_conf_usdt)
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
@@ -1168,6 +1170,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
order_id='100',
ft_pair=trade.pair,
ft_is_open=True,
ft_amount=trade.amount,
ft_price=0.0,
))
assert trade
@@ -1187,7 +1191,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
caplog.clear()
mocker.patch(
'freqtrade.exchange.Exchange.stoploss',
'freqtrade.exchange.Exchange.create_stoploss',
side_effect=ExchangeError()
)
trade.is_open = True
@@ -1201,7 +1205,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
stoploss.reset_mock()
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order',
side_effect=InvalidOrderException())
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
mocker.patch('freqtrade.exchange.Exchange.create_stoploss', stoploss)
freqtrade.handle_stoploss_on_exchange(trade)
assert stoploss.call_count == 1
@@ -1211,7 +1215,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
trade.is_open = False
stoploss.reset_mock()
mocker.patch('freqtrade.exchange.Exchange.fetch_order')
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
mocker.patch('freqtrade.exchange.Exchange.create_stoploss', stoploss)
assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert stoploss.call_count == 0
@@ -1236,7 +1240,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order_with_result',
side_effect=InvalidOrderException())
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_cancelled)
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
mocker.patch('freqtrade.exchange.Exchange.create_stoploss', stoploss)
assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert trade.stoploss_order_id is None
assert trade.is_open is False
@@ -1267,7 +1271,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog,
mocker.patch.multiple(
'freqtrade.exchange.Binance',
fetch_stoploss_order=MagicMock(return_value={'status': 'canceled', 'id': 100}),
stoploss=MagicMock(side_effect=ExchangeError()),
create_stoploss=MagicMock(side_effect=ExchangeError()),
)
freqtrade = FreqtradeBot(default_conf_usdt)
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
@@ -1311,7 +1315,7 @@ def test_create_stoploss_order_invalid_order(
mocker.patch.multiple(
'freqtrade.exchange.Binance',
fetch_order=MagicMock(return_value={'status': 'canceled'}),
stoploss=MagicMock(side_effect=InvalidOrderException()),
create_stoploss=MagicMock(side_effect=InvalidOrderException()),
)
freqtrade = FreqtradeBot(default_conf_usdt)
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
@@ -1363,7 +1367,7 @@ def test_create_stoploss_order_insufficient_funds(
)
mocker.patch.multiple(
'freqtrade.exchange.Binance',
stoploss=MagicMock(side_effect=InsufficientFundsError()),
create_stoploss=MagicMock(side_effect=InsufficientFundsError()),
)
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
@@ -1413,7 +1417,7 @@ def test_handle_stoploss_on_exchange_trailing(
)
mocker.patch.multiple(
'freqtrade.exchange.Binance',
stoploss=stoploss,
create_stoploss=stoploss,
stoploss_adjust=MagicMock(return_value=True),
)
@@ -1474,7 +1478,7 @@ def test_handle_stoploss_on_exchange_trailing(
cancel_order_mock = MagicMock()
stoploss_order_mock = MagicMock(return_value={'id': 'so1'})
mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', cancel_order_mock)
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock)
mocker.patch('freqtrade.exchange.Binance.create_stoploss', stoploss_order_mock)
# stoploss should not be updated as the interval is 60 seconds
assert freqtrade.handle_trade(trade) is False
@@ -1538,7 +1542,7 @@ def test_handle_stoploss_on_exchange_trailing_error(
)
mocker.patch.multiple(
'freqtrade.exchange.Binance',
stoploss=stoploss,
create_stoploss=stoploss,
stoploss_adjust=MagicMock(return_value=True),
)
@@ -1589,7 +1593,7 @@ def test_handle_stoploss_on_exchange_trailing_error(
trade.stoploss_last_update = arrow.utcnow().shift(minutes=-601).datetime
caplog.clear()
cancel_mock = mocker.patch("freqtrade.exchange.Binance.cancel_stoploss_order", MagicMock())
mocker.patch("freqtrade.exchange.Binance.stoploss", side_effect=ExchangeError())
mocker.patch("freqtrade.exchange.Binance.create_stoploss", side_effect=ExchangeError())
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
assert cancel_mock.call_count == 1
assert log_has_re(r"Could not create trailing stoploss order for pair ETH/USDT\..*", caplog)
@@ -1607,7 +1611,7 @@ def test_stoploss_on_exchange_price_rounding(
adjust_mock = MagicMock(return_value=False)
mocker.patch.multiple(
'freqtrade.exchange.Binance',
stoploss=stoploss_mock,
create_stoploss=stoploss_mock,
stoploss_adjust=adjust_mock,
price_to_precision=price_mock,
)
@@ -1646,7 +1650,7 @@ def test_handle_stoploss_on_exchange_custom_stop(
)
mocker.patch.multiple(
'freqtrade.exchange.Binance',
stoploss=stoploss,
create_stoploss=stoploss,
stoploss_adjust=MagicMock(return_value=True),
)
@@ -1706,7 +1710,7 @@ def test_handle_stoploss_on_exchange_custom_stop(
cancel_order_mock = MagicMock()
stoploss_order_mock = MagicMock(return_value={'id': 'so1'})
mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', cancel_order_mock)
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock)
mocker.patch('freqtrade.exchange.Binance.create_stoploss', stoploss_order_mock)
# stoploss should not be updated as the interval is 60 seconds
assert freqtrade.handle_trade(trade) is False
@@ -1771,7 +1775,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde
{'id': exit_order['id']},
]),
get_fee=fee,
stoploss=stoploss,
create_stoploss=stoploss,
)
# enabling TSL
@@ -1823,7 +1827,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde
cancel_order_mock = MagicMock()
stoploss_order_mock = MagicMock()
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock)
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock)
mocker.patch('freqtrade.exchange.Binance.create_stoploss', stoploss_order_mock)
# price goes down 5%
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={
@@ -2378,7 +2382,7 @@ def test_close_trade(
trade.is_short = is_short
assert trade
oobj = Order.parse_from_ccxt_object(enter_order, enter_order['symbol'], trade.enter_side)
oobj = Order.parse_from_ccxt_object(enter_order, enter_order['symbol'], trade.entry_side)
trade.update_trade(oobj)
oobj = Order.parse_from_ccxt_object(exit_order, exit_order['symbol'], trade.exit_side)
trade.update_trade(oobj)
@@ -3603,7 +3607,7 @@ def test_execute_trade_exit_with_stoploss_on_exchange(
get_fee=fee,
amount_to_precision=lambda s, x, y: y,
price_to_precision=lambda s, x, y: y,
stoploss=stoploss,
create_stoploss=stoploss,
cancel_stoploss_order=cancel_order,
_is_dry_limit_order_filled=MagicMock(side_effect=[True, False]),
)
@@ -3664,7 +3668,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
}
})
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
mocker.patch('freqtrade.exchange.Binance.create_stoploss', stoploss)
freqtrade = FreqtradeBot(default_conf_usdt)
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
@@ -3898,7 +3902,7 @@ def test_exit_profit_only(
if exit_type == ExitType.EXIT_SIGNAL.value:
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
else:
freqtrade.strategy.stop_loss_reached = MagicMock(return_value=ExitCheckTuple(
freqtrade.strategy.ft_stoploss_reached = MagicMock(return_value=ExitCheckTuple(
exit_type=ExitType.NONE))
freqtrade.enter_positions()
@@ -4615,6 +4619,7 @@ def test_get_real_amount_open_trade_usdt(default_conf_usdt, fee, mocker):
'amount': amount,
'status': 'open',
'side': 'buy',
'price': 0.245441,
}
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
order_obj = Order.parse_from_ccxt_object(order, 'LTC/ETH', 'buy')
@@ -5023,7 +5028,7 @@ def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog, is_s
assert log_has_re(r"Error updating Order .*", caplog)
mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=InvalidOrderException)
hto_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_timedout_order')
hto_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_order')
# Orders which are no longer found after X days should be assumed as canceled.
freqtrade.startup_update_open_orders()
assert log_has_re(r"Order is older than \d days.*", caplog)

View File

@@ -56,7 +56,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
[ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)]]
)
cancel_order_mock = MagicMock()
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
mocker.patch('freqtrade.exchange.Binance.create_stoploss', stoploss)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,

View File

@@ -5,6 +5,7 @@ from copy import deepcopy
from pathlib import Path
from unittest.mock import MagicMock
import pandas as pd
import pytest
from freqtrade.misc import (dataframe_to_json, decimals_per_coin, deep_merge_dicts, file_dump_json,
@@ -231,3 +232,7 @@ def test_dataframe_json(ohlcv_history):
assert len(ohlcv_history) == len(dataframe)
assert_frame_equal(ohlcv_history, dataframe)
ohlcv_history.at[1, 'date'] = pd.NaT
json = dataframe_to_json(ohlcv_history)
dataframe = json_to_dataframe(json)

View File

@@ -45,8 +45,7 @@ def test_init_plotscript(default_conf, mocker, testdatadir):
default_conf['timerange'] = "20180110-20180112"
default_conf['trade_source'] = "file"
default_conf['timeframe'] = "5m"
default_conf["datadir"] = testdatadir
default_conf['exportfilename'] = testdatadir / "backtest-result_new.json"
default_conf['exportfilename'] = testdatadir / "backtest-result.json"
supported_markets = ["TRX/BTC", "ADA/BTC"]
ret = init_plotscript(default_conf, supported_markets)
assert "ohlcv" in ret
@@ -158,7 +157,7 @@ def test_plot_trades(testdatadir, caplog):
assert fig == fig1
assert log_has("No trades found.", caplog)
pair = "ADA/BTC"
filename = testdatadir / "backtest_results/backtest-result_new.json"
filename = testdatadir / "backtest_results/backtest-result.json"
trades = load_backtest_data(filename)
trades = trades.loc[trades['pair'] == pair]
@@ -299,7 +298,7 @@ def test_generate_plot_file(mocker, caplog):
def test_add_profit(testdatadir):
filename = testdatadir / "backtest_results/backtest-result_new.json"
filename = testdatadir / "backtest_results/backtest-result.json"
bt_data = load_backtest_data(filename)
timerange = TimeRange.parse_timerange("20180110-20180112")
@@ -319,7 +318,7 @@ def test_add_profit(testdatadir):
def test_generate_profit_graph(testdatadir):
filename = testdatadir / "backtest_results/backtest-result_new.json"
filename = testdatadir / "backtest_results/backtest-result.json"
trades = load_backtest_data(filename)
timerange = TimeRange.parse_timerange("20180110-20180112")
pairs = ["TRX/BTC", "XLM/BTC"]
@@ -354,7 +353,7 @@ def test_generate_profit_graph(testdatadir):
profit = find_trace_in_fig_data(figure.data, "Profit")
assert isinstance(profit, go.Scatter)
drawdown = find_trace_in_fig_data(figure.data, "Max drawdown 35.69%")
drawdown = find_trace_in_fig_data(figure.data, "Max drawdown 73.89%")
assert isinstance(drawdown, go.Scatter)
parallel = find_trace_in_fig_data(figure.data, "Parallel trades")
assert isinstance(parallel, go.Scatter)
@@ -394,8 +393,7 @@ def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir):
patch_exchange(mocker)
default_conf['trade_source'] = 'file'
default_conf["datadir"] = testdatadir
default_conf['exportfilename'] = testdatadir / "backtest-result_new.json"
default_conf['exportfilename'] = testdatadir / "backtest-result.json"
default_conf['indicators1'] = ["sma5", "ema10"]
default_conf['indicators2'] = ["macd"]
default_conf['pairs'] = ["ETH/BTC", "LTC/BTC"]
@@ -451,7 +449,6 @@ def test_start_plot_profit_error(mocker):
def test_plot_profit(default_conf, mocker, testdatadir):
patch_exchange(mocker)
default_conf['trade_source'] = 'file'
default_conf['datadir'] = testdatadir
default_conf['exportfilename'] = testdatadir / 'backtest-result_test_nofile.json'
default_conf['pairs'] = ['ETH/BTC', 'LTC/BTC']
@@ -466,7 +463,7 @@ def test_plot_profit(default_conf, mocker, testdatadir):
match=r"No trades found, cannot generate Profit-plot.*"):
plot_profit(default_conf)
default_conf['exportfilename'] = testdatadir / "backtest_results/backtest-result_new.json"
default_conf['exportfilename'] = testdatadir / "backtest_results/backtest-result.json"
plot_profit(default_conf)

View File

@@ -180,17 +180,17 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r
assert result == 0
@pytest.mark.parametrize('stake_amount,min_stake,stake_available,max_stake,expected', [
(22, 11, 50, 10000, 22),
(100, 11, 500, 10000, 100),
(1000, 11, 500, 10000, 500), # Above stake_available
(700, 11, 1000, 400, 400), # Above max_stake, below stake available
(20, 15, 10, 10000, 0), # Minimum stake > stake_available
(9, 11, 100, 10000, 11), # Below min stake
(1, 15, 10, 10000, 0), # Below min stake and min_stake > stake_available
(20, 50, 100, 10000, 0), # Below min stake and stake * 1.3 > min_stake
(1000, None, 1000, 10000, 1000), # No min-stake-amount could be determined
@pytest.mark.parametrize('stake_amount,min_stake,stake_available,max_stake,trade_amount,expected', [
(22, 11, 50, 10000, None, 22),
(100, 11, 500, 10000, None, 100),
(1000, 11, 500, 10000, None, 500), # Above stake_available
(700, 11, 1000, 400, None, 400), # Above max_stake, below stake available
(20, 15, 10, 10000, None, 0), # Minimum stake > stake_available
(9, 11, 100, 10000, None, 11), # Below min stake
(1, 15, 10, 10000, None, 0), # Below min stake and min_stake > stake_available
(20, 50, 100, 10000, None, 0), # Below min stake and stake * 1.3 > min_stake
(1000, None, 1000, 10000, None, 1000), # No min-stake-amount could be determined
(2000, 15, 2000, 3000, 1500, 1500), # Rebuy - resulting in too high stake amount. Adjusting.
])
def test_validate_stake_amount(
mocker,
@@ -199,13 +199,15 @@ def test_validate_stake_amount(
min_stake,
stake_available,
max_stake,
trade_amount,
expected,
):
freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch("freqtrade.wallets.Wallets.get_available_stake_amount",
return_value=stake_available)
res = freqtrade.wallets.validate_stake_amount('XRP/USDT', stake_amount, min_stake, max_stake)
res = freqtrade.wallets.validate_stake_amount(
'XRP/USDT', stake_amount, min_stake, max_stake, trade_amount)
assert res == expected

View File

@@ -1 +1 @@
{"latest_backtest":"backtest-result_new.json"}
{"latest_backtest":"backtest-result.json"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long