merge develop into feat/freqai-rl-dev
This commit is contained in:
@@ -18,6 +18,7 @@ from freqtrade.commands import (start_backtesting_show, start_convert_data, star
|
||||
from freqtrade.commands.db_commands import start_convert_db
|
||||
from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui,
|
||||
get_ui_download_url, read_ui_version)
|
||||
from freqtrade.commands.list_commands import start_list_freqAI_models
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
@@ -944,6 +945,34 @@ def test_start_list_strategies(capsys):
|
||||
assert str(Path("broken_strats/broken_futures_strategies.py")) in captured.out
|
||||
|
||||
|
||||
def test_start_list_freqAI_models(capsys):
|
||||
|
||||
args = [
|
||||
"list-freqaimodels",
|
||||
"-1"
|
||||
]
|
||||
pargs = get_args(args)
|
||||
pargs['config'] = None
|
||||
start_list_freqAI_models(pargs)
|
||||
captured = capsys.readouterr()
|
||||
assert "LightGBMClassifier" in captured.out
|
||||
assert "LightGBMRegressor" in captured.out
|
||||
assert "XGBoostRegressor" in captured.out
|
||||
assert "<builtin>/LightGBMRegressor.py" not in captured.out
|
||||
|
||||
args = [
|
||||
"list-freqaimodels",
|
||||
]
|
||||
pargs = get_args(args)
|
||||
pargs['config'] = None
|
||||
start_list_freqAI_models(pargs)
|
||||
captured = capsys.readouterr()
|
||||
assert "LightGBMClassifier" in captured.out
|
||||
assert "LightGBMRegressor" in captured.out
|
||||
assert "XGBoostRegressor" in captured.out
|
||||
assert "<builtin>/LightGBMRegressor.py" in captured.out
|
||||
|
||||
|
||||
def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
|
||||
patch_exchange(mocker, mock_markets=True)
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
|
@@ -10,6 +10,7 @@ from unittest.mock import MagicMock, Mock, PropertyMock
|
||||
|
||||
import arrow
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pytest
|
||||
from telegram import Chat, Message, Update
|
||||
|
||||
@@ -19,6 +20,7 @@ from freqtrade.data.converter import ohlcv_to_dataframe
|
||||
from freqtrade.edge import PairInfo
|
||||
from freqtrade.enums import CandleType, MarginMode, RunMode, SignalDirection, TradingMode
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.exchange import timeframe_to_minutes
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
from freqtrade.persistence import LocalTrade, Order, Trade, init_db
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
@@ -82,6 +84,33 @@ def get_args(args):
|
||||
return Arguments(args).get_parsed_arg()
|
||||
|
||||
|
||||
def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'):
|
||||
np.random.seed(42)
|
||||
tf_mins = timeframe_to_minutes(timeframe)
|
||||
|
||||
base = np.random.normal(20, 2, size=size)
|
||||
|
||||
date = pd.date_range(start, periods=size, freq=f'{tf_mins}min', tz='UTC')
|
||||
df = pd.DataFrame({
|
||||
'date': date,
|
||||
'open': base,
|
||||
'high': base + np.random.normal(2, 1, size=size),
|
||||
'low': base - np.random.normal(2, 1, size=size),
|
||||
'close': base + np.random.normal(0, 1, size=size),
|
||||
'volume': np.random.normal(200, size=size)
|
||||
}
|
||||
)
|
||||
df = df.dropna()
|
||||
return df
|
||||
|
||||
|
||||
def generate_test_data_raw(timeframe: str, size: int, start: str = '2020-07-05'):
|
||||
""" Generates data in the ohlcv format used by ccxt """
|
||||
df = generate_test_data(timeframe, size, start)
|
||||
df['date'] = df.loc[:, 'date'].view(np.int64) // 1000 // 1000
|
||||
return list(list(x) for x in zip(*(df[x].values.tolist() for x in df.columns)))
|
||||
|
||||
|
||||
# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines
|
||||
# TODO: This should be replaced with AsyncMock once support for python 3.7 is dropped.
|
||||
def get_mock_coro(return_value=None, side_effect=None):
|
||||
|
@@ -15,7 +15,7 @@ from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler, g
|
||||
from freqtrade.data.history.jsondatahandler import JsonDataHandler, JsonGzDataHandler
|
||||
from freqtrade.data.history.parquetdatahandler import ParquetDataHandler
|
||||
from freqtrade.enums import CandleType, TradingMode
|
||||
from tests.conftest import log_has
|
||||
from tests.conftest import log_has, log_has_re
|
||||
|
||||
|
||||
def test_datahandler_ohlcv_get_pairs(testdatadir):
|
||||
@@ -154,6 +154,85 @@ def test_jsondatahandler_ohlcv_load(testdatadir, caplog):
|
||||
assert df.columns.equals(df1.columns)
|
||||
|
||||
|
||||
def test_datahandler__check_empty_df(testdatadir, caplog):
|
||||
dh = JsonDataHandler(testdatadir)
|
||||
expected_text = r"Price jump in UNITTEST/USDT, 1h, spot between"
|
||||
df = DataFrame([
|
||||
[
|
||||
1511686200000, # 8:50:00
|
||||
8.794, # open
|
||||
8.948, # high
|
||||
8.794, # low
|
||||
8.88, # close
|
||||
2255, # volume (in quote currency)
|
||||
],
|
||||
[
|
||||
1511686500000, # 8:55:00
|
||||
8.88,
|
||||
8.942,
|
||||
8.88,
|
||||
8.893,
|
||||
9911,
|
||||
],
|
||||
[
|
||||
1511687100000, # 9:05:00
|
||||
8.891,
|
||||
8.893,
|
||||
8.875,
|
||||
8.877,
|
||||
2251
|
||||
],
|
||||
[
|
||||
1511687400000, # 9:10:00
|
||||
8.877,
|
||||
8.883,
|
||||
8.895,
|
||||
8.817,
|
||||
123551
|
||||
]
|
||||
], columns=['date', 'open', 'high', 'low', 'close', 'volume'])
|
||||
|
||||
dh._check_empty_df(df, 'UNITTEST/USDT', '1h', CandleType.SPOT, True, True)
|
||||
assert not log_has_re(expected_text, caplog)
|
||||
df = DataFrame([
|
||||
[
|
||||
1511686200000, # 8:50:00
|
||||
8.794, # open
|
||||
8.948, # high
|
||||
8.794, # low
|
||||
8.88, # close
|
||||
2255, # volume (in quote currency)
|
||||
],
|
||||
[
|
||||
1511686500000, # 8:55:00
|
||||
8.88,
|
||||
8.942,
|
||||
8.88,
|
||||
8.893,
|
||||
9911,
|
||||
],
|
||||
[
|
||||
1511687100000, # 9:05:00
|
||||
889.1, # Price jump by several decimals
|
||||
889.3,
|
||||
887.5,
|
||||
887.7,
|
||||
2251
|
||||
],
|
||||
[
|
||||
1511687400000, # 9:10:00
|
||||
8.877,
|
||||
8.883,
|
||||
8.895,
|
||||
8.817,
|
||||
123551
|
||||
]
|
||||
], columns=['date', 'open', 'high', 'low', 'close', 'volume'])
|
||||
|
||||
dh._check_empty_df(df, 'UNITTEST/USDT', '1h', CandleType.SPOT, True, True)
|
||||
assert log_has_re(expected_text, caplog)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('datahandler', ['feather', 'parquet'])
|
||||
def test_datahandler_trades_not_supported(datahandler, testdatadir, ):
|
||||
dh = get_datahandler(testdatadir, datahandler)
|
||||
|
@@ -5,7 +5,7 @@ from unittest.mock import MagicMock, PropertyMock
|
||||
import ccxt
|
||||
import pytest
|
||||
|
||||
from freqtrade.enums import MarginMode, TradingMode
|
||||
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
||||
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
||||
from tests.conftest import get_mock_coro, get_patched_exchange, log_has_re
|
||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
@@ -162,9 +162,6 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side):
|
||||
}
|
||||
assert exchange.stoploss_adjust(sl1, order, side=side)
|
||||
assert not exchange.stoploss_adjust(sl2, order, side=side)
|
||||
# Test with invalid order case
|
||||
order['type'] = 'stop_loss'
|
||||
assert not exchange.stoploss_adjust(sl3, order, side=side)
|
||||
|
||||
|
||||
def test_fill_leverage_tiers_binance(default_conf, mocker):
|
||||
@@ -542,7 +539,7 @@ def test__set_leverage_binance(mocker, default_conf):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('candle_type', ['mark', ''])
|
||||
@pytest.mark.parametrize('candle_type', [CandleType.MARK, ''])
|
||||
async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog, candle_type):
|
||||
ohlcv = [
|
||||
[
|
||||
|
@@ -56,7 +56,7 @@ EXCHANGES = {
|
||||
'leverage_in_spot_market': True,
|
||||
},
|
||||
'kucoin': {
|
||||
'pair': 'BTC/USDT',
|
||||
'pair': 'XRP/USDT',
|
||||
'stake_currency': 'USDT',
|
||||
'hasQuoteVolume': True,
|
||||
'timeframe': '5m',
|
||||
@@ -268,9 +268,8 @@ class TestCCXTExchange():
|
||||
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)
|
||||
|
||||
def ccxt__async_get_candle_history(self, exchange, exchangename, pair, timeframe):
|
||||
def ccxt__async_get_candle_history(self, exchange, exchangename, pair, timeframe, candle_type):
|
||||
|
||||
candle_type = CandleType.SPOT
|
||||
timeframe_ms = timeframe_to_msecs(timeframe)
|
||||
now = timeframe_to_prev_date(
|
||||
timeframe, datetime.now(timezone.utc))
|
||||
@@ -302,7 +301,8 @@ class TestCCXTExchange():
|
||||
return
|
||||
pair = EXCHANGES[exchangename]['pair']
|
||||
timeframe = EXCHANGES[exchangename]['timeframe']
|
||||
self.ccxt__async_get_candle_history(exchange, exchangename, pair, timeframe)
|
||||
self.ccxt__async_get_candle_history(
|
||||
exchange, exchangename, pair, timeframe, CandleType.SPOT)
|
||||
|
||||
def test_ccxt__async_get_candle_history_futures(self, exchange_futures):
|
||||
exchange, exchangename = exchange_futures
|
||||
@@ -311,7 +311,8 @@ class TestCCXTExchange():
|
||||
return
|
||||
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
||||
timeframe = EXCHANGES[exchangename]['timeframe']
|
||||
self.ccxt__async_get_candle_history(exchange, exchangename, pair, timeframe)
|
||||
self.ccxt__async_get_candle_history(
|
||||
exchange, exchangename, pair, timeframe, CandleType.FUTURES)
|
||||
|
||||
def test_ccxt_fetch_funding_rate_history(self, exchange_futures):
|
||||
exchange, exchangename = exchange_futures
|
||||
|
@@ -22,7 +22,8 @@ from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_CO
|
||||
calculate_backoff, remove_credentials)
|
||||
from freqtrade.exchange.exchange import amount_to_contract_precision
|
||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||
from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has_re, num_log_has_re
|
||||
from tests.conftest import (generate_test_data_raw, get_mock_coro, get_patched_exchange, log_has,
|
||||
log_has_re, num_log_has_re)
|
||||
|
||||
|
||||
# Make sure to always keep one exchange here which is NOT subclassed!!
|
||||
@@ -1833,6 +1834,7 @@ def test_get_tickers(default_conf, mocker, exchange_name):
|
||||
'last': 41,
|
||||
}
|
||||
}
|
||||
mocker.patch('freqtrade.exchange.exchange.Exchange.exchange_has', return_value=True)
|
||||
api_mock.fetch_tickers = MagicMock(return_value=tick)
|
||||
api_mock.fetch_bids_asks = MagicMock(return_value={})
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
@@ -1882,6 +1884,11 @@ def test_get_tickers(default_conf, mocker, exchange_name):
|
||||
assert api_mock.fetch_tickers.call_count == 1
|
||||
assert api_mock.fetch_bids_asks.call_count == (1 if exchange_name == 'binance' else 0)
|
||||
|
||||
api_mock.fetch_tickers.reset_mock()
|
||||
api_mock.fetch_bids_asks.reset_mock()
|
||||
mocker.patch('freqtrade.exchange.exchange.Exchange.exchange_has', return_value=False)
|
||||
assert exchange.get_tickers() == {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_fetch_ticker(default_conf, mocker, exchange_name):
|
||||
@@ -2083,7 +2090,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
|
||||
def test_refresh_latest_ohlcv(mocker, default_conf, caplog, candle_type) -> None:
|
||||
ohlcv = [
|
||||
[
|
||||
(arrow.utcnow().int_timestamp - 1) * 1000, # unix timestamp ms
|
||||
(arrow.utcnow().shift(minutes=-5).int_timestamp) * 1000, # unix timestamp ms
|
||||
1, # open
|
||||
2, # high
|
||||
3, # low
|
||||
@@ -2140,10 +2147,22 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog, candle_type) -> None
|
||||
assert len(res) == len(pairs)
|
||||
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 0
|
||||
exchange.required_candle_call_count = 1
|
||||
assert log_has(f"Using cached candle (OHLCV) data for {pairs[0][0]}, "
|
||||
f"{pairs[0][1]}, {candle_type} ...",
|
||||
caplog)
|
||||
caplog.clear()
|
||||
# Reset refresh times - must do 2 call per pair as cache is expired
|
||||
exchange._pairs_last_refresh_time = {}
|
||||
res = exchange.refresh_latest_ohlcv(
|
||||
[('IOTA/ETH', '5m', candle_type), ('XRP/ETH', '5m', candle_type)])
|
||||
assert len(res) == len(pairs)
|
||||
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 4
|
||||
|
||||
# cache - but disabled caching
|
||||
exchange._api_async.fetch_ohlcv.reset_mock()
|
||||
exchange.required_candle_call_count = 1
|
||||
|
||||
pairlist = [
|
||||
('IOTA/ETH', '5m', candle_type),
|
||||
('XRP/ETH', '5m', candle_type),
|
||||
@@ -2159,6 +2178,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog, candle_type) -> None
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 3
|
||||
exchange._api_async.fetch_ohlcv.reset_mock()
|
||||
caplog.clear()
|
||||
|
||||
# Call with invalid timeframe
|
||||
res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '3m', candle_type)], cache=False)
|
||||
if candle_type != CandleType.MARK:
|
||||
@@ -2169,6 +2189,101 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog, candle_type) -> None
|
||||
assert len(res) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize('candle_type', [CandleType.FUTURES, CandleType.MARK, CandleType.SPOT])
|
||||
def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_machine) -> None:
|
||||
start = datetime(2021, 8, 1, 0, 0, 0, 0, tzinfo=timezone.utc)
|
||||
ohlcv = generate_test_data_raw('1h', 100, start.strftime('%Y-%m-%d'))
|
||||
time_machine.move_to(start + timedelta(hours=99, minutes=30))
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
mocker.patch("freqtrade.exchange.Exchange.ohlcv_candle_limit", return_value=100)
|
||||
assert exchange._startup_candle_count == 0
|
||||
|
||||
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
|
||||
pair1 = ('IOTA/ETH', '1h', candle_type)
|
||||
pair2 = ('XRP/ETH', '1h', candle_type)
|
||||
pairs = [pair1, pair2]
|
||||
|
||||
# No caching
|
||||
assert not exchange._klines
|
||||
res = exchange.refresh_latest_ohlcv(pairs, cache=False)
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||
assert len(res) == 2
|
||||
assert len(res[pair1]) == 99
|
||||
assert len(res[pair2]) == 99
|
||||
assert not exchange._klines
|
||||
exchange._api_async.fetch_ohlcv.reset_mock()
|
||||
|
||||
# With caching
|
||||
res = exchange.refresh_latest_ohlcv(pairs)
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||
assert len(res) == 2
|
||||
assert len(res[pair1]) == 99
|
||||
assert len(res[pair2]) == 99
|
||||
assert exchange._klines
|
||||
assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-1][0] // 1000
|
||||
exchange._api_async.fetch_ohlcv.reset_mock()
|
||||
|
||||
# Returned from cache
|
||||
res = exchange.refresh_latest_ohlcv(pairs)
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 0
|
||||
assert len(res) == 2
|
||||
assert len(res[pair1]) == 99
|
||||
assert len(res[pair2]) == 99
|
||||
assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-1][0] // 1000
|
||||
|
||||
# Move time 1 candle further but result didn't change yet
|
||||
time_machine.move_to(start + timedelta(hours=101))
|
||||
res = exchange.refresh_latest_ohlcv(pairs)
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||
assert len(res) == 2
|
||||
assert len(res[pair1]) == 99
|
||||
assert len(res[pair2]) == 99
|
||||
assert res[pair2].at[0, 'open']
|
||||
assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-1][0] // 1000
|
||||
refresh_pior = exchange._pairs_last_refresh_time[pair1]
|
||||
|
||||
# New candle on exchange - return 100 candles - but skip one candle so we actually get 2 candles
|
||||
# in one go
|
||||
new_startdate = (start + timedelta(hours=2)).strftime('%Y-%m-%d %H:%M')
|
||||
# mocker.patch("freqtrade.exchange.Exchange.ohlcv_candle_limit", return_value=100)
|
||||
ohlcv = generate_test_data_raw('1h', 100, new_startdate)
|
||||
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
|
||||
res = exchange.refresh_latest_ohlcv(pairs)
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||
assert len(res) == 2
|
||||
assert len(res[pair1]) == 100
|
||||
assert len(res[pair2]) == 100
|
||||
# Verify index starts at 0
|
||||
assert res[pair2].at[0, 'open']
|
||||
assert refresh_pior != exchange._pairs_last_refresh_time[pair1]
|
||||
|
||||
assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-1][0] // 1000
|
||||
assert exchange._pairs_last_refresh_time[pair2] == ohlcv[-1][0] // 1000
|
||||
exchange._api_async.fetch_ohlcv.reset_mock()
|
||||
|
||||
# Retry same call - from cache
|
||||
res = exchange.refresh_latest_ohlcv(pairs)
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 0
|
||||
assert len(res) == 2
|
||||
assert len(res[pair1]) == 100
|
||||
assert len(res[pair2]) == 100
|
||||
assert res[pair2].at[0, 'open']
|
||||
|
||||
# Move to distant future (so a 1 call would cause a hole in the data)
|
||||
time_machine.move_to(start + timedelta(hours=2000))
|
||||
ohlcv = generate_test_data_raw('1h', 100, start + timedelta(hours=1900))
|
||||
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
|
||||
res = exchange.refresh_latest_ohlcv(pairs)
|
||||
|
||||
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||
assert len(res) == 2
|
||||
# Cache eviction - new data.
|
||||
assert len(res[pair1]) == 99
|
||||
assert len(res[pair2]) == 99
|
||||
assert res[pair2].at[0, 'open']
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name):
|
||||
@@ -2240,7 +2355,8 @@ async def test__async_kucoin_get_candle_history(default_conf, mocker, caplog):
|
||||
for _ in range(3):
|
||||
with pytest.raises(DDosProtection, match=r'429 Too Many Requests'):
|
||||
await exchange._async_get_candle_history(
|
||||
"ETH/BTC", "5m", (arrow.utcnow().int_timestamp - 2000) * 1000, count=3)
|
||||
"ETH/BTC", "5m", CandleType.SPOT,
|
||||
since_ms=(arrow.utcnow().int_timestamp - 2000) * 1000, count=3)
|
||||
assert num_log_has_re(msg, caplog) == 3
|
||||
|
||||
caplog.clear()
|
||||
@@ -2256,7 +2372,8 @@ async def test__async_kucoin_get_candle_history(default_conf, mocker, caplog):
|
||||
for _ in range(3):
|
||||
with pytest.raises(DDosProtection, match=r'429 Too Many Requests'):
|
||||
await exchange._async_get_candle_history(
|
||||
"ETH/BTC", "5m", (arrow.utcnow().int_timestamp - 2000) * 1000, count=3)
|
||||
"ETH/BTC", "5m", CandleType.SPOT,
|
||||
(arrow.utcnow().int_timestamp - 2000) * 1000, count=3)
|
||||
# Expect the "returned exception" message 12 times (4 retries * 3 (loop))
|
||||
assert num_log_has_re(msg, caplog) == 12
|
||||
assert num_log_has_re(msg2, caplog) == 9
|
||||
@@ -4234,9 +4351,10 @@ def test__fetch_and_calculate_funding_fees_datetime_called(
|
||||
('XLTCUSDT', 1, 'spot'),
|
||||
('LTC/USD', 1, 'futures'),
|
||||
('XLTCUSDT', 0.01, 'futures'),
|
||||
('ETH/USDT:USDT', 10, 'futures')
|
||||
('ETH/USDT:USDT', 10, 'futures'),
|
||||
('TORN/USDT:USDT', None, 'futures'), # Don't fail for unavailable pairs.
|
||||
])
|
||||
def est__get_contract_size(mocker, default_conf, pair, expected_size, trading_mode):
|
||||
def test__get_contract_size(mocker, default_conf, pair, expected_size, trading_mode):
|
||||
api_mock = MagicMock()
|
||||
default_conf['trading_mode'] = trading_mode
|
||||
default_conf['margin_mode'] = 'isolated'
|
||||
|
@@ -113,5 +113,4 @@ def test_stoploss_adjust_huobi(mocker, default_conf):
|
||||
assert exchange.stoploss_adjust(1501, order, 'sell')
|
||||
assert not exchange.stoploss_adjust(1499, order, 'sell')
|
||||
# Test with invalid order case
|
||||
order['type'] = 'stop_loss'
|
||||
assert not exchange.stoploss_adjust(1501, order, 'sell')
|
||||
assert exchange.stoploss_adjust(1501, order, 'sell')
|
||||
|
@@ -131,6 +131,8 @@ def make_unfiltered_dataframe(mocker, freqai_conf):
|
||||
unfiltered_dataframe = freqai.dk.use_strategy_to_populate_indicators(
|
||||
strategy, corr_dataframes, base_dataframes, freqai.dk.pair
|
||||
)
|
||||
for i in range(5):
|
||||
unfiltered_dataframe[f'constant_{i}'] = i
|
||||
|
||||
unfiltered_dataframe = freqai.dk.slice_dataframe(new_timerange, unfiltered_dataframe)
|
||||
|
||||
|
@@ -26,7 +26,7 @@ def test_freqai_backtest_start_backtest_list(freqai_conf, mocker, testdatadir, c
|
||||
'--config', 'config.json',
|
||||
'--datadir', str(testdatadir),
|
||||
'--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'),
|
||||
'--timeframe', '1h',
|
||||
'--timeframe', '1m',
|
||||
'--strategy-list', CURRENT_TEST_STRATEGY
|
||||
]
|
||||
args = get_args(args)
|
||||
|
@@ -125,7 +125,8 @@ def test_normalize_data(mocker, freqai_conf):
|
||||
freqai = make_data_dictionary(mocker, freqai_conf)
|
||||
data_dict = freqai.dk.data_dictionary
|
||||
freqai.dk.normalize_data(data_dict)
|
||||
assert len(freqai.dk.data) == 32
|
||||
assert any('_max' in entry for entry in freqai.dk.data.keys())
|
||||
assert any('_min' in entry for entry in freqai.dk.data.keys())
|
||||
|
||||
|
||||
def test_filter_features(mocker, freqai_conf):
|
||||
|
@@ -30,6 +30,7 @@ def is_mac() -> bool:
|
||||
@pytest.mark.parametrize('model', [
|
||||
'LightGBMRegressor',
|
||||
'XGBoostRegressor',
|
||||
'XGBoostRFRegressor',
|
||||
'CatboostRegressor',
|
||||
'ReinforcementLearner',
|
||||
'ReinforcementLearner_multiproc',
|
||||
@@ -69,10 +70,17 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model):
|
||||
|
||||
data_load_timerange = TimeRange.parse_timerange("20180125-20180130")
|
||||
new_timerange = TimeRange.parse_timerange("20180127-20180130")
|
||||
freqai.dk.set_paths('ADA/BTC', None)
|
||||
|
||||
freqai.train_timer("start", "ADA/BTC")
|
||||
freqai.extract_data_and_train_model(
|
||||
new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange)
|
||||
freqai.train_timer("stop", "ADA/BTC")
|
||||
freqai.dd.save_metric_tracker_to_disk()
|
||||
freqai.dd.save_drawer_to_disk()
|
||||
|
||||
assert Path(freqai.dk.full_path / "metric_tracker.json").is_file()
|
||||
assert Path(freqai.dk.full_path / "pair_dictionary.json").is_file()
|
||||
assert Path(freqai.dk.data_path /
|
||||
f"{freqai.dk.model_filename}_model.{model_save_ext}").is_file()
|
||||
assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").is_file()
|
||||
@@ -107,6 +115,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model):
|
||||
|
||||
data_load_timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
new_timerange = TimeRange.parse_timerange("20180120-20180130")
|
||||
freqai.dk.set_paths('ADA/BTC', None)
|
||||
|
||||
freqai.extract_data_and_train_model(
|
||||
new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange)
|
||||
@@ -125,6 +134,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model):
|
||||
'LightGBMClassifier',
|
||||
'CatboostClassifier',
|
||||
'XGBoostClassifier',
|
||||
'XGBoostRFClassifier',
|
||||
])
|
||||
def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model):
|
||||
if is_arm() and model == 'CatboostClassifier':
|
||||
@@ -148,6 +158,7 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model):
|
||||
|
||||
data_load_timerange = TimeRange.parse_timerange("20180110-20180130")
|
||||
new_timerange = TimeRange.parse_timerange("20180120-20180130")
|
||||
freqai.dk.set_paths('ADA/BTC', None)
|
||||
|
||||
freqai.extract_data_and_train_model(new_timerange, "ADA/BTC",
|
||||
strategy, freqai.dk, data_load_timerange)
|
||||
@@ -172,7 +183,7 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model):
|
||||
("CatboostClassifier", 6, "freqai_test_classifier")
|
||||
],
|
||||
)
|
||||
def test_start_backtesting(mocker, freqai_conf, model, num_files, strat):
|
||||
def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog):
|
||||
freqai_conf.get("freqai", {}).update({"save_backtest_models": True})
|
||||
freqai_conf['runmode'] = RunMode.BACKTEST
|
||||
if is_arm() and "Catboost" in model:
|
||||
@@ -205,6 +216,9 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat):
|
||||
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")
|
||||
for i in range(5):
|
||||
df[f'%-constant_{i}'] = i
|
||||
# df.loc[:, f'%-constant_{i}'] = i
|
||||
|
||||
metadata = {"pair": "LTC/BTC"}
|
||||
freqai.start_backtesting(df, metadata, freqai.dk)
|
||||
@@ -212,6 +226,14 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat):
|
||||
|
||||
assert len(model_folders) == num_files
|
||||
Trade.use_db = True
|
||||
assert log_has_re(
|
||||
"Removed features ",
|
||||
caplog,
|
||||
)
|
||||
assert log_has_re(
|
||||
"Removed 5 features from prediction features, ",
|
||||
caplog,
|
||||
)
|
||||
Backtesting.cleanup()
|
||||
shutil.rmtree(Path(freqai.dk.full_path))
|
||||
|
||||
@@ -237,6 +259,7 @@ def test_start_backtesting_subdaily_backtest_period(mocker, freqai_conf):
|
||||
metadata = {"pair": "LTC/BTC"}
|
||||
freqai.start_backtesting(df, metadata, freqai.dk)
|
||||
model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()]
|
||||
|
||||
assert len(model_folders) == 9
|
||||
|
||||
shutil.rmtree(Path(freqai.dk.full_path))
|
||||
@@ -281,6 +304,7 @@ def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
|
||||
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")
|
||||
|
||||
freqai.start_backtesting(df, metadata, freqai.dk)
|
||||
|
||||
assert log_has_re(
|
||||
@@ -337,6 +361,7 @@ def test_follow_mode(mocker, freqai_conf):
|
||||
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
|
||||
|
||||
df = strategy.dp.get_pair_dataframe('ADA/BTC', '5m')
|
||||
|
||||
freqai.start_live(df, metadata, strategy, freqai.dk)
|
||||
|
||||
assert len(freqai.dk.return_dataframe.index) == 5702
|
||||
|
@@ -97,7 +97,6 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'):
|
||||
'start_date': min_date,
|
||||
'end_date': max_date,
|
||||
'max_open_trades': 10,
|
||||
'position_stacking': False,
|
||||
}
|
||||
|
||||
|
||||
@@ -735,7 +734,6 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=10,
|
||||
position_stacking=False,
|
||||
)
|
||||
results = result['results']
|
||||
assert not results.empty
|
||||
@@ -799,6 +797,34 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
||||
t["close_rate"], 6) < round(ln.iloc[0]["high"], 6))
|
||||
|
||||
|
||||
def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir) -> None:
|
||||
# This strategy intentionally places unfillable orders.
|
||||
default_conf['strategy'] = 'StrategyTestV3CustomEntryPrice'
|
||||
default_conf['startup_candle_count'] = 0
|
||||
# Cancel unfilled order after 4 minutes on 5m timeframe.
|
||||
default_conf["unfilledtimeout"] = {"entry": 4}
|
||||
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'))
|
||||
patch_exchange(mocker)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting._set_strategy(backtesting.strategylist[0])
|
||||
# Testing dataframe contains 11 candles. Expecting 10 timed out orders.
|
||||
timerange = TimeRange('date', 'date', 1517227800, 1517231100)
|
||||
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
|
||||
timerange=timerange)
|
||||
min_date, max_date = get_timerange(data)
|
||||
|
||||
result = backtesting.backtest(
|
||||
processed=deepcopy(data),
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=1,
|
||||
)
|
||||
|
||||
assert result['timedout_entry_orders'] == 10
|
||||
|
||||
|
||||
def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None:
|
||||
default_conf['use_exit_signal'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
@@ -819,7 +845,6 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=1,
|
||||
position_stacking=False,
|
||||
)
|
||||
assert not results['results'].empty
|
||||
assert len(results['results']) == 1
|
||||
@@ -851,7 +876,6 @@ def test_backtest_trim_no_data_left(default_conf, fee, mocker, testdatadir) -> N
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=10,
|
||||
position_stacking=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -906,7 +930,6 @@ def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadi
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=10,
|
||||
position_stacking=False,
|
||||
)
|
||||
assert count == 5
|
||||
|
||||
@@ -950,8 +973,6 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=1,
|
||||
position_stacking=False,
|
||||
enable_protections=default_conf.get('enable_protections', False),
|
||||
)
|
||||
assert len(results['results']) == numres
|
||||
|
||||
@@ -994,8 +1015,6 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir,
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=1,
|
||||
position_stacking=False,
|
||||
enable_protections=default_conf.get('enable_protections', False),
|
||||
)
|
||||
assert len(results['results']) == expected
|
||||
|
||||
@@ -1107,7 +1126,6 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
||||
'start_date': min_date,
|
||||
'end_date': max_date,
|
||||
'max_open_trades': 3,
|
||||
'position_stacking': False,
|
||||
}
|
||||
|
||||
results = backtesting.backtest(**backtest_conf)
|
||||
@@ -1130,7 +1148,6 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
||||
'start_date': min_date,
|
||||
'end_date': max_date,
|
||||
'max_open_trades': 1,
|
||||
'position_stacking': False,
|
||||
}
|
||||
results = backtesting.backtest(**backtest_conf)
|
||||
assert len(evaluate_result_multi(results['results'], '5m', 1)) == 0
|
||||
|
@@ -42,7 +42,6 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) ->
|
||||
start_date=min_date,
|
||||
end_date=max_date,
|
||||
max_open_trades=10,
|
||||
position_stacking=False,
|
||||
)
|
||||
results = result['results']
|
||||
assert not results.empty
|
||||
|
@@ -336,7 +336,7 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
|
||||
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
|
||||
assert hasattr(hyperopt, "max_open_trades")
|
||||
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
|
||||
assert hasattr(hyperopt, "position_stacking")
|
||||
assert hasattr(hyperopt.backtesting, "_position_stacking")
|
||||
|
||||
|
||||
def test_hyperopt_format_results(hyperopt):
|
||||
@@ -704,7 +704,7 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non
|
||||
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
|
||||
assert hasattr(hyperopt, "max_open_trades")
|
||||
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
|
||||
assert hasattr(hyperopt, "position_stacking")
|
||||
assert hasattr(hyperopt.backtesting, "_position_stacking")
|
||||
|
||||
|
||||
def test_simplified_interface_all_failed(mocker, hyperopt_conf, caplog) -> None:
|
||||
@@ -778,7 +778,7 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
|
||||
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
|
||||
assert hasattr(hyperopt, "max_open_trades")
|
||||
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
|
||||
assert hasattr(hyperopt, "position_stacking")
|
||||
assert hasattr(hyperopt.backtesting, "_position_stacking")
|
||||
|
||||
|
||||
def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
|
||||
@@ -821,7 +821,7 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
|
||||
assert hasattr(hyperopt.backtesting.strategy, "advise_entry")
|
||||
assert hasattr(hyperopt, "max_open_trades")
|
||||
assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
|
||||
assert hasattr(hyperopt, "position_stacking")
|
||||
assert hasattr(hyperopt.backtesting, "_position_stacking")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("space", [
|
||||
@@ -910,8 +910,9 @@ def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir,
|
||||
})
|
||||
hyperopt = Hyperopt(hyperopt_conf)
|
||||
hyperopt.backtesting.exchange.get_max_leverage = lambda *x, **xx: 1.0
|
||||
hyperopt.backtesting.exchange.get_min_pair_stake_amount = lambda *x, **xx: 1.0
|
||||
hyperopt.backtesting.exchange.get_min_pair_stake_amount = lambda *x, **xx: 0.00001
|
||||
hyperopt.backtesting.exchange.get_max_pair_stake_amount = lambda *x, **xx: 100.0
|
||||
hyperopt.backtesting.exchange._markets = get_markets()
|
||||
|
||||
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
|
||||
assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter)
|
||||
|
0
tests/persistence/__init__.py
Normal file
0
tests/persistence/__init__.py
Normal file
@@ -2404,8 +2404,10 @@ def test_Trade_object_idem():
|
||||
'get_enter_tag_performance',
|
||||
'get_mix_tag_performance',
|
||||
'get_trading_volume',
|
||||
|
||||
'from_json',
|
||||
)
|
||||
EXCLUDES2 = ('trades', 'trades_open', 'bt_trades_open_pp', 'bt_open_open_trade_count',
|
||||
'total_profit')
|
||||
|
||||
# Parent (LocalTrade) should have the same attributes
|
||||
for item in trade:
|
||||
@@ -2416,7 +2418,7 @@ def test_Trade_object_idem():
|
||||
# Fails if only a column is added without corresponding parent field
|
||||
for item in localtrade:
|
||||
if (not item.startswith('__')
|
||||
and item not in ('trades', 'trades_open', 'total_profit')
|
||||
and item not in EXCLUDES2
|
||||
and type(getattr(LocalTrade, item)) not in (property, FunctionType)):
|
||||
assert item in trade
|
||||
|
181
tests/persistence/test_trade_fromjson.py
Normal file
181
tests/persistence/test_trade_fromjson.py
Normal file
@@ -0,0 +1,181 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from freqtrade.persistence.trade_model import Trade
|
||||
|
||||
|
||||
def test_trade_fromjson():
|
||||
"""Test the Trade.from_json() method."""
|
||||
trade_string = """{
|
||||
"trade_id": 25,
|
||||
"pair": "ETH/USDT",
|
||||
"base_currency": "ETH",
|
||||
"quote_currency": "USDT",
|
||||
"is_open": false,
|
||||
"exchange": "binance",
|
||||
"amount": 407.0,
|
||||
"amount_requested": 102.92547026,
|
||||
"stake_amount": 102.7494348,
|
||||
"strategy": "SampleStrategy55",
|
||||
"buy_tag": "Strategy2",
|
||||
"enter_tag": "Strategy2",
|
||||
"timeframe": 5,
|
||||
"fee_open": 0.001,
|
||||
"fee_open_cost": 0.1027494,
|
||||
"fee_open_currency": "ETH",
|
||||
"fee_close": 0.001,
|
||||
"fee_close_cost": 0.1054944,
|
||||
"fee_close_currency": "USDT",
|
||||
"open_date": "2022-10-18 09:12:42",
|
||||
"open_timestamp": 1666084362912,
|
||||
"open_rate": 0.2518998249562391,
|
||||
"open_rate_requested": 0.2516,
|
||||
"open_trade_value": 102.62575199,
|
||||
"close_date": "2022-10-18 09:45:22",
|
||||
"close_timestamp": 1666086322208,
|
||||
"realized_profit": 2.76315361,
|
||||
"close_rate": 0.2592,
|
||||
"close_rate_requested": 0.2592,
|
||||
"close_profit": 0.026865,
|
||||
"close_profit_pct": 2.69,
|
||||
"close_profit_abs": 2.76315361,
|
||||
"trade_duration_s": 1959,
|
||||
"trade_duration": 32,
|
||||
"profit_ratio": 0.02686,
|
||||
"profit_pct": 2.69,
|
||||
"profit_abs": 2.76315361,
|
||||
"sell_reason": "no longer good",
|
||||
"exit_reason": "no longer good",
|
||||
"exit_order_status": "closed",
|
||||
"stop_loss_abs": 0.1981,
|
||||
"stop_loss_ratio": -0.216,
|
||||
"stop_loss_pct": -21.6,
|
||||
"stoploss_order_id": null,
|
||||
"stoploss_last_update": null,
|
||||
"stoploss_last_update_timestamp": null,
|
||||
"initial_stop_loss_abs": 0.1981,
|
||||
"initial_stop_loss_ratio": -0.216,
|
||||
"initial_stop_loss_pct": -21.6,
|
||||
"min_rate": 0.2495,
|
||||
"max_rate": 0.2592,
|
||||
"leverage": 1.0,
|
||||
"interest_rate": 0.0,
|
||||
"liquidation_price": null,
|
||||
"is_short": false,
|
||||
"trading_mode": "spot",
|
||||
"funding_fees": 0.0,
|
||||
"open_order_id": null,
|
||||
"orders": [
|
||||
{
|
||||
"amount": 102.0,
|
||||
"safe_price": 0.2526,
|
||||
"ft_order_side": "buy",
|
||||
"order_filled_timestamp": 1666084370887,
|
||||
"ft_is_entry": true,
|
||||
"pair": "ETH/USDT",
|
||||
"order_id": "78404228",
|
||||
"status": "closed",
|
||||
"average": 0.2526,
|
||||
"cost": 25.7652,
|
||||
"filled": 102.0,
|
||||
"is_open": false,
|
||||
"order_date": "2022-10-18 09:12:42",
|
||||
"order_timestamp": 1666084362684,
|
||||
"order_filled_date": "2022-10-18 09:12:50",
|
||||
"order_type": "limit",
|
||||
"price": 0.2526,
|
||||
"remaining": 0.0
|
||||
},
|
||||
{
|
||||
"amount": 102.0,
|
||||
"safe_price": 0.2517,
|
||||
"ft_order_side": "buy",
|
||||
"order_filled_timestamp": 1666084379056,
|
||||
"ft_is_entry": true,
|
||||
"pair": "ETH/USDT",
|
||||
"order_id": "78405139",
|
||||
"status": "closed",
|
||||
"average": 0.2517,
|
||||
"cost": 25.6734,
|
||||
"filled": 102.0,
|
||||
"is_open": false,
|
||||
"order_date": "2022-10-18 09:12:57",
|
||||
"order_timestamp": 1666084377681,
|
||||
"order_filled_date": "2022-10-18 09:12:59",
|
||||
"order_type": "limit",
|
||||
"price": 0.2517,
|
||||
"remaining": 0.0
|
||||
},
|
||||
{
|
||||
"amount": 102.0,
|
||||
"safe_price": 0.2517,
|
||||
"ft_order_side": "buy",
|
||||
"order_filled_timestamp": 1666084389644,
|
||||
"ft_is_entry": true,
|
||||
"pair": "ETH/USDT",
|
||||
"order_id": "78405265",
|
||||
"status": "closed",
|
||||
"average": 0.2517,
|
||||
"cost": 25.6734,
|
||||
"filled": 102.0,
|
||||
"is_open": false,
|
||||
"order_date": "2022-10-18 09:13:03",
|
||||
"order_timestamp": 1666084383295,
|
||||
"order_filled_date": "2022-10-18 09:13:09",
|
||||
"order_type": "limit",
|
||||
"price": 0.2517,
|
||||
"remaining": 0.0
|
||||
},
|
||||
{
|
||||
"amount": 102.0,
|
||||
"safe_price": 0.2516,
|
||||
"ft_order_side": "buy",
|
||||
"order_filled_timestamp": 1666084723521,
|
||||
"ft_is_entry": true,
|
||||
"pair": "ETH/USDT",
|
||||
"order_id": "78405395",
|
||||
"status": "closed",
|
||||
"average": 0.2516,
|
||||
"cost": 25.6632,
|
||||
"filled": 102.0,
|
||||
"is_open": false,
|
||||
"order_date": "2022-10-18 09:13:13",
|
||||
"order_timestamp": 1666084393920,
|
||||
"order_filled_date": "2022-10-18 09:18:43",
|
||||
"order_type": "limit",
|
||||
"price": 0.2516,
|
||||
"remaining": 0.0
|
||||
},
|
||||
{
|
||||
"amount": 407.0,
|
||||
"safe_price": 0.2592,
|
||||
"ft_order_side": "sell",
|
||||
"order_filled_timestamp": 1666086322198,
|
||||
"ft_is_entry": false,
|
||||
"pair": "ETH/USDT",
|
||||
"order_id": "78432649",
|
||||
"status": "closed",
|
||||
"average": 0.2592,
|
||||
"cost": 105.4944,
|
||||
"filled": 407.0,
|
||||
"is_open": false,
|
||||
"order_date": "2022-10-18 09:45:21",
|
||||
"order_timestamp": 1666086321435,
|
||||
"order_filled_date": "2022-10-18 09:45:22",
|
||||
"order_type": "market",
|
||||
"price": 0.2592,
|
||||
"remaining": 0.0
|
||||
}
|
||||
]
|
||||
}"""
|
||||
trade = Trade.from_json(trade_string)
|
||||
|
||||
assert trade.id == 25
|
||||
assert trade.pair == 'ETH/USDT'
|
||||
assert trade.open_date == datetime(2022, 10, 18, 9, 12, 42, tzinfo=timezone.utc)
|
||||
assert isinstance(trade.open_date, datetime)
|
||||
assert trade.exit_reason == 'no longer good'
|
||||
|
||||
assert len(trade.orders) == 5
|
||||
last_o = trade.orders[-1]
|
||||
assert last_o.order_filled_date == datetime(2022, 10, 18, 9, 45, 22, tzinfo=timezone.utc)
|
||||
assert isinstance(last_o.order_date, datetime)
|
@@ -1443,8 +1443,9 @@ def test_api_plot_config(botclient):
|
||||
assert isinstance(rc.json()['subplots'], dict)
|
||||
|
||||
|
||||
def test_api_strategies(botclient):
|
||||
def test_api_strategies(botclient, tmpdir):
|
||||
ftbot, client = botclient
|
||||
ftbot.config['user_data_dir'] = Path(tmpdir)
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/strategies")
|
||||
|
||||
@@ -1456,6 +1457,7 @@ def test_api_strategies(botclient):
|
||||
'InformativeDecoratorTest',
|
||||
'StrategyTestV2',
|
||||
'StrategyTestV3',
|
||||
'StrategyTestV3CustomEntryPrice',
|
||||
'StrategyTestV3Futures',
|
||||
'freqai_rl_test_strat',
|
||||
'freqai_test_classifier',
|
||||
|
@@ -99,6 +99,7 @@ def test_send_msg_telegram_error(mocker, default_conf, caplog) -> None:
|
||||
|
||||
def test_process_msg_queue(mocker, default_conf, caplog) -> None:
|
||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg')
|
||||
default_conf['telegram']['allow_custom_messages'] = True
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init')
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
@@ -108,8 +109,8 @@ def test_process_msg_queue(mocker, default_conf, caplog) -> None:
|
||||
queue.append('Test message 2')
|
||||
rpc_manager.process_msg_queue(queue)
|
||||
|
||||
assert log_has("Sending rpc message: {'type': strategy_msg, 'msg': 'Test message'}", caplog)
|
||||
assert log_has("Sending rpc message: {'type': strategy_msg, 'msg': 'Test message 2'}", caplog)
|
||||
assert log_has("Sending rpc strategy_msg: Test message", caplog)
|
||||
assert log_has("Sending rpc strategy_msg: Test message 2", caplog)
|
||||
assert telegram_mock.call_count == 2
|
||||
|
||||
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from requests import RequestException
|
||||
|
||||
from freqtrade.enums import ExitType, RPCMessageType
|
||||
@@ -337,34 +336,18 @@ def test_exception_send_msg(default_conf, mocker, caplog):
|
||||
caplog)
|
||||
|
||||
default_conf["webhook"] = get_webhook_dict()
|
||||
default_conf["webhook"]["webhookentry"]["value1"] = "{DEADBEEF:8f}"
|
||||
default_conf["webhook"]["strategy_msg"] = {"value1": "{DEADBEEF:8f}"}
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||
webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
|
||||
msg = {
|
||||
'type': RPCMessageType.ENTRY,
|
||||
'exchange': 'Binance',
|
||||
'pair': 'ETH/BTC',
|
||||
'limit': 0.005,
|
||||
'order_type': 'limit',
|
||||
'stake_amount': 0.8,
|
||||
'stake_amount_fiat': 500,
|
||||
'stake_currency': 'BTC',
|
||||
'fiat_currency': 'EUR'
|
||||
'type': RPCMessageType.STRATEGY_MSG,
|
||||
'msg': 'hello world',
|
||||
}
|
||||
webhook.send_msg(msg)
|
||||
assert log_has("Problem calling Webhook. Please check your webhook configuration. "
|
||||
"Exception: 'DEADBEEF'", caplog)
|
||||
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||
msg = {
|
||||
'type': 'DEADBEEF',
|
||||
'status': 'whatever'
|
||||
}
|
||||
with pytest.raises(NotImplementedError):
|
||||
webhook.send_msg(msg)
|
||||
|
||||
# Test no failure for not implemented but known messagetypes
|
||||
for e in RPCMessageType:
|
||||
msg = {
|
||||
|
37
tests/strategy/strats/strategy_test_v3_custom_entry_price.py
Normal file
37
tests/strategy/strats/strategy_test_v3_custom_entry_price.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pandas import DataFrame
|
||||
from strategy_test_v3 import StrategyTestV3
|
||||
|
||||
|
||||
class StrategyTestV3CustomEntryPrice(StrategyTestV3):
|
||||
"""
|
||||
Strategy used by tests freqtrade bot.
|
||||
Please do not modify this strategy, it's intended for internal use only.
|
||||
Please look at the SampleStrategy in the user_data/strategy directory
|
||||
or strategy repository https://github.com/freqtrade/freqtrade-strategies
|
||||
for samples and inspiration.
|
||||
"""
|
||||
new_entry_price: float = 0.001
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
|
||||
dataframe.loc[
|
||||
dataframe['volume'] > 0,
|
||||
'enter_long'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
return dataframe
|
||||
|
||||
def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
|
||||
entry_tag: Optional[str], side: str, **kwargs) -> float:
|
||||
|
||||
return self.new_entry_price
|
@@ -5,29 +5,8 @@ import pytest
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||
from freqtrade.strategy import (merge_informative_pair, stoploss_from_absolute, stoploss_from_open,
|
||||
timeframe_to_minutes)
|
||||
from tests.conftest import get_patched_exchange
|
||||
|
||||
|
||||
def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'):
|
||||
np.random.seed(42)
|
||||
tf_mins = timeframe_to_minutes(timeframe)
|
||||
|
||||
base = np.random.normal(20, 2, size=size)
|
||||
|
||||
date = pd.date_range(start, periods=size, freq=f'{tf_mins}min', tz='UTC')
|
||||
df = pd.DataFrame({
|
||||
'date': date,
|
||||
'open': base,
|
||||
'high': base + np.random.normal(2, 1, size=size),
|
||||
'low': base - np.random.normal(2, 1, size=size),
|
||||
'close': base + np.random.normal(0, 1, size=size),
|
||||
'volume': np.random.normal(200, size=size)
|
||||
}
|
||||
)
|
||||
df = df.dropna()
|
||||
return df
|
||||
from freqtrade.strategy import merge_informative_pair, stoploss_from_absolute, stoploss_from_open
|
||||
from tests.conftest import generate_test_data, get_patched_exchange
|
||||
|
||||
|
||||
def test_merge_informative_pair():
|
||||
|
@@ -32,24 +32,25 @@ def test_search_strategy():
|
||||
|
||||
def test_search_all_strategies_no_failed():
|
||||
directory = Path(__file__).parent / "strats"
|
||||
strategies = StrategyResolver.search_all_objects(directory, enum_failed=False)
|
||||
strategies = StrategyResolver._search_all_objects(directory, enum_failed=False)
|
||||
assert isinstance(strategies, list)
|
||||
assert len(strategies) == 10
|
||||
assert len(strategies) == 11
|
||||
assert isinstance(strategies[0], dict)
|
||||
|
||||
|
||||
def test_search_all_strategies_with_failed():
|
||||
directory = Path(__file__).parent / "strats"
|
||||
strategies = StrategyResolver.search_all_objects(directory, enum_failed=True)
|
||||
strategies = StrategyResolver._search_all_objects(directory, enum_failed=True)
|
||||
assert isinstance(strategies, list)
|
||||
assert len(strategies) == 11
|
||||
assert len(strategies) == 12
|
||||
# with enum_failed=True search_all_objects() shall find 2 good strategies
|
||||
# and 1 which fails to load
|
||||
assert len([x for x in strategies if x['class'] is not None]) == 10
|
||||
assert len([x for x in strategies if x['class'] is not None]) == 11
|
||||
|
||||
assert len([x for x in strategies if x['class'] is None]) == 1
|
||||
|
||||
directory = Path(__file__).parent / "strats_nonexistingdir"
|
||||
strategies = StrategyResolver.search_all_objects(directory, enum_failed=True)
|
||||
strategies = StrategyResolver._search_all_objects(directory, enum_failed=True)
|
||||
assert len(strategies) == 0
|
||||
|
||||
|
||||
@@ -77,10 +78,9 @@ def test_load_strategy_base64(dataframe_1m, caplog, default_conf):
|
||||
|
||||
|
||||
def test_load_strategy_invalid_directory(caplog, default_conf):
|
||||
default_conf['strategy'] = 'StrategyTestV3'
|
||||
extra_dir = Path.cwd() / 'some/path'
|
||||
with pytest.raises(OperationalException):
|
||||
StrategyResolver._load_strategy(CURRENT_TEST_STRATEGY, config=default_conf,
|
||||
with pytest.raises(OperationalException, match=r"Impossible to load Strategy.*"):
|
||||
StrategyResolver._load_strategy('StrategyTestV333', config=default_conf,
|
||||
extra_dir=extra_dir)
|
||||
|
||||
assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog)
|
||||
@@ -102,8 +102,8 @@ def test_load_strategy_noname(default_conf):
|
||||
StrategyResolver.load_strategy(default_conf)
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:deprecated")
|
||||
@pytest.mark.parametrize('strategy_name', ['StrategyTestV2'])
|
||||
@ pytest.mark.filterwarnings("ignore:deprecated")
|
||||
@ pytest.mark.parametrize('strategy_name', ['StrategyTestV2'])
|
||||
def test_strategy_pre_v3(dataframe_1m, default_conf, strategy_name):
|
||||
default_conf.update({'strategy': strategy_name})
|
||||
|
||||
@@ -349,7 +349,7 @@ 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)
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:deprecated")
|
||||
@ pytest.mark.filterwarnings("ignore:deprecated")
|
||||
def test_missing_implements(default_conf, caplog):
|
||||
|
||||
default_location = Path(__file__).parent / "strats"
|
||||
|
@@ -1028,6 +1028,31 @@ def test__validate_pricing_rules(default_conf, caplog) -> None:
|
||||
validate_config_consistency(conf)
|
||||
|
||||
|
||||
def test__validate_freqai_include_timeframes(default_conf, caplog) -> None:
|
||||
conf = deepcopy(default_conf)
|
||||
conf.update({
|
||||
"freqai": {
|
||||
"enabled": True,
|
||||
"feature_parameters": {
|
||||
"include_timeframes": ["1m", "5m"],
|
||||
"include_corr_pairlist": [],
|
||||
},
|
||||
"data_split_parameters": {},
|
||||
"model_training_parameters": {}
|
||||
}
|
||||
})
|
||||
with pytest.raises(OperationalException, match=r"Main timeframe of .*"):
|
||||
validate_config_consistency(conf)
|
||||
# Validation pass
|
||||
conf.update({'timeframe': '1m'})
|
||||
validate_config_consistency(conf)
|
||||
conf.update({'analyze_per_epoch': True})
|
||||
|
||||
with pytest.raises(OperationalException,
|
||||
match=r"Using analyze-per-epoch .* not supported with a FreqAI strategy."):
|
||||
validate_config_consistency(conf)
|
||||
|
||||
|
||||
def test__validate_consumers(default_conf, caplog) -> None:
|
||||
conf = deepcopy(default_conf)
|
||||
conf.update({
|
||||
|
@@ -25,7 +25,7 @@ def test_create_userdata_dir(mocker, default_conf, caplog) -> None:
|
||||
md = mocker.patch.object(Path, 'mkdir', MagicMock())
|
||||
|
||||
x = create_userdata_dir('/tmp/bar', create_dir=True)
|
||||
assert md.call_count == 9
|
||||
assert md.call_count == 10
|
||||
assert md.call_args[1]['parents'] is False
|
||||
assert log_has(f'Created user-data directory: {Path("/tmp/bar")}', caplog)
|
||||
assert isinstance(x, Path)
|
||||
|
@@ -3969,15 +3969,17 @@ def test__safe_exit_amount(default_conf_usdt, fee, caplog, mocker, amount_wallet
|
||||
patch_get_signal(freqtrade)
|
||||
if has_err:
|
||||
with pytest.raises(DependencyException, match=r"Not enough amount to exit trade."):
|
||||
assert freqtrade._safe_exit_amount(trade.pair, trade.amount)
|
||||
assert freqtrade._safe_exit_amount(trade, trade.pair, trade.amount)
|
||||
else:
|
||||
wallet_update.reset_mock()
|
||||
assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet
|
||||
assert trade.amount != amount_wallet
|
||||
assert freqtrade._safe_exit_amount(trade, trade.pair, trade.amount) == amount_wallet
|
||||
assert log_has_re(r'.*Falling back to wallet-amount.', caplog)
|
||||
assert trade.amount == amount_wallet
|
||||
assert wallet_update.call_count == 1
|
||||
caplog.clear()
|
||||
wallet_update.reset_mock()
|
||||
assert freqtrade._safe_exit_amount(trade.pair, amount_wallet) == amount_wallet
|
||||
assert freqtrade._safe_exit_amount(trade, trade.pair, amount_wallet) == amount_wallet
|
||||
assert not log_has_re(r'.*Falling back to wallet-amount.', caplog)
|
||||
assert wallet_update.call_count == 1
|
||||
|
||||
|
@@ -420,7 +420,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
|
||||
assert trade.open_order_id is None
|
||||
# Open rate is not adjusted yet
|
||||
assert trade.open_rate == 1.99
|
||||
assert trade.stake_amount == 60
|
||||
assert pytest.approx(trade.stake_amount) == 60
|
||||
assert trade.stop_loss_pct == -0.1
|
||||
assert pytest.approx(trade.stop_loss) == 1.99 * (1 - 0.1 / leverage)
|
||||
assert pytest.approx(trade.initial_stop_loss) == 1.99 * (1 - 0.1 / leverage)
|
||||
@@ -446,7 +446,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
|
||||
assert len(trade.orders) == 4
|
||||
assert trade.open_order_id is not None
|
||||
assert trade.open_rate == 1.99
|
||||
assert trade.stake_amount == 60
|
||||
assert pytest.approx(trade.stake_amount) == 60
|
||||
assert trade.orders[-1].price == 1.95
|
||||
assert pytest.approx(trade.orders[-1].cost) == 120 * leverage
|
||||
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import logging
|
||||
import time
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
import time_machine
|
||||
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.enums import State
|
||||
from freqtrade.worker import Worker
|
||||
@@ -59,13 +62,58 @@ def test_throttle(mocker, default_conf, caplog) -> None:
|
||||
end = time.time()
|
||||
|
||||
assert result == 42
|
||||
assert end - start > 0.1
|
||||
assert 0.3 > end - start > 0.1
|
||||
assert log_has_re(r"Throttling with 'throttled_func\(\)': sleep for \d\.\d{2} s.*", caplog)
|
||||
|
||||
result = worker._throttle(throttled_func, throttle_secs=-1)
|
||||
assert result == 42
|
||||
|
||||
|
||||
def test_throttle_sleep_time(mocker, default_conf, caplog) -> None:
|
||||
|
||||
caplog.set_level(logging.DEBUG)
|
||||
worker = get_patched_worker(mocker, default_conf)
|
||||
sleep_mock = mocker.patch("freqtrade.worker.Worker._sleep")
|
||||
with time_machine.travel("2022-09-01 05:00:00 +00:00") as t:
|
||||
def throttled_func(x=1):
|
||||
t.shift(timedelta(seconds=x))
|
||||
return 42
|
||||
|
||||
assert worker._throttle(throttled_func, throttle_secs=5) == 42
|
||||
# This moves the clock by 1 second
|
||||
assert sleep_mock.call_count == 1
|
||||
assert 3.8 < sleep_mock.call_args[0][0] < 4.1
|
||||
|
||||
sleep_mock.reset_mock()
|
||||
# This moves the clock by 1 second
|
||||
assert worker._throttle(throttled_func, throttle_secs=10) == 42
|
||||
assert sleep_mock.call_count == 1
|
||||
assert 8.8 < sleep_mock.call_args[0][0] < 9.1
|
||||
|
||||
sleep_mock.reset_mock()
|
||||
# This moves the clock by 5 second, so we only throttle by 5s
|
||||
assert worker._throttle(throttled_func, throttle_secs=10, x=5) == 42
|
||||
assert sleep_mock.call_count == 1
|
||||
assert 4.8 < sleep_mock.call_args[0][0] < 5.1
|
||||
|
||||
t.move_to("2022-09-01 05:01:00 +00:00")
|
||||
sleep_mock.reset_mock()
|
||||
# Throttle for more than 5m (1 timeframe)
|
||||
assert worker._throttle(throttled_func, throttle_secs=400, x=5) == 42
|
||||
assert sleep_mock.call_count == 1
|
||||
assert 394.8 < sleep_mock.call_args[0][0] < 395.1
|
||||
|
||||
t.move_to("2022-09-01 05:01:00 +00:00")
|
||||
|
||||
sleep_mock.reset_mock()
|
||||
# Throttle for more than 5m (1 timeframe)
|
||||
assert worker._throttle(throttled_func, throttle_secs=400, timeframe='5m',
|
||||
timeframe_offset=0.4, x=5) == 42
|
||||
assert sleep_mock.call_count == 1
|
||||
# 300 (5m) - 60 (1m - see set time above) - 5 (duration of throttled_func) = 235
|
||||
assert 235.2 < sleep_mock.call_args[0][0] < 235.6
|
||||
|
||||
|
||||
def test_throttle_with_assets(mocker, default_conf) -> None:
|
||||
def throttled_func(nb_assets=-1):
|
||||
return nb_assets
|
||||
|
Reference in New Issue
Block a user