Move tests out of freqtrade module
This commit is contained in:
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
133
tests/config_test_comments.json
Normal file
133
tests/config_test_comments.json
Normal file
@@ -0,0 +1,133 @@
|
||||
{
|
||||
/* Single-line C-style comment */
|
||||
"max_open_trades": 3,
|
||||
/*
|
||||
* Multi-line C-style comment
|
||||
*/
|
||||
"stake_currency": "BTC",
|
||||
"stake_amount": 0.05,
|
||||
"fiat_display_currency": "USD", // C++-style comment
|
||||
"amount_reserve_percent" : 0.05, // And more, tabs before this comment
|
||||
"dry_run": false,
|
||||
"ticker_interval": "5m",
|
||||
"trailing_stop": false,
|
||||
"trailing_stop_positive": 0.005,
|
||||
"trailing_stop_positive_offset": 0.0051,
|
||||
"trailing_only_offset_is_reached": false,
|
||||
"minimal_roi": {
|
||||
"40": 0.0,
|
||||
"30": 0.01,
|
||||
"20": 0.02,
|
||||
"0": 0.04
|
||||
},
|
||||
"stoploss": -0.10,
|
||||
"unfilledtimeout": {
|
||||
"buy": 10,
|
||||
"sell": 30, // Trailing comma should also be accepted now
|
||||
},
|
||||
"bid_strategy": {
|
||||
"use_order_book": false,
|
||||
"ask_last_balance": 0.0,
|
||||
"order_book_top": 1,
|
||||
"check_depth_of_market": {
|
||||
"enabled": false,
|
||||
"bids_to_ask_delta": 1
|
||||
}
|
||||
},
|
||||
"ask_strategy":{
|
||||
"use_order_book": false,
|
||||
"order_book_min": 1,
|
||||
"order_book_max": 9
|
||||
},
|
||||
"order_types": {
|
||||
"buy": "limit",
|
||||
"sell": "limit",
|
||||
"stoploss": "market",
|
||||
"stoploss_on_exchange": false,
|
||||
"stoploss_on_exchange_interval": 60
|
||||
},
|
||||
"order_time_in_force": {
|
||||
"buy": "gtc",
|
||||
"sell": "gtc"
|
||||
},
|
||||
"pairlist": {
|
||||
"method": "VolumePairList",
|
||||
"config": {
|
||||
"number_assets": 20,
|
||||
"sort_key": "quoteVolume",
|
||||
"precision_filter": false
|
||||
}
|
||||
},
|
||||
"exchange": {
|
||||
"name": "bittrex",
|
||||
"sandbox": false,
|
||||
"key": "your_exchange_key",
|
||||
"secret": "your_exchange_secret",
|
||||
"password": "",
|
||||
"ccxt_config": {"enableRateLimit": true},
|
||||
"ccxt_async_config": {
|
||||
"enableRateLimit": false,
|
||||
"rateLimit": 500,
|
||||
"aiohttp_trust_env": false
|
||||
},
|
||||
"pair_whitelist": [
|
||||
"ETH/BTC",
|
||||
"LTC/BTC",
|
||||
"ETC/BTC",
|
||||
"DASH/BTC",
|
||||
"ZEC/BTC",
|
||||
"XLM/BTC",
|
||||
"NXT/BTC",
|
||||
"POWR/BTC",
|
||||
"ADA/BTC",
|
||||
"XMR/BTC"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"DOGE/BTC"
|
||||
],
|
||||
"outdated_offset": 5,
|
||||
"markets_refresh_interval": 60
|
||||
},
|
||||
"edge": {
|
||||
"enabled": false,
|
||||
"process_throttle_secs": 3600,
|
||||
"calculate_since_number_of_days": 7,
|
||||
"capital_available_percentage": 0.5,
|
||||
"allowed_risk": 0.01,
|
||||
"stoploss_range_min": -0.01,
|
||||
"stoploss_range_max": -0.1,
|
||||
"stoploss_range_step": -0.01,
|
||||
"minimum_winrate": 0.60,
|
||||
"minimum_expectancy": 0.20,
|
||||
"min_trade_number": 10,
|
||||
"max_trade_duration_minute": 1440,
|
||||
"remove_pumps": false
|
||||
},
|
||||
"experimental": {
|
||||
"use_sell_signal": false,
|
||||
"sell_profit_only": false,
|
||||
"ignore_roi_if_buy_signal": false
|
||||
},
|
||||
"telegram": {
|
||||
// We can now comment out some settings
|
||||
// "enabled": true,
|
||||
"enabled": false,
|
||||
"token": "your_telegram_token",
|
||||
"chat_id": "your_telegram_chat_id"
|
||||
},
|
||||
"api_server": {
|
||||
"enabled": false,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": 8080,
|
||||
"username": "freqtrader",
|
||||
"password": "SuperSecurePassword"
|
||||
},
|
||||
"db_url": "sqlite:///tradesv3.sqlite",
|
||||
"initial_state": "running",
|
||||
"forcebuy_enable": false,
|
||||
"internals": {
|
||||
"process_throttle_secs": 5
|
||||
},
|
||||
"strategy": "DefaultStrategy",
|
||||
"strategy_path": "user_data/strategies/"
|
||||
}
|
1056
tests/conftest.py
Normal file
1056
tests/conftest.py
Normal file
File diff suppressed because it is too large
Load Diff
0
tests/data/__init__.py
Normal file
0
tests/data/__init__.py
Normal file
136
tests/data/test_btanalysis.py
Normal file
136
tests/data/test_btanalysis.py
Normal file
@@ -0,0 +1,136 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from arrow import Arrow
|
||||
from pandas import DataFrame, to_datetime
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data.btanalysis import (BT_DATA_COLUMNS,
|
||||
combine_tickers_with_mean,
|
||||
create_cum_profit,
|
||||
extract_trades_of_period,
|
||||
load_backtest_data, load_trades,
|
||||
load_trades_from_db)
|
||||
from freqtrade.data.history import load_data, load_pair_history
|
||||
from freqtrade.tests.test_persistence import create_mock_trades
|
||||
|
||||
|
||||
def test_load_backtest_data(testdatadir):
|
||||
|
||||
filename = testdatadir / "backtest-result_test.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
assert isinstance(bt_data, DataFrame)
|
||||
assert list(bt_data.columns) == BT_DATA_COLUMNS + ["profitabs"]
|
||||
assert len(bt_data) == 179
|
||||
|
||||
# Test loading from string (must yield same result)
|
||||
bt_data2 = load_backtest_data(str(filename))
|
||||
assert bt_data.equals(bt_data2)
|
||||
|
||||
with pytest.raises(ValueError, match=r"File .* does not exist\."):
|
||||
load_backtest_data(str("filename") + "nofile")
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_load_trades_db(default_conf, fee, mocker):
|
||||
|
||||
create_mock_trades(fee)
|
||||
# remove init so it does not init again
|
||||
init_mock = mocker.patch('freqtrade.persistence.init', MagicMock())
|
||||
|
||||
trades = load_trades_from_db(db_url=default_conf['db_url'])
|
||||
assert init_mock.call_count == 1
|
||||
assert len(trades) == 3
|
||||
assert isinstance(trades, DataFrame)
|
||||
assert "pair" in trades.columns
|
||||
assert "open_time" in trades.columns
|
||||
assert "profitperc" in trades.columns
|
||||
|
||||
for col in BT_DATA_COLUMNS:
|
||||
if col not in ['index', 'open_at_end']:
|
||||
assert col in trades.columns
|
||||
|
||||
|
||||
def test_extract_trades_of_period(testdatadir):
|
||||
pair = "UNITTEST/BTC"
|
||||
timerange = TimeRange(None, 'line', 0, -1000)
|
||||
|
||||
data = load_pair_history(pair=pair, ticker_interval='1m',
|
||||
datadir=testdatadir, timerange=timerange)
|
||||
|
||||
# timerange = 2017-11-14 06:07 - 2017-11-14 22:58:00
|
||||
trades = DataFrame(
|
||||
{'pair': [pair, pair, pair, pair],
|
||||
'profit_percent': [0.0, 0.1, -0.2, -0.5],
|
||||
'profit_abs': [0.0, 1, -2, -5],
|
||||
'open_time': to_datetime([Arrow(2017, 11, 13, 15, 40, 0).datetime,
|
||||
Arrow(2017, 11, 14, 9, 41, 0).datetime,
|
||||
Arrow(2017, 11, 14, 14, 20, 0).datetime,
|
||||
Arrow(2017, 11, 15, 3, 40, 0).datetime,
|
||||
], utc=True
|
||||
),
|
||||
'close_time': to_datetime([Arrow(2017, 11, 13, 16, 40, 0).datetime,
|
||||
Arrow(2017, 11, 14, 10, 41, 0).datetime,
|
||||
Arrow(2017, 11, 14, 15, 25, 0).datetime,
|
||||
Arrow(2017, 11, 15, 3, 55, 0).datetime,
|
||||
], utc=True)
|
||||
})
|
||||
trades1 = extract_trades_of_period(data, trades)
|
||||
# First and last trade are dropped as they are out of range
|
||||
assert len(trades1) == 2
|
||||
assert trades1.iloc[0].open_time == Arrow(2017, 11, 14, 9, 41, 0).datetime
|
||||
assert trades1.iloc[0].close_time == Arrow(2017, 11, 14, 10, 41, 0).datetime
|
||||
assert trades1.iloc[-1].open_time == Arrow(2017, 11, 14, 14, 20, 0).datetime
|
||||
assert trades1.iloc[-1].close_time == Arrow(2017, 11, 14, 15, 25, 0).datetime
|
||||
|
||||
|
||||
def test_load_trades(default_conf, mocker):
|
||||
db_mock = mocker.patch("freqtrade.data.btanalysis.load_trades_from_db", MagicMock())
|
||||
bt_mock = mocker.patch("freqtrade.data.btanalysis.load_backtest_data", MagicMock())
|
||||
|
||||
load_trades("DB",
|
||||
db_url=default_conf.get('db_url'),
|
||||
exportfilename=default_conf.get('exportfilename'),
|
||||
)
|
||||
|
||||
assert db_mock.call_count == 1
|
||||
assert bt_mock.call_count == 0
|
||||
|
||||
db_mock.reset_mock()
|
||||
bt_mock.reset_mock()
|
||||
default_conf['exportfilename'] = "testfile.json"
|
||||
load_trades("file",
|
||||
db_url=default_conf.get('db_url'),
|
||||
exportfilename=default_conf.get('exportfilename'),)
|
||||
|
||||
assert db_mock.call_count == 0
|
||||
assert bt_mock.call_count == 1
|
||||
|
||||
|
||||
def test_combine_tickers_with_mean(testdatadir):
|
||||
pairs = ["ETH/BTC", "XLM/BTC"]
|
||||
tickers = load_data(datadir=testdatadir,
|
||||
pairs=pairs,
|
||||
ticker_interval='5m'
|
||||
)
|
||||
df = combine_tickers_with_mean(tickers)
|
||||
assert isinstance(df, DataFrame)
|
||||
assert "ETH/BTC" in df.columns
|
||||
assert "XLM/BTC" in df.columns
|
||||
assert "mean" in df.columns
|
||||
|
||||
|
||||
def test_create_cum_profit(testdatadir):
|
||||
filename = testdatadir / "backtest-result_test.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
timerange = TimeRange.parse_timerange("20180110-20180112")
|
||||
|
||||
df = load_pair_history(pair="POWR/BTC", ticker_interval='5m',
|
||||
datadir=testdatadir, timerange=timerange)
|
||||
|
||||
cum_profits = create_cum_profit(df.set_index('date'),
|
||||
bt_data[bt_data["pair"] == 'POWR/BTC'],
|
||||
"cum_profits")
|
||||
assert "cum_profits" in cum_profits.columns
|
||||
assert cum_profits.iloc[0]['cum_profits'] == 0
|
||||
assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005
|
148
tests/data/test_converter.py
Normal file
148
tests/data/test_converter.py
Normal file
@@ -0,0 +1,148 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
import logging
|
||||
|
||||
from freqtrade.data.converter import parse_ticker_dataframe, ohlcv_fill_up_missing_data
|
||||
from freqtrade.data.history import load_pair_history, validate_backtest_data, get_timeframe
|
||||
from freqtrade.tests.conftest import log_has
|
||||
|
||||
|
||||
def test_dataframe_correct_columns(result):
|
||||
assert result.columns.tolist() == ['date', 'open', 'high', 'low', 'close', 'volume']
|
||||
|
||||
|
||||
def test_parse_ticker_dataframe(ticker_history_list, caplog):
|
||||
columns = ['date', 'open', 'high', 'low', 'close', 'volume']
|
||||
|
||||
caplog.set_level(logging.DEBUG)
|
||||
# Test file with BV data
|
||||
dataframe = parse_ticker_dataframe(ticker_history_list, '5m',
|
||||
pair="UNITTEST/BTC", fill_missing=True)
|
||||
assert dataframe.columns.tolist() == columns
|
||||
assert log_has('Parsing tickerlist to dataframe', caplog)
|
||||
|
||||
|
||||
def test_ohlcv_fill_up_missing_data(testdatadir, caplog):
|
||||
data = load_pair_history(datadir=testdatadir,
|
||||
ticker_interval='1m',
|
||||
refresh_pairs=False,
|
||||
pair='UNITTEST/BTC',
|
||||
fill_up_missing=False)
|
||||
caplog.set_level(logging.DEBUG)
|
||||
data2 = ohlcv_fill_up_missing_data(data, '1m', 'UNITTEST/BTC')
|
||||
assert len(data2) > len(data)
|
||||
# Column names should not change
|
||||
assert (data.columns == data2.columns).all()
|
||||
|
||||
assert log_has(f"Missing data fillup for UNITTEST/BTC: before: "
|
||||
f"{len(data)} - after: {len(data2)}", caplog)
|
||||
|
||||
# Test fillup actually fixes invalid backtest data
|
||||
min_date, max_date = get_timeframe({'UNITTEST/BTC': data})
|
||||
assert validate_backtest_data(data, 'UNITTEST/BTC', min_date, max_date, 1)
|
||||
assert not validate_backtest_data(data2, 'UNITTEST/BTC', min_date, max_date, 1)
|
||||
|
||||
|
||||
def test_ohlcv_fill_up_missing_data2(caplog):
|
||||
ticker_interval = '5m'
|
||||
ticks = [[
|
||||
1511686200000, # 8:50:00
|
||||
8.794e-05, # open
|
||||
8.948e-05, # high
|
||||
8.794e-05, # low
|
||||
8.88e-05, # close
|
||||
2255, # volume (in quote currency)
|
||||
],
|
||||
[
|
||||
1511686500000, # 8:55:00
|
||||
8.88e-05,
|
||||
8.942e-05,
|
||||
8.88e-05,
|
||||
8.893e-05,
|
||||
9911,
|
||||
],
|
||||
[
|
||||
1511687100000, # 9:05:00
|
||||
8.891e-05,
|
||||
8.893e-05,
|
||||
8.875e-05,
|
||||
8.877e-05,
|
||||
2251
|
||||
],
|
||||
[
|
||||
1511687400000, # 9:10:00
|
||||
8.877e-05,
|
||||
8.883e-05,
|
||||
8.895e-05,
|
||||
8.817e-05,
|
||||
123551
|
||||
]
|
||||
]
|
||||
|
||||
# Generate test-data without filling missing
|
||||
data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC", fill_missing=False)
|
||||
assert len(data) == 3
|
||||
caplog.set_level(logging.DEBUG)
|
||||
data2 = ohlcv_fill_up_missing_data(data, ticker_interval, "UNITTEST/BTC")
|
||||
assert len(data2) == 4
|
||||
# 3rd candle has been filled
|
||||
row = data2.loc[2, :]
|
||||
assert row['volume'] == 0
|
||||
# close shoult match close of previous candle
|
||||
assert row['close'] == data.loc[1, 'close']
|
||||
assert row['open'] == row['close']
|
||||
assert row['high'] == row['close']
|
||||
assert row['low'] == row['close']
|
||||
# Column names should not change
|
||||
assert (data.columns == data2.columns).all()
|
||||
|
||||
assert log_has(f"Missing data fillup for UNITTEST/BTC: before: "
|
||||
f"{len(data)} - after: {len(data2)}", caplog)
|
||||
|
||||
|
||||
def test_ohlcv_drop_incomplete(caplog):
|
||||
ticker_interval = '1d'
|
||||
ticks = [[
|
||||
1559750400000, # 2019-06-04
|
||||
8.794e-05, # open
|
||||
8.948e-05, # high
|
||||
8.794e-05, # low
|
||||
8.88e-05, # close
|
||||
2255, # volume (in quote currency)
|
||||
],
|
||||
[
|
||||
1559836800000, # 2019-06-05
|
||||
8.88e-05,
|
||||
8.942e-05,
|
||||
8.88e-05,
|
||||
8.893e-05,
|
||||
9911,
|
||||
],
|
||||
[
|
||||
1559923200000, # 2019-06-06
|
||||
8.891e-05,
|
||||
8.893e-05,
|
||||
8.875e-05,
|
||||
8.877e-05,
|
||||
2251
|
||||
],
|
||||
[
|
||||
1560009600000, # 2019-06-07
|
||||
8.877e-05,
|
||||
8.883e-05,
|
||||
8.895e-05,
|
||||
8.817e-05,
|
||||
123551
|
||||
]
|
||||
]
|
||||
caplog.set_level(logging.DEBUG)
|
||||
data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC",
|
||||
fill_missing=False, drop_incomplete=False)
|
||||
assert len(data) == 4
|
||||
assert not log_has("Dropping last candle", caplog)
|
||||
|
||||
# Drop last candle
|
||||
data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC",
|
||||
fill_missing=False, drop_incomplete=True)
|
||||
assert len(data) == 3
|
||||
|
||||
assert log_has("Dropping last candle", caplog)
|
123
tests/data/test_dataprovider.py
Normal file
123
tests/data/test_dataprovider.py
Normal file
@@ -0,0 +1,123 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.state import RunMode
|
||||
from freqtrade.tests.conftest import get_patched_exchange
|
||||
|
||||
|
||||
def test_ohlcv(mocker, default_conf, ticker_history):
|
||||
default_conf["runmode"] = RunMode.DRY_RUN
|
||||
ticker_interval = default_conf["ticker_interval"]
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history
|
||||
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history
|
||||
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
assert dp.runmode == RunMode.DRY_RUN
|
||||
assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", ticker_interval))
|
||||
assert isinstance(dp.ohlcv("UNITTEST/BTC", ticker_interval), DataFrame)
|
||||
assert dp.ohlcv("UNITTEST/BTC", ticker_interval) is not ticker_history
|
||||
assert dp.ohlcv("UNITTEST/BTC", ticker_interval, copy=False) is ticker_history
|
||||
assert not dp.ohlcv("UNITTEST/BTC", ticker_interval).empty
|
||||
assert dp.ohlcv("NONESENSE/AAA", ticker_interval).empty
|
||||
|
||||
# Test with and without parameter
|
||||
assert dp.ohlcv("UNITTEST/BTC", ticker_interval).equals(dp.ohlcv("UNITTEST/BTC"))
|
||||
|
||||
default_conf["runmode"] = RunMode.LIVE
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
assert dp.runmode == RunMode.LIVE
|
||||
assert isinstance(dp.ohlcv("UNITTEST/BTC", ticker_interval), DataFrame)
|
||||
|
||||
default_conf["runmode"] = RunMode.BACKTEST
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
assert dp.runmode == RunMode.BACKTEST
|
||||
assert dp.ohlcv("UNITTEST/BTC", ticker_interval).empty
|
||||
|
||||
|
||||
def test_historic_ohlcv(mocker, default_conf, ticker_history):
|
||||
historymock = MagicMock(return_value=ticker_history)
|
||||
mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock)
|
||||
|
||||
dp = DataProvider(default_conf, None)
|
||||
data = dp.historic_ohlcv("UNITTEST/BTC", "5m")
|
||||
assert isinstance(data, DataFrame)
|
||||
assert historymock.call_count == 1
|
||||
assert historymock.call_args_list[0][1]["refresh_pairs"] is False
|
||||
assert historymock.call_args_list[0][1]["ticker_interval"] == "5m"
|
||||
|
||||
|
||||
def test_get_pair_dataframe(mocker, default_conf, ticker_history):
|
||||
default_conf["runmode"] = RunMode.DRY_RUN
|
||||
ticker_interval = default_conf["ticker_interval"]
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history
|
||||
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history
|
||||
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
assert dp.runmode == RunMode.DRY_RUN
|
||||
assert ticker_history.equals(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval))
|
||||
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame)
|
||||
assert dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval) is not ticker_history
|
||||
assert not dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval).empty
|
||||
assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
|
||||
|
||||
# Test with and without parameter
|
||||
assert dp.get_pair_dataframe("UNITTEST/BTC",
|
||||
ticker_interval).equals(dp.get_pair_dataframe("UNITTEST/BTC"))
|
||||
|
||||
default_conf["runmode"] = RunMode.LIVE
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
assert dp.runmode == RunMode.LIVE
|
||||
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame)
|
||||
assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
|
||||
|
||||
historymock = MagicMock(return_value=ticker_history)
|
||||
mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock)
|
||||
default_conf["runmode"] = RunMode.BACKTEST
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
assert dp.runmode == RunMode.BACKTEST
|
||||
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame)
|
||||
# assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
|
||||
|
||||
|
||||
def test_available_pairs(mocker, default_conf, ticker_history):
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
ticker_interval = default_conf["ticker_interval"]
|
||||
exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history
|
||||
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history
|
||||
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
assert len(dp.available_pairs) == 2
|
||||
assert dp.available_pairs == [
|
||||
("XRP/BTC", ticker_interval),
|
||||
("UNITTEST/BTC", ticker_interval),
|
||||
]
|
||||
|
||||
|
||||
def test_refresh(mocker, default_conf, ticker_history):
|
||||
refresh_mock = MagicMock()
|
||||
mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||
ticker_interval = default_conf["ticker_interval"]
|
||||
pairs = [("XRP/BTC", ticker_interval), ("UNITTEST/BTC", ticker_interval)]
|
||||
|
||||
pairs_non_trad = [("ETH/USDT", ticker_interval), ("BTC/TUSD", "1h")]
|
||||
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
dp.refresh(pairs)
|
||||
|
||||
assert refresh_mock.call_count == 1
|
||||
assert len(refresh_mock.call_args[0]) == 1
|
||||
assert len(refresh_mock.call_args[0][0]) == len(pairs)
|
||||
assert refresh_mock.call_args[0][0] == pairs
|
||||
|
||||
refresh_mock.reset_mock()
|
||||
dp.refresh(pairs, pairs_non_trad)
|
||||
assert refresh_mock.call_count == 1
|
||||
assert len(refresh_mock.call_args[0]) == 1
|
||||
assert len(refresh_mock.call_args[0][0]) == len(pairs) + len(pairs_non_trad)
|
||||
assert refresh_mock.call_args[0][0] == pairs + pairs_non_trad
|
602
tests/data/test_history.py
Normal file
602
tests/data/test_history.py
Normal file
@@ -0,0 +1,602 @@
|
||||
# pragma pylint: disable=missing-docstring, protected-access, C0103
|
||||
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from shutil import copyfile
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
import arrow
|
||||
import pytest
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data import history
|
||||
from freqtrade.data.history import (download_pair_history,
|
||||
load_cached_data_for_updating,
|
||||
load_tickerdata_file,
|
||||
refresh_backtest_ohlcv_data,
|
||||
trim_tickerlist)
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.misc import file_dump_json
|
||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||
from freqtrade.tests.conftest import get_patched_exchange, log_has, log_has_re, patch_exchange
|
||||
|
||||
# Change this if modifying UNITTEST/BTC testdatafile
|
||||
_BTC_UNITTEST_LENGTH = 13681
|
||||
|
||||
|
||||
def _backup_file(file: str, copy_file: bool = False) -> None:
|
||||
"""
|
||||
Backup existing file to avoid deleting the user file
|
||||
:param file: complete path to the file
|
||||
:param touch_file: create an empty file in replacement
|
||||
:return: None
|
||||
"""
|
||||
file_swp = file + '.swp'
|
||||
if os.path.isfile(file):
|
||||
os.rename(file, file_swp)
|
||||
|
||||
if copy_file:
|
||||
copyfile(file_swp, file)
|
||||
|
||||
|
||||
def _clean_test_file(file: str) -> None:
|
||||
"""
|
||||
Backup existing file to avoid deleting the user file
|
||||
:param file: complete path to the file
|
||||
:return: None
|
||||
"""
|
||||
file_swp = file + '.swp'
|
||||
# 1. Delete file from the test
|
||||
if os.path.isfile(file):
|
||||
os.remove(file)
|
||||
|
||||
# 2. Rollback to the initial file
|
||||
if os.path.isfile(file_swp):
|
||||
os.rename(file_swp, file)
|
||||
|
||||
|
||||
def test_load_data_30min_ticker(mocker, caplog, default_conf, testdatadir) -> None:
|
||||
ld = history.load_pair_history(pair='UNITTEST/BTC', ticker_interval='30m', datadir=testdatadir)
|
||||
assert isinstance(ld, DataFrame)
|
||||
assert not log_has(
|
||||
'Download history data for pair: "UNITTEST/BTC", interval: 30m '
|
||||
'and store in None.', caplog
|
||||
)
|
||||
|
||||
|
||||
def test_load_data_7min_ticker(mocker, caplog, default_conf, testdatadir) -> None:
|
||||
ld = history.load_pair_history(pair='UNITTEST/BTC', ticker_interval='7m', datadir=testdatadir)
|
||||
assert not isinstance(ld, DataFrame)
|
||||
assert ld is None
|
||||
assert log_has(
|
||||
'No history data for pair: "UNITTEST/BTC", interval: 7m. '
|
||||
'Use --refresh-pairs-cached option or `freqtrade download-data` '
|
||||
'script to download the data', caplog
|
||||
)
|
||||
|
||||
|
||||
def test_load_data_1min_ticker(ticker_history, mocker, caplog, testdatadir) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history)
|
||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
|
||||
_backup_file(file, copy_file=True)
|
||||
history.load_data(datadir=testdatadir, ticker_interval='1m', pairs=['UNITTEST/BTC'])
|
||||
assert os.path.isfile(file) is True
|
||||
assert not log_has(
|
||||
'Download history data for pair: "UNITTEST/BTC", interval: 1m '
|
||||
'and store in None.', caplog
|
||||
)
|
||||
_clean_test_file(file)
|
||||
|
||||
|
||||
def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog,
|
||||
default_conf, testdatadir) -> None:
|
||||
"""
|
||||
Test load_pair_history() with 1 min ticker
|
||||
"""
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history_list)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
||||
|
||||
_backup_file(file)
|
||||
# do not download a new pair if refresh_pairs isn't set
|
||||
history.load_pair_history(datadir=testdatadir,
|
||||
ticker_interval='1m',
|
||||
refresh_pairs=False,
|
||||
pair='MEME/BTC')
|
||||
assert os.path.isfile(file) is False
|
||||
assert log_has(
|
||||
'No history data for pair: "MEME/BTC", interval: 1m. '
|
||||
'Use --refresh-pairs-cached option or `freqtrade download-data` '
|
||||
'script to download the data', caplog
|
||||
)
|
||||
|
||||
# download a new pair if refresh_pairs is set
|
||||
history.load_pair_history(datadir=testdatadir,
|
||||
ticker_interval='1m',
|
||||
refresh_pairs=True,
|
||||
exchange=exchange,
|
||||
pair='MEME/BTC')
|
||||
assert os.path.isfile(file) is True
|
||||
assert log_has_re(
|
||||
'Download history data for pair: "MEME/BTC", interval: 1m '
|
||||
'and store in .*', caplog
|
||||
)
|
||||
with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'):
|
||||
history.load_pair_history(datadir=testdatadir,
|
||||
ticker_interval='1m',
|
||||
refresh_pairs=True,
|
||||
exchange=None,
|
||||
pair='MEME/BTC')
|
||||
_clean_test_file(file)
|
||||
|
||||
|
||||
def test_load_data_live(default_conf, mocker, caplog, testdatadir) -> None:
|
||||
refresh_mock = MagicMock()
|
||||
mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
|
||||
history.load_data(datadir=testdatadir, ticker_interval='5m',
|
||||
pairs=['UNITTEST/BTC', 'UNITTEST2/BTC'],
|
||||
live=True,
|
||||
exchange=exchange)
|
||||
assert refresh_mock.call_count == 1
|
||||
assert len(refresh_mock.call_args_list[0][0][0]) == 2
|
||||
assert log_has('Live: Downloading data for all defined pairs ...', caplog)
|
||||
|
||||
|
||||
def test_load_data_live_noexchange(default_conf, mocker, caplog, testdatadir) -> None:
|
||||
|
||||
with pytest.raises(OperationalException,
|
||||
match=r'Exchange needs to be initialized when using live data.'):
|
||||
history.load_data(datadir=testdatadir, ticker_interval='5m',
|
||||
pairs=['UNITTEST/BTC', 'UNITTEST2/BTC'],
|
||||
exchange=None,
|
||||
live=True,
|
||||
)
|
||||
|
||||
|
||||
def test_testdata_path(testdatadir) -> None:
|
||||
assert str(Path('freqtrade') / 'tests' / 'testdata') in str(testdatadir)
|
||||
|
||||
|
||||
def test_load_cached_data_for_updating(mocker) -> None:
|
||||
datadir = Path(__file__).parent.parent.joinpath('testdata')
|
||||
|
||||
test_data = None
|
||||
test_filename = datadir.joinpath('UNITTEST_BTC-1m.json')
|
||||
with open(test_filename, "rt") as file:
|
||||
test_data = json.load(file)
|
||||
|
||||
# change now time to test 'line' cases
|
||||
# now = last cached item + 1 hour
|
||||
now_ts = test_data[-1][0] / 1000 + 60 * 60
|
||||
mocker.patch('arrow.utcnow', return_value=arrow.get(now_ts))
|
||||
|
||||
# timeframe starts earlier than the cached data
|
||||
# should fully update data
|
||||
timerange = TimeRange('date', None, test_data[0][0] / 1000 - 1, 0)
|
||||
data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange)
|
||||
assert data == []
|
||||
assert start_ts == test_data[0][0] - 1000
|
||||
|
||||
# same with 'line' timeframe
|
||||
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 120
|
||||
data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m',
|
||||
TimeRange(None, 'line', 0, -num_lines))
|
||||
assert data == []
|
||||
assert start_ts < test_data[0][0] - 1
|
||||
|
||||
# timeframe starts in the center of the cached data
|
||||
# should return the chached data w/o the last item
|
||||
timerange = TimeRange('date', None, test_data[0][0] / 1000 + 1, 0)
|
||||
data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange)
|
||||
assert data == test_data[:-1]
|
||||
assert test_data[-2][0] < start_ts < test_data[-1][0]
|
||||
|
||||
# same with 'line' timeframe
|
||||
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 30
|
||||
timerange = TimeRange(None, 'line', 0, -num_lines)
|
||||
data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange)
|
||||
assert data == test_data[:-1]
|
||||
assert test_data[-2][0] < start_ts < test_data[-1][0]
|
||||
|
||||
# timeframe starts after the chached data
|
||||
# should return the chached data w/o the last item
|
||||
timerange = TimeRange('date', None, test_data[-1][0] / 1000 + 1, 0)
|
||||
data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange)
|
||||
assert data == test_data[:-1]
|
||||
assert test_data[-2][0] < start_ts < test_data[-1][0]
|
||||
|
||||
# Try loading last 30 lines.
|
||||
# Not supported by load_cached_data_for_updating, we always need to get the full data.
|
||||
num_lines = 30
|
||||
timerange = TimeRange(None, 'line', 0, -num_lines)
|
||||
data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange)
|
||||
assert data == test_data[:-1]
|
||||
assert test_data[-2][0] < start_ts < test_data[-1][0]
|
||||
|
||||
# no timeframe is set
|
||||
# should return the chached data w/o the last item
|
||||
num_lines = 30
|
||||
timerange = TimeRange(None, 'line', 0, -num_lines)
|
||||
data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange)
|
||||
assert data == test_data[:-1]
|
||||
assert test_data[-2][0] < start_ts < test_data[-1][0]
|
||||
|
||||
# no datafile exist
|
||||
# should return timestamp start time
|
||||
timerange = TimeRange('date', None, now_ts - 10000, 0)
|
||||
data, start_ts = load_cached_data_for_updating(datadir, 'NONEXIST/BTC', '1m', timerange)
|
||||
assert data == []
|
||||
assert start_ts == (now_ts - 10000) * 1000
|
||||
|
||||
# same with 'line' timeframe
|
||||
num_lines = 30
|
||||
timerange = TimeRange(None, 'line', 0, -num_lines)
|
||||
data, start_ts = load_cached_data_for_updating(datadir, 'NONEXIST/BTC', '1m', timerange)
|
||||
assert data == []
|
||||
assert start_ts == (now_ts - num_lines * 60) * 1000
|
||||
|
||||
# no datafile exist, no timeframe is set
|
||||
# should return an empty array and None
|
||||
data, start_ts = load_cached_data_for_updating(datadir, 'NONEXIST/BTC', '1m', None)
|
||||
assert data == []
|
||||
assert start_ts is None
|
||||
|
||||
|
||||
def test_download_pair_history(ticker_history_list, mocker, default_conf, testdatadir) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history_list)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
||||
file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json')
|
||||
file2_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'CFI_BTC-1m.json')
|
||||
file2_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'CFI_BTC-5m.json')
|
||||
|
||||
_backup_file(file1_1)
|
||||
_backup_file(file1_5)
|
||||
_backup_file(file2_1)
|
||||
_backup_file(file2_5)
|
||||
|
||||
assert os.path.isfile(file1_1) is False
|
||||
assert os.path.isfile(file2_1) is False
|
||||
|
||||
assert download_pair_history(datadir=testdatadir, exchange=exchange,
|
||||
pair='MEME/BTC',
|
||||
ticker_interval='1m')
|
||||
assert download_pair_history(datadir=testdatadir, exchange=exchange,
|
||||
pair='CFI/BTC',
|
||||
ticker_interval='1m')
|
||||
assert not exchange._pairs_last_refresh_time
|
||||
assert os.path.isfile(file1_1) is True
|
||||
assert os.path.isfile(file2_1) is True
|
||||
|
||||
# clean files freshly downloaded
|
||||
_clean_test_file(file1_1)
|
||||
_clean_test_file(file2_1)
|
||||
|
||||
assert os.path.isfile(file1_5) is False
|
||||
assert os.path.isfile(file2_5) is False
|
||||
|
||||
assert download_pair_history(datadir=testdatadir, exchange=exchange,
|
||||
pair='MEME/BTC',
|
||||
ticker_interval='5m')
|
||||
assert download_pair_history(datadir=testdatadir, exchange=exchange,
|
||||
pair='CFI/BTC',
|
||||
ticker_interval='5m')
|
||||
assert not exchange._pairs_last_refresh_time
|
||||
assert os.path.isfile(file1_5) is True
|
||||
assert os.path.isfile(file2_5) is True
|
||||
|
||||
# clean files freshly downloaded
|
||||
_clean_test_file(file1_5)
|
||||
_clean_test_file(file2_5)
|
||||
|
||||
|
||||
def test_download_pair_history2(mocker, default_conf, testdatadir) -> None:
|
||||
tick = [
|
||||
[1509836520000, 0.00162008, 0.00162008, 0.00162008, 0.00162008, 108.14853839],
|
||||
[1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199]
|
||||
]
|
||||
json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", ticker_interval='1m')
|
||||
download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", ticker_interval='3m')
|
||||
assert json_dump_mock.call_count == 2
|
||||
|
||||
|
||||
def test_download_backtesting_data_exception(ticker_history, mocker, caplog,
|
||||
default_conf, testdatadir) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv',
|
||||
side_effect=Exception('File Error'))
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
|
||||
file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
||||
file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json')
|
||||
_backup_file(file1_1)
|
||||
_backup_file(file1_5)
|
||||
|
||||
assert not download_pair_history(datadir=testdatadir, exchange=exchange,
|
||||
pair='MEME/BTC',
|
||||
ticker_interval='1m')
|
||||
# clean files freshly downloaded
|
||||
_clean_test_file(file1_1)
|
||||
_clean_test_file(file1_5)
|
||||
assert log_has(
|
||||
'Failed to download history data for pair: "MEME/BTC", interval: 1m. '
|
||||
'Error: File Error', caplog
|
||||
)
|
||||
|
||||
|
||||
def test_load_tickerdata_file(testdatadir) -> None:
|
||||
# 7 does not exist in either format.
|
||||
assert not load_tickerdata_file(testdatadir, 'UNITTEST/BTC', '7m')
|
||||
# 1 exists only as a .json
|
||||
tickerdata = load_tickerdata_file(testdatadir, 'UNITTEST/BTC', '1m')
|
||||
assert _BTC_UNITTEST_LENGTH == len(tickerdata)
|
||||
# 8 .json is empty and will fail if it's loaded. .json.gz is a copy of 1.json
|
||||
tickerdata = load_tickerdata_file(testdatadir, 'UNITTEST/BTC', '8m')
|
||||
assert _BTC_UNITTEST_LENGTH == len(tickerdata)
|
||||
|
||||
|
||||
def test_load_partial_missing(testdatadir, caplog) -> None:
|
||||
# Make sure we start fresh - test missing data at start
|
||||
start = arrow.get('2018-01-01T00:00:00')
|
||||
end = arrow.get('2018-01-11T00:00:00')
|
||||
tickerdata = history.load_data(testdatadir, '5m', ['UNITTEST/BTC'],
|
||||
refresh_pairs=False,
|
||||
timerange=TimeRange('date', 'date',
|
||||
start.timestamp, end.timestamp))
|
||||
# timedifference in 5 minutes
|
||||
td = ((end - start).total_seconds() // 60 // 5) + 1
|
||||
assert td != len(tickerdata['UNITTEST/BTC'])
|
||||
start_real = tickerdata['UNITTEST/BTC'].iloc[0, 0]
|
||||
assert log_has(f'Missing data at start for pair '
|
||||
f'UNITTEST/BTC, data starts at {start_real.strftime("%Y-%m-%d %H:%M:%S")}',
|
||||
caplog)
|
||||
# Make sure we start fresh - test missing data at end
|
||||
caplog.clear()
|
||||
start = arrow.get('2018-01-10T00:00:00')
|
||||
end = arrow.get('2018-02-20T00:00:00')
|
||||
tickerdata = history.load_data(datadir=testdatadir, ticker_interval='5m',
|
||||
pairs=['UNITTEST/BTC'], refresh_pairs=False,
|
||||
timerange=TimeRange('date', 'date',
|
||||
start.timestamp, end.timestamp))
|
||||
# timedifference in 5 minutes
|
||||
td = ((end - start).total_seconds() // 60 // 5) + 1
|
||||
assert td != len(tickerdata['UNITTEST/BTC'])
|
||||
# Shift endtime with +5 - as last candle is dropped (partial candle)
|
||||
end_real = arrow.get(tickerdata['UNITTEST/BTC'].iloc[-1, 0]).shift(minutes=5)
|
||||
assert log_has(f'Missing data at end for pair '
|
||||
f'UNITTEST/BTC, data ends at {end_real.strftime("%Y-%m-%d %H:%M:%S")}',
|
||||
caplog)
|
||||
|
||||
|
||||
def test_init(default_conf, mocker) -> None:
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
assert {} == history.load_data(
|
||||
datadir='',
|
||||
exchange=exchange,
|
||||
pairs=[],
|
||||
refresh_pairs=True,
|
||||
ticker_interval=default_conf['ticker_interval']
|
||||
)
|
||||
|
||||
|
||||
def test_trim_tickerlist() -> None:
|
||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
|
||||
with open(file) as data_file:
|
||||
ticker_list = json.load(data_file)
|
||||
ticker_list_len = len(ticker_list)
|
||||
|
||||
# Test the pattern ^(-\d+)$
|
||||
# This pattern uses the latest N elements
|
||||
timerange = TimeRange(None, 'line', 0, -5)
|
||||
ticker = trim_tickerlist(ticker_list, timerange)
|
||||
ticker_len = len(ticker)
|
||||
|
||||
assert ticker_len == 5
|
||||
assert ticker_list[0] is not ticker[0] # The first element should be different
|
||||
assert ticker_list[-1] is ticker[-1] # The last element must be the same
|
||||
|
||||
# Test the pattern ^(\d+)-$
|
||||
# This pattern keep X element from the end
|
||||
timerange = TimeRange('line', None, 5, 0)
|
||||
ticker = trim_tickerlist(ticker_list, timerange)
|
||||
ticker_len = len(ticker)
|
||||
|
||||
assert ticker_len == 5
|
||||
assert ticker_list[0] is ticker[0] # The first element must be the same
|
||||
assert ticker_list[-1] is not ticker[-1] # The last element should be different
|
||||
|
||||
# Test the pattern ^(\d+)-(\d+)$
|
||||
# This pattern extract a window
|
||||
timerange = TimeRange('index', 'index', 5, 10)
|
||||
ticker = trim_tickerlist(ticker_list, timerange)
|
||||
ticker_len = len(ticker)
|
||||
|
||||
assert ticker_len == 5
|
||||
assert ticker_list[0] is not ticker[0] # The first element should be different
|
||||
assert ticker_list[5] is ticker[0] # The list starts at the index 5
|
||||
assert ticker_list[9] is ticker[-1] # The list ends at the index 9 (5 elements)
|
||||
|
||||
# Test the pattern ^(\d{8})-(\d{8})$
|
||||
# This pattern extract a window between the dates
|
||||
timerange = TimeRange('date', 'date', ticker_list[5][0] / 1000, ticker_list[10][0] / 1000 - 1)
|
||||
ticker = trim_tickerlist(ticker_list, timerange)
|
||||
ticker_len = len(ticker)
|
||||
|
||||
assert ticker_len == 5
|
||||
assert ticker_list[0] is not ticker[0] # The first element should be different
|
||||
assert ticker_list[5] is ticker[0] # The list starts at the index 5
|
||||
assert ticker_list[9] is ticker[-1] # The list ends at the index 9 (5 elements)
|
||||
|
||||
# Test the pattern ^-(\d{8})$
|
||||
# This pattern extracts elements from the start to the date
|
||||
timerange = TimeRange(None, 'date', 0, ticker_list[10][0] / 1000 - 1)
|
||||
ticker = trim_tickerlist(ticker_list, timerange)
|
||||
ticker_len = len(ticker)
|
||||
|
||||
assert ticker_len == 10
|
||||
assert ticker_list[0] is ticker[0] # The start of the list is included
|
||||
assert ticker_list[9] is ticker[-1] # The element 10 is not included
|
||||
|
||||
# Test the pattern ^(\d{8})-$
|
||||
# This pattern extracts elements from the date to now
|
||||
timerange = TimeRange('date', None, ticker_list[10][0] / 1000 - 1, None)
|
||||
ticker = trim_tickerlist(ticker_list, timerange)
|
||||
ticker_len = len(ticker)
|
||||
|
||||
assert ticker_len == ticker_list_len - 10
|
||||
assert ticker_list[10] is ticker[0] # The first element is element #10
|
||||
assert ticker_list[-1] is ticker[-1] # The last element is the same
|
||||
|
||||
# Test a wrong pattern
|
||||
# This pattern must return the list unchanged
|
||||
timerange = TimeRange(None, None, None, 5)
|
||||
ticker = trim_tickerlist(ticker_list, timerange)
|
||||
ticker_len = len(ticker)
|
||||
|
||||
assert ticker_list_len == ticker_len
|
||||
|
||||
# Test invalid timerange (start after stop)
|
||||
timerange = TimeRange('index', 'index', 10, 5)
|
||||
with pytest.raises(ValueError, match=r'The timerange .* is incorrect'):
|
||||
trim_tickerlist(ticker_list, timerange)
|
||||
|
||||
assert ticker_list_len == ticker_len
|
||||
|
||||
# passing empty list
|
||||
timerange = TimeRange(None, None, None, 5)
|
||||
ticker = trim_tickerlist([], timerange)
|
||||
assert 0 == len(ticker)
|
||||
assert not ticker
|
||||
|
||||
|
||||
def test_file_dump_json_tofile() -> None:
|
||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata',
|
||||
'test_{id}.json'.format(id=str(uuid.uuid4())))
|
||||
data = {'bar': 'foo'}
|
||||
|
||||
# check the file we will create does not exist
|
||||
assert os.path.isfile(file) is False
|
||||
|
||||
# Create the Json file
|
||||
file_dump_json(file, data)
|
||||
|
||||
# Check the file was create
|
||||
assert os.path.isfile(file) is True
|
||||
|
||||
# Open the Json file created and test the data is in it
|
||||
with open(file) as data_file:
|
||||
json_from_file = json.load(data_file)
|
||||
|
||||
assert 'bar' in json_from_file
|
||||
assert json_from_file['bar'] == 'foo'
|
||||
|
||||
# Remove the file
|
||||
_clean_test_file(file)
|
||||
|
||||
|
||||
def test_get_timeframe(default_conf, mocker, testdatadir) -> None:
|
||||
patch_exchange(mocker)
|
||||
strategy = DefaultStrategy(default_conf)
|
||||
|
||||
data = strategy.tickerdata_to_dataframe(
|
||||
history.load_data(
|
||||
datadir=testdatadir,
|
||||
ticker_interval='1m',
|
||||
pairs=['UNITTEST/BTC']
|
||||
)
|
||||
)
|
||||
min_date, max_date = history.get_timeframe(data)
|
||||
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
|
||||
assert max_date.isoformat() == '2017-11-14T22:58:00+00:00'
|
||||
|
||||
|
||||
def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) -> None:
|
||||
patch_exchange(mocker)
|
||||
strategy = DefaultStrategy(default_conf)
|
||||
|
||||
data = strategy.tickerdata_to_dataframe(
|
||||
history.load_data(
|
||||
datadir=testdatadir,
|
||||
ticker_interval='1m',
|
||||
pairs=['UNITTEST/BTC'],
|
||||
fill_up_missing=False
|
||||
)
|
||||
)
|
||||
min_date, max_date = history.get_timeframe(data)
|
||||
caplog.clear()
|
||||
assert history.validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC',
|
||||
min_date, max_date, timeframe_to_minutes('1m'))
|
||||
assert len(caplog.record_tuples) == 1
|
||||
assert log_has(
|
||||
"UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values",
|
||||
caplog)
|
||||
|
||||
|
||||
def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> None:
|
||||
patch_exchange(mocker)
|
||||
strategy = DefaultStrategy(default_conf)
|
||||
|
||||
timerange = TimeRange('index', 'index', 200, 250)
|
||||
data = strategy.tickerdata_to_dataframe(
|
||||
history.load_data(
|
||||
datadir=testdatadir,
|
||||
ticker_interval='5m',
|
||||
pairs=['UNITTEST/BTC'],
|
||||
timerange=timerange
|
||||
)
|
||||
)
|
||||
|
||||
min_date, max_date = history.get_timeframe(data)
|
||||
caplog.clear()
|
||||
assert not history.validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC',
|
||||
min_date, max_date, timeframe_to_minutes('5m'))
|
||||
assert len(caplog.record_tuples) == 0
|
||||
|
||||
|
||||
def test_refresh_backtest_ohlcv_data(mocker, default_conf, markets, caplog, testdatadir):
|
||||
dl_mock = mocker.patch('freqtrade.data.history.download_pair_history', MagicMock())
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
|
||||
)
|
||||
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
||||
mocker.patch.object(Path, "unlink", MagicMock())
|
||||
|
||||
ex = get_patched_exchange(mocker, default_conf)
|
||||
timerange = TimeRange.parse_timerange("20190101-20190102")
|
||||
refresh_backtest_ohlcv_data(exchange=ex, pairs=["ETH/BTC", "XRP/BTC"],
|
||||
timeframes=["1m", "5m"], dl_path=testdatadir,
|
||||
timerange=timerange, erase=True
|
||||
)
|
||||
|
||||
assert dl_mock.call_count == 4
|
||||
assert dl_mock.call_args[1]['timerange'].starttype == 'date'
|
||||
|
||||
assert log_has("Downloading pair ETH/BTC, interval 1m.", caplog)
|
||||
|
||||
|
||||
def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir):
|
||||
dl_mock = mocker.patch('freqtrade.data.history.download_pair_history', MagicMock())
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
|
||||
)
|
||||
ex = get_patched_exchange(mocker, default_conf)
|
||||
timerange = TimeRange.parse_timerange("20190101-20190102")
|
||||
unav_pairs = refresh_backtest_ohlcv_data(exchange=ex, pairs=["ETH/BTC", "XRP/BTC"],
|
||||
timeframes=["1m", "5m"],
|
||||
dl_path=testdatadir,
|
||||
timerange=timerange, erase=False
|
||||
)
|
||||
|
||||
assert dl_mock.call_count == 0
|
||||
assert "ETH/BTC" in unav_pairs
|
||||
assert "XRP/BTC" in unav_pairs
|
||||
assert log_has("Skipping pair ETH/BTC...", caplog)
|
0
tests/edge/__init__.py
Normal file
0
tests/edge/__init__.py
Normal file
405
tests/edge/test_edge.py
Normal file
405
tests/edge/test_edge.py
Normal file
@@ -0,0 +1,405 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103, C0330
|
||||
# pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments
|
||||
|
||||
import logging
|
||||
import math
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import arrow
|
||||
import numpy as np
|
||||
import pytest
|
||||
from pandas import DataFrame, to_datetime
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.data.converter import parse_ticker_dataframe
|
||||
from freqtrade.edge import Edge, PairInfo
|
||||
from freqtrade.strategy.interface import SellType
|
||||
from freqtrade.tests.conftest import get_patched_freqtradebot, log_has
|
||||
from freqtrade.tests.optimize import (BTContainer, BTrade,
|
||||
_build_backtest_dataframe,
|
||||
_get_frame_time_from_offset)
|
||||
|
||||
# Cases to be tested:
|
||||
# 1) Open trade should be removed from the end
|
||||
# 2) Two complete trades within dataframe (with sell hit for all)
|
||||
# 3) Entered, sl 1%, candle drops 8% => Trade closed, 1% loss
|
||||
# 4) Entered, sl 3%, candle drops 4%, recovers to 1% => Trade closed, 3% loss
|
||||
# 5) Stoploss and sell are hit. should sell on stoploss
|
||||
####################################################################
|
||||
|
||||
ticker_start_time = arrow.get(2018, 10, 3)
|
||||
ticker_interval_in_minute = 60
|
||||
_ohlc = {'date': 0, 'buy': 1, 'open': 2, 'high': 3, 'low': 4, 'close': 5, 'sell': 6, 'volume': 7}
|
||||
|
||||
# Helpers for this test file
|
||||
|
||||
|
||||
def _validate_ohlc(buy_ohlc_sell_matrice):
|
||||
for index, ohlc in enumerate(buy_ohlc_sell_matrice):
|
||||
# if not high < open < low or not high < close < low
|
||||
if not ohlc[3] >= ohlc[2] >= ohlc[4] or not ohlc[3] >= ohlc[5] >= ohlc[4]:
|
||||
raise Exception('Line ' + str(index + 1) + ' of ohlc has invalid values!')
|
||||
return True
|
||||
|
||||
|
||||
def _build_dataframe(buy_ohlc_sell_matrice):
|
||||
_validate_ohlc(buy_ohlc_sell_matrice)
|
||||
tickers = []
|
||||
for ohlc in buy_ohlc_sell_matrice:
|
||||
ticker = {
|
||||
'date': ticker_start_time.shift(
|
||||
minutes=(
|
||||
ohlc[0] *
|
||||
ticker_interval_in_minute)).timestamp *
|
||||
1000,
|
||||
'buy': ohlc[1],
|
||||
'open': ohlc[2],
|
||||
'high': ohlc[3],
|
||||
'low': ohlc[4],
|
||||
'close': ohlc[5],
|
||||
'sell': ohlc[6]}
|
||||
tickers.append(ticker)
|
||||
|
||||
frame = DataFrame(tickers)
|
||||
frame['date'] = to_datetime(frame['date'],
|
||||
unit='ms',
|
||||
utc=True,
|
||||
infer_datetime_format=True)
|
||||
|
||||
return frame
|
||||
|
||||
|
||||
def _time_on_candle(number):
|
||||
return np.datetime64(ticker_start_time.shift(
|
||||
minutes=(number * ticker_interval_in_minute)).timestamp * 1000, 'ms')
|
||||
|
||||
|
||||
# End helper functions
|
||||
# Open trade should be removed from the end
|
||||
tc0 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 1]], # enter trade (signal on last candle)
|
||||
stop_loss=-0.99, roi=float('inf'), profit_perc=0.00,
|
||||
trades=[]
|
||||
)
|
||||
|
||||
# Two complete trades within dataframe(with sell hit for all)
|
||||
tc1 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 1], # enter trade (signal on last candle)
|
||||
[2, 5000, 5025, 4975, 4987, 6172, 0, 0], # exit at open
|
||||
[3, 5000, 5025, 4975, 4987, 6172, 1, 0], # no action
|
||||
[4, 5000, 5025, 4975, 4987, 6172, 0, 0], # should enter the trade
|
||||
[5, 5000, 5025, 4975, 4987, 6172, 0, 1], # no action
|
||||
[6, 5000, 5025, 4975, 4987, 6172, 0, 0], # should sell
|
||||
],
|
||||
stop_loss=-0.99, roi=float('inf'), profit_perc=0.00,
|
||||
trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=2),
|
||||
BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=4, close_tick=6)]
|
||||
)
|
||||
|
||||
# 3) Entered, sl 1%, candle drops 8% => Trade closed, 1% loss
|
||||
tc2 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4600, 4987, 6172, 0, 0], # enter trade, stoploss hit
|
||||
[2, 5000, 5025, 4975, 4987, 6172, 0, 0],
|
||||
],
|
||||
stop_loss=-0.01, roi=float('inf'), profit_perc=-0.01,
|
||||
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)]
|
||||
)
|
||||
|
||||
# 4) Entered, sl 3 %, candle drops 4%, recovers to 1 % = > Trade closed, 3 % loss
|
||||
tc3 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4800, 4987, 6172, 0, 0], # enter trade, stoploss hit
|
||||
[2, 5000, 5025, 4975, 4987, 6172, 0, 0],
|
||||
],
|
||||
stop_loss=-0.03, roi=float('inf'), profit_perc=-0.03,
|
||||
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)]
|
||||
)
|
||||
|
||||
# 5) Stoploss and sell are hit. should sell on stoploss
|
||||
tc4 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4800, 4987, 6172, 0, 1], # enter trade, stoploss hit, sell signal
|
||||
[2, 5000, 5025, 4975, 4987, 6172, 0, 0],
|
||||
],
|
||||
stop_loss=-0.03, roi=float('inf'), profit_perc=-0.03,
|
||||
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)]
|
||||
)
|
||||
|
||||
TESTS = [
|
||||
tc0,
|
||||
tc1,
|
||||
tc2,
|
||||
tc3,
|
||||
tc4
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data", TESTS)
|
||||
def test_edge_results(edge_conf, mocker, caplog, data) -> None:
|
||||
"""
|
||||
run functional tests
|
||||
"""
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
frame = _build_backtest_dataframe(data.data)
|
||||
caplog.set_level(logging.DEBUG)
|
||||
edge.fee = 0
|
||||
|
||||
trades = edge._find_trades_for_stoploss_range(frame, 'TEST/BTC', [data.stop_loss])
|
||||
results = edge._fill_calculable_fields(DataFrame(trades)) if trades else DataFrame()
|
||||
|
||||
print(results)
|
||||
|
||||
assert len(trades) == len(data.trades)
|
||||
|
||||
if not results.empty:
|
||||
assert round(results["profit_percent"].sum(), 3) == round(data.profit_perc, 3)
|
||||
|
||||
for c, trade in enumerate(data.trades):
|
||||
res = results.iloc[c]
|
||||
assert res.exit_type == trade.sell_reason
|
||||
assert arrow.get(res.open_time) == _get_frame_time_from_offset(trade.open_tick)
|
||||
assert arrow.get(res.close_time) == _get_frame_time_from_offset(trade.close_tick)
|
||||
|
||||
|
||||
def test_adjust(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(
|
||||
return_value={
|
||||
'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||
'C/D': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||
'N/O': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60)
|
||||
}
|
||||
))
|
||||
|
||||
pairs = ['A/B', 'C/D', 'E/F', 'G/H']
|
||||
assert(edge.adjust(pairs) == ['E/F', 'C/D'])
|
||||
|
||||
|
||||
def test_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(
|
||||
return_value={
|
||||
'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||
'C/D': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||
'N/O': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60)
|
||||
}
|
||||
))
|
||||
|
||||
assert edge.stoploss('E/F') == -0.01
|
||||
|
||||
|
||||
def test_nonexisting_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(
|
||||
return_value={
|
||||
'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||
}
|
||||
))
|
||||
|
||||
assert edge.stoploss('N/O') == -0.1
|
||||
|
||||
|
||||
def test_stake_amount(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(
|
||||
return_value={
|
||||
'E/F': PairInfo(-0.02, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||
}
|
||||
))
|
||||
free = 100
|
||||
total = 100
|
||||
in_trade = 25
|
||||
assert edge.stake_amount('E/F', free, total, in_trade) == 31.25
|
||||
|
||||
free = 20
|
||||
total = 100
|
||||
in_trade = 25
|
||||
assert edge.stake_amount('E/F', free, total, in_trade) == 20
|
||||
|
||||
free = 0
|
||||
total = 100
|
||||
in_trade = 25
|
||||
assert edge.stake_amount('E/F', free, total, in_trade) == 0
|
||||
|
||||
|
||||
def test_nonexisting_stake_amount(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(
|
||||
return_value={
|
||||
'E/F': PairInfo(-0.11, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||
}
|
||||
))
|
||||
# should use strategy stoploss
|
||||
assert edge.stake_amount('N/O', 1, 2, 1) == 0.15
|
||||
|
||||
|
||||
def test_edge_heartbeat_calculate(mocker, edge_conf):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
heartbeat = edge_conf['edge']['process_throttle_secs']
|
||||
|
||||
# should not recalculate if heartbeat not reached
|
||||
edge._last_updated = arrow.utcnow().timestamp - heartbeat + 1
|
||||
|
||||
assert edge.calculate() is False
|
||||
|
||||
|
||||
def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False,
|
||||
timerange=None, exchange=None):
|
||||
hz = 0.1
|
||||
base = 0.001
|
||||
|
||||
NEOBTC = [
|
||||
[
|
||||
ticker_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000,
|
||||
math.sin(x * hz) / 1000 + base,
|
||||
math.sin(x * hz) / 1000 + base + 0.0001,
|
||||
math.sin(x * hz) / 1000 + base - 0.0001,
|
||||
math.sin(x * hz) / 1000 + base,
|
||||
123.45
|
||||
] for x in range(0, 500)]
|
||||
|
||||
hz = 0.2
|
||||
base = 0.002
|
||||
LTCBTC = [
|
||||
[
|
||||
ticker_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000,
|
||||
math.sin(x * hz) / 1000 + base,
|
||||
math.sin(x * hz) / 1000 + base + 0.0001,
|
||||
math.sin(x * hz) / 1000 + base - 0.0001,
|
||||
math.sin(x * hz) / 1000 + base,
|
||||
123.45
|
||||
] for x in range(0, 500)]
|
||||
|
||||
pairdata = {'NEO/BTC': parse_ticker_dataframe(NEOBTC, '1h', pair="NEO/BTC", fill_missing=True),
|
||||
'LTC/BTC': parse_ticker_dataframe(LTCBTC, '1h', pair="LTC/BTC", fill_missing=True)}
|
||||
return pairdata
|
||||
|
||||
|
||||
def test_edge_process_downloaded_data(mocker, edge_conf):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
|
||||
mocker.patch('freqtrade.data.history.load_data', mocked_load_data)
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
|
||||
assert edge.calculate()
|
||||
assert len(edge._cached_pairs) == 2
|
||||
assert edge._last_updated <= arrow.utcnow().timestamp + 2
|
||||
|
||||
|
||||
def test_edge_process_no_data(mocker, edge_conf, caplog):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
|
||||
mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={}))
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
|
||||
assert not edge.calculate()
|
||||
assert len(edge._cached_pairs) == 0
|
||||
assert log_has("No data found. Edge is stopped ...", caplog)
|
||||
assert edge._last_updated == 0
|
||||
|
||||
|
||||
def test_edge_process_no_trades(mocker, edge_conf, caplog):
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
|
||||
mocker.patch('freqtrade.data.history.load_data', mocked_load_data)
|
||||
# Return empty
|
||||
mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', MagicMock(return_value=[]))
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
|
||||
assert not edge.calculate()
|
||||
assert len(edge._cached_pairs) == 0
|
||||
assert log_has("No trades found.", caplog)
|
||||
|
||||
|
||||
def test_edge_init_error(mocker, edge_conf,):
|
||||
edge_conf['stake_amount'] = 0.5
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
|
||||
with pytest.raises(OperationalException, match='Edge works only with unlimited stake amount'):
|
||||
get_patched_freqtradebot(mocker, edge_conf)
|
||||
|
||||
|
||||
def test_process_expectancy(mocker, edge_conf):
|
||||
edge_conf['edge']['min_trade_number'] = 2
|
||||
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||
|
||||
def get_fee():
|
||||
return 0.001
|
||||
|
||||
freqtrade.exchange.get_fee = get_fee
|
||||
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||
|
||||
trades = [
|
||||
{'pair': 'TEST/BTC',
|
||||
'stoploss': -0.9,
|
||||
'profit_percent': '',
|
||||
'profit_abs': '',
|
||||
'open_time': np.datetime64('2018-10-03T00:05:00.000000000'),
|
||||
'close_time': np.datetime64('2018-10-03T00:10:00.000000000'),
|
||||
'open_index': 1,
|
||||
'close_index': 1,
|
||||
'trade_duration': '',
|
||||
'open_rate': 17,
|
||||
'close_rate': 17,
|
||||
'exit_type': 'sell_signal'},
|
||||
|
||||
{'pair': 'TEST/BTC',
|
||||
'stoploss': -0.9,
|
||||
'profit_percent': '',
|
||||
'profit_abs': '',
|
||||
'open_time': np.datetime64('2018-10-03T00:20:00.000000000'),
|
||||
'close_time': np.datetime64('2018-10-03T00:25:00.000000000'),
|
||||
'open_index': 4,
|
||||
'close_index': 4,
|
||||
'trade_duration': '',
|
||||
'open_rate': 20,
|
||||
'close_rate': 20,
|
||||
'exit_type': 'sell_signal'},
|
||||
|
||||
{'pair': 'TEST/BTC',
|
||||
'stoploss': -0.9,
|
||||
'profit_percent': '',
|
||||
'profit_abs': '',
|
||||
'open_time': np.datetime64('2018-10-03T00:30:00.000000000'),
|
||||
'close_time': np.datetime64('2018-10-03T00:40:00.000000000'),
|
||||
'open_index': 6,
|
||||
'close_index': 7,
|
||||
'trade_duration': '',
|
||||
'open_rate': 26,
|
||||
'close_rate': 34,
|
||||
'exit_type': 'sell_signal'}
|
||||
]
|
||||
|
||||
trades_df = DataFrame(trades)
|
||||
trades_df = edge._fill_calculable_fields(trades_df)
|
||||
final = edge._process_expectancy(trades_df)
|
||||
assert len(final) == 1
|
||||
|
||||
assert 'TEST/BTC' in final
|
||||
assert final['TEST/BTC'].stoploss == -0.9
|
||||
assert round(final['TEST/BTC'].winrate, 10) == 0.3333333333
|
||||
assert round(final['TEST/BTC'].risk_reward_ratio, 10) == 306.5384615384
|
||||
assert round(final['TEST/BTC'].required_risk_reward, 10) == 2.0
|
||||
assert round(final['TEST/BTC'].expectancy, 10) == 101.5128205128
|
||||
|
||||
# Pop last item so no trade is profitable
|
||||
trades.pop()
|
||||
trades_df = DataFrame(trades)
|
||||
trades_df = edge._fill_calculable_fields(trades_df)
|
||||
final = edge._process_expectancy(trades_df)
|
||||
assert len(final) == 0
|
||||
assert isinstance(final, dict)
|
0
tests/exchange/__init__.py
Normal file
0
tests/exchange/__init__.py
Normal file
92
tests/exchange/test_binance.py
Normal file
92
tests/exchange/test_binance.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from random import randint
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import ccxt
|
||||
import pytest
|
||||
|
||||
from freqtrade import (DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.tests.conftest import get_patched_exchange
|
||||
|
||||
|
||||
def test_stoploss_limit_order(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
order_type = 'stop_loss_limit'
|
||||
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
'id': order_id,
|
||||
'info': {
|
||||
'foo': 'bar'
|
||||
}
|
||||
})
|
||||
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200)
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
|
||||
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
assert order['id'] == order_id
|
||||
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
||||
assert api_mock.create_order.call_args[0][1] == order_type
|
||||
assert api_mock.create_order.call_args[0][2] == 'sell'
|
||||
assert api_mock.create_order.call_args[0][3] == 1
|
||||
assert api_mock.create_order.call_args[0][4] == 200
|
||||
assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220}
|
||||
|
||||
# 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_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
|
||||
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_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
|
||||
with pytest.raises(OperationalException, match=r".*DeadBeef.*"):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
|
||||
|
||||
def test_stoploss_limit_order_dry_run(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_type = 'stop_loss_limit'
|
||||
default_conf['dry_run'] = True
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200)
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
|
||||
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
assert 'type' in order
|
||||
|
||||
assert order['type'] == order_type
|
||||
assert order['price'] == 220
|
||||
assert order['amount'] == 1
|
1559
tests/exchange/test_exchange.py
Normal file
1559
tests/exchange/test_exchange.py
Normal file
File diff suppressed because it is too large
Load Diff
69
tests/exchange/test_kraken.py
Normal file
69
tests/exchange/test_kraken.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
|
||||
# pragma pylint: disable=protected-access
|
||||
from random import randint
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freqtrade.tests.conftest import get_patched_exchange
|
||||
|
||||
|
||||
def test_buy_kraken_trading_agreement(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
order_type = 'limit'
|
||||
time_in_force = 'ioc'
|
||||
api_mock.options = {}
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
'id': order_id,
|
||||
'info': {
|
||||
'foo': 'bar'
|
||||
}
|
||||
})
|
||||
default_conf['dry_run'] = False
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
|
||||
|
||||
order = exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||
amount=1, rate=200, time_in_force=time_in_force)
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
assert order['id'] == order_id
|
||||
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
||||
assert api_mock.create_order.call_args[0][1] == order_type
|
||||
assert api_mock.create_order.call_args[0][2] == 'buy'
|
||||
assert api_mock.create_order.call_args[0][3] == 1
|
||||
assert api_mock.create_order.call_args[0][4] == 200
|
||||
assert api_mock.create_order.call_args[0][5] == {'timeInForce': 'ioc',
|
||||
'trading_agreement': 'agree'}
|
||||
|
||||
|
||||
def test_sell_kraken_trading_agreement(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
|
||||
order_type = 'market'
|
||||
api_mock.options = {}
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
'id': order_id,
|
||||
'info': {
|
||||
'foo': 'bar'
|
||||
}
|
||||
})
|
||||
default_conf['dry_run'] = False
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
|
||||
|
||||
order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
assert order['id'] == order_id
|
||||
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
||||
assert api_mock.create_order.call_args[0][1] == order_type
|
||||
assert api_mock.create_order.call_args[0][2] == 'sell'
|
||||
assert api_mock.create_order.call_args[0][3] == 1
|
||||
assert api_mock.create_order.call_args[0][4] is None
|
||||
assert api_mock.create_order.call_args[0][5] == {'trading_agreement': 'agree'}
|
51
tests/optimize/__init__.py
Normal file
51
tests/optimize/__init__.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from typing import NamedTuple, List
|
||||
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.strategy.interface import SellType
|
||||
|
||||
ticker_start_time = arrow.get(2018, 10, 3)
|
||||
tests_ticker_interval = '1h'
|
||||
|
||||
|
||||
class BTrade(NamedTuple):
|
||||
"""
|
||||
Minimalistic Trade result used for functional backtesting
|
||||
"""
|
||||
sell_reason: SellType
|
||||
open_tick: int
|
||||
close_tick: int
|
||||
|
||||
|
||||
class BTContainer(NamedTuple):
|
||||
"""
|
||||
Minimal BacktestContainer defining Backtest inputs and results.
|
||||
"""
|
||||
data: List[float]
|
||||
stop_loss: float
|
||||
roi: float
|
||||
trades: List[BTrade]
|
||||
profit_perc: float
|
||||
trailing_stop: bool = False
|
||||
trailing_only_offset_is_reached: bool = False
|
||||
trailing_stop_positive: float = None
|
||||
trailing_stop_positive_offset: float = 0.0
|
||||
use_sell_signal: bool = False
|
||||
|
||||
|
||||
def _get_frame_time_from_offset(offset):
|
||||
return ticker_start_time.shift(minutes=(offset * timeframe_to_minutes(tests_ticker_interval))
|
||||
).datetime
|
||||
|
||||
|
||||
def _build_backtest_dataframe(ticker_with_signals):
|
||||
columns = ['date', 'open', 'high', 'low', 'close', 'volume', 'buy', 'sell']
|
||||
|
||||
frame = DataFrame.from_records(ticker_with_signals, columns=columns)
|
||||
frame['date'] = frame['date'].apply(_get_frame_time_from_offset)
|
||||
# Ensure floats are in place
|
||||
for column in ['open', 'high', 'low', 'close', 'volume']:
|
||||
frame[column] = frame[column].astype('float64')
|
||||
return frame
|
322
tests/optimize/test_backtest_detail.py
Normal file
322
tests/optimize/test_backtest_detail.py
Normal file
@@ -0,0 +1,322 @@
|
||||
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, C0330, unused-argument
|
||||
import logging
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.data.history import get_timeframe
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
from freqtrade.strategy.interface import SellType
|
||||
from freqtrade.tests.conftest import patch_exchange
|
||||
from freqtrade.tests.optimize import (BTContainer, BTrade,
|
||||
_build_backtest_dataframe,
|
||||
_get_frame_time_from_offset,
|
||||
tests_ticker_interval)
|
||||
|
||||
# Test 0: Sell with signal sell in candle 3
|
||||
# Test with Stop-loss at 1%
|
||||
tc0 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||
[2, 4987, 5012, 4986, 4600, 6172, 0, 0], # exit with stoploss hit
|
||||
[3, 5010, 5000, 4980, 5010, 6172, 0, 1],
|
||||
[4, 5010, 4987, 4977, 4995, 6172, 0, 0],
|
||||
[5, 4995, 4995, 4995, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.01, roi=1, profit_perc=0.002, use_sell_signal=True,
|
||||
trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)]
|
||||
)
|
||||
|
||||
# Test 1: Stop-Loss Triggered 1% loss
|
||||
# Test with Stop-loss at 1%
|
||||
tc1 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||
[2, 4987, 5012, 4600, 4600, 6172, 0, 0], # exit with stoploss hit
|
||||
[3, 4975, 5000, 4980, 4977, 6172, 0, 0],
|
||||
[4, 4977, 4987, 4977, 4995, 6172, 0, 0],
|
||||
[5, 4995, 4995, 4995, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.01, roi=1, profit_perc=-0.01,
|
||||
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)]
|
||||
)
|
||||
|
||||
|
||||
# Test 2: Minus 4% Low, minus 1% close
|
||||
# Test with Stop-Loss at 3%
|
||||
tc2 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||
[2, 4987, 5012, 4962, 4975, 6172, 0, 0],
|
||||
[3, 4975, 5000, 4800, 4962, 6172, 0, 0], # exit with stoploss hit
|
||||
[4, 4962, 4987, 4937, 4950, 6172, 0, 0],
|
||||
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.03, roi=1, profit_perc=-0.03,
|
||||
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=3)]
|
||||
)
|
||||
|
||||
|
||||
# Test 3: Multiple trades.
|
||||
# Candle drops 4%, Recovers 1%.
|
||||
# Entry Criteria Met
|
||||
# Candle drops 20%
|
||||
# Trade-A: Stop-Loss Triggered 2% Loss
|
||||
# Trade-B: Stop-Loss Triggered 2% Loss
|
||||
tc3 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||
[2, 4987, 5012, 4800, 4975, 6172, 0, 0], # exit with stoploss hit
|
||||
[3, 4975, 5000, 4950, 4962, 6172, 1, 0],
|
||||
[4, 4975, 5000, 4950, 4962, 6172, 0, 0], # enter trade 2 (signal on last candle)
|
||||
[5, 4962, 4987, 4000, 4000, 6172, 0, 0], # exit with stoploss hit
|
||||
[6, 4950, 4975, 4975, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.02, roi=1, profit_perc=-0.04,
|
||||
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2),
|
||||
BTrade(sell_reason=SellType.STOP_LOSS, open_tick=4, close_tick=5)]
|
||||
)
|
||||
|
||||
# Test 4: Minus 3% / recovery +15%
|
||||
# Candle Data for test 3 – Candle drops 3% Closed 15% up
|
||||
# Test with Stop-loss at 2% ROI 6%
|
||||
# Stop-Loss Triggered 2% Loss
|
||||
tc4 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||
[2, 4987, 5750, 4850, 5750, 6172, 0, 0], # Exit with stoploss hit
|
||||
[3, 4975, 5000, 4950, 4962, 6172, 0, 0],
|
||||
[4, 4962, 4987, 4937, 4950, 6172, 0, 0],
|
||||
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.02, roi=0.06, profit_perc=-0.02,
|
||||
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)]
|
||||
)
|
||||
|
||||
# Test 5: Drops 0.5% Closes +20%, ROI triggers 3% Gain
|
||||
# stop-loss: 1%, ROI: 3%
|
||||
tc5 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4980, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4980, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||
[2, 4987, 5025, 4975, 4987, 6172, 0, 0],
|
||||
[3, 4975, 6000, 4975, 6000, 6172, 0, 0], # ROI
|
||||
[4, 4962, 4987, 4972, 4950, 6172, 0, 0],
|
||||
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.01, roi=0.03, profit_perc=0.03,
|
||||
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)]
|
||||
)
|
||||
|
||||
# Test 6: Drops 3% / Recovers 6% Positive / Closes 1% positve, Stop-Loss triggers 2% Loss
|
||||
# stop-loss: 2% ROI: 5%
|
||||
tc6 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||
[2, 4987, 5300, 4850, 5050, 6172, 0, 0], # Exit with stoploss
|
||||
[3, 4975, 5000, 4950, 4962, 6172, 0, 0],
|
||||
[4, 4962, 4987, 4972, 4950, 6172, 0, 0],
|
||||
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.02, roi=0.05, profit_perc=-0.02,
|
||||
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)]
|
||||
)
|
||||
|
||||
# Test 7: 6% Positive / 1% Negative / Close 1% Positve, ROI Triggers 3% Gain
|
||||
# stop-loss: 2% ROI: 3%
|
||||
tc7 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||
[1, 5000, 5025, 4975, 4987, 6172, 0, 0],
|
||||
[2, 4987, 5300, 4950, 5050, 6172, 0, 0],
|
||||
[3, 4975, 5000, 4950, 4962, 6172, 0, 0],
|
||||
[4, 4962, 4987, 4972, 4950, 6172, 0, 0],
|
||||
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
|
||||
stop_loss=-0.02, roi=0.03, profit_perc=0.03,
|
||||
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)]
|
||||
)
|
||||
|
||||
|
||||
# Test 8: trailing_stop should raise so candle 3 causes a stoploss.
|
||||
# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted in candle 2
|
||||
tc8 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||
[1, 5000, 5050, 4950, 5000, 6172, 0, 0],
|
||||
[2, 5000, 5250, 4750, 4850, 6172, 0, 0],
|
||||
[3, 4850, 5050, 4650, 4750, 6172, 0, 0],
|
||||
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||
stop_loss=-0.10, roi=0.10, profit_perc=-0.055, trailing_stop=True,
|
||||
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)]
|
||||
)
|
||||
|
||||
|
||||
# Test 9: trailing_stop should raise - high and low in same candle.
|
||||
# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted in candle 3
|
||||
tc9 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||
[1, 5000, 5050, 4950, 5000, 6172, 0, 0],
|
||||
[2, 5000, 5050, 4950, 5000, 6172, 0, 0],
|
||||
[3, 5000, 5200, 4550, 4850, 6172, 0, 0],
|
||||
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||
stop_loss=-0.10, roi=0.10, profit_perc=-0.064, trailing_stop=True,
|
||||
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)]
|
||||
)
|
||||
|
||||
# Test 10: trailing_stop should raise so candle 3 causes a stoploss
|
||||
# without applying trailing_stop_positive since stoploss_offset is at 10%.
|
||||
# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2
|
||||
tc10 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||
[1, 5000, 5050, 4950, 5100, 6172, 0, 0],
|
||||
[2, 5100, 5251, 5100, 5100, 6172, 0, 0],
|
||||
[3, 4850, 5050, 4650, 4750, 6172, 0, 0],
|
||||
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||
stop_loss=-0.10, roi=0.10, profit_perc=-0.1, trailing_stop=True,
|
||||
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.10,
|
||||
trailing_stop_positive=0.03,
|
||||
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=4)]
|
||||
)
|
||||
|
||||
# Test 11: trailing_stop should raise so candle 3 causes a stoploss
|
||||
# applying a positive trailing stop of 3% since stop_positive_offset is reached.
|
||||
# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2
|
||||
tc11 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||
[1, 5000, 5050, 4950, 5100, 6172, 0, 0],
|
||||
[2, 5100, 5251, 5100, 5100, 6172, 0, 0],
|
||||
[3, 4850, 5050, 4650, 4750, 6172, 0, 0],
|
||||
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||
stop_loss=-0.10, roi=0.10, profit_perc=0.019, trailing_stop=True,
|
||||
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05,
|
||||
trailing_stop_positive=0.03,
|
||||
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)]
|
||||
)
|
||||
|
||||
# Test 12: trailing_stop should raise in candle 2 and cause a stoploss in the same candle
|
||||
# applying a positive trailing stop of 3% since stop_positive_offset is reached.
|
||||
# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2
|
||||
tc12 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||
[1, 5000, 5050, 4950, 5100, 6172, 0, 0],
|
||||
[2, 5100, 5251, 4650, 5100, 6172, 0, 0],
|
||||
[3, 4850, 5050, 4650, 4750, 6172, 0, 0],
|
||||
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||
stop_loss=-0.10, roi=0.10, profit_perc=0.019, trailing_stop=True,
|
||||
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05,
|
||||
trailing_stop_positive=0.03,
|
||||
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)]
|
||||
)
|
||||
|
||||
# Test 13: Buy and sell ROI on same candle
|
||||
# stop-loss: 10% (should not apply), ROI: 1%
|
||||
tc13 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||
[1, 5000, 5100, 4950, 5100, 6172, 0, 0],
|
||||
[2, 5100, 5251, 4850, 5100, 6172, 0, 0],
|
||||
[3, 4850, 5050, 4850, 4750, 6172, 0, 0],
|
||||
[4, 4750, 4950, 4850, 4750, 6172, 0, 0]],
|
||||
stop_loss=-0.10, roi=0.01, profit_perc=0.01,
|
||||
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)]
|
||||
)
|
||||
|
||||
# Test 14 - Buy and Stoploss on same candle
|
||||
# stop-loss: 5%, ROI: 10% (should not apply)
|
||||
tc14 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||
[1, 5000, 5100, 4600, 5100, 6172, 0, 0],
|
||||
[2, 5100, 5251, 4850, 5100, 6172, 0, 0],
|
||||
[3, 4850, 5050, 4850, 4750, 6172, 0, 0],
|
||||
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||
stop_loss=-0.05, roi=0.10, profit_perc=-0.05,
|
||||
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)]
|
||||
)
|
||||
|
||||
|
||||
# Test 15 - Buy and ROI on same candle, followed by buy and Stoploss on next candle
|
||||
# stop-loss: 5%, ROI: 10% (should not apply)
|
||||
tc15 = BTContainer(data=[
|
||||
# D O H L C V B S
|
||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||
[1, 5000, 5100, 4900, 5100, 6172, 1, 0],
|
||||
[2, 5100, 5251, 4650, 5100, 6172, 0, 0],
|
||||
[3, 4850, 5050, 4850, 4750, 6172, 0, 0],
|
||||
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||
stop_loss=-0.05, roi=0.01, profit_perc=-0.04,
|
||||
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1),
|
||||
BTrade(sell_reason=SellType.STOP_LOSS, open_tick=2, close_tick=2)]
|
||||
)
|
||||
|
||||
TESTS = [
|
||||
tc0,
|
||||
tc1,
|
||||
tc2,
|
||||
tc3,
|
||||
tc4,
|
||||
tc5,
|
||||
tc6,
|
||||
tc7,
|
||||
tc8,
|
||||
tc9,
|
||||
tc10,
|
||||
tc11,
|
||||
tc12,
|
||||
tc13,
|
||||
tc14,
|
||||
tc15,
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data", TESTS)
|
||||
def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
|
||||
"""
|
||||
run functional tests
|
||||
"""
|
||||
default_conf["stoploss"] = data.stop_loss
|
||||
default_conf["minimal_roi"] = {"0": data.roi}
|
||||
default_conf["ticker_interval"] = tests_ticker_interval
|
||||
default_conf["trailing_stop"] = data.trailing_stop
|
||||
default_conf["trailing_only_offset_is_reached"] = data.trailing_only_offset_is_reached
|
||||
# Only add this to configuration If it's necessary
|
||||
if data.trailing_stop_positive:
|
||||
default_conf["trailing_stop_positive"] = data.trailing_stop_positive
|
||||
default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset
|
||||
default_conf["experimental"] = {"use_sell_signal": data.use_sell_signal}
|
||||
|
||||
mocker.patch("freqtrade.exchange.Exchange.get_fee", MagicMock(return_value=0.0))
|
||||
patch_exchange(mocker)
|
||||
frame = _build_backtest_dataframe(data.data)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting.advise_buy = lambda a, m: frame
|
||||
backtesting.advise_sell = lambda a, m: frame
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
pair = "UNITTEST/BTC"
|
||||
# Dummy data as we mock the analyze functions
|
||||
data_processed = {pair: DataFrame()}
|
||||
min_date, max_date = get_timeframe({pair: frame})
|
||||
results = backtesting.backtest(
|
||||
{
|
||||
'stake_amount': default_conf['stake_amount'],
|
||||
'processed': data_processed,
|
||||
'max_open_trades': 10,
|
||||
'start_date': min_date,
|
||||
'end_date': max_date,
|
||||
}
|
||||
)
|
||||
print(results.T)
|
||||
|
||||
assert len(results) == len(data.trades)
|
||||
assert round(results["profit_percent"].sum(), 3) == round(data.profit_perc, 3)
|
||||
|
||||
for c, trade in enumerate(data.trades):
|
||||
res = results.iloc[c]
|
||||
assert res.sell_reason == trade.sell_reason
|
||||
assert res.open_time == _get_frame_time_from_offset(trade.open_tick)
|
||||
assert res.close_time == _get_frame_time_from_offset(trade.close_tick)
|
906
tests/optimize/test_backtesting.py
Normal file
906
tests/optimize/test_backtesting.py
Normal file
@@ -0,0 +1,906 @@
|
||||
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument
|
||||
|
||||
import math
|
||||
import random
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pytest
|
||||
from arrow import Arrow
|
||||
|
||||
from freqtrade import DependencyException, OperationalException, constants
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data import history
|
||||
from freqtrade.data.btanalysis import evaluate_result_multi
|
||||
from freqtrade.data.converter import parse_ticker_dataframe
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.data.history import get_timeframe
|
||||
from freqtrade.optimize import setup_configuration, start_backtesting
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
from freqtrade.state import RunMode
|
||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||
from freqtrade.strategy.interface import SellType
|
||||
from freqtrade.tests.conftest import (get_args, log_has, log_has_re,
|
||||
patch_exchange,
|
||||
patched_configuration_load_config_file)
|
||||
|
||||
|
||||
def trim_dictlist(dict_list, num):
|
||||
new = {}
|
||||
for pair, pair_data in dict_list.items():
|
||||
new[pair] = pair_data[num:].reset_index()
|
||||
return new
|
||||
|
||||
|
||||
def load_data_test(what, testdatadir):
|
||||
timerange = TimeRange(None, 'line', 0, -101)
|
||||
pair = history.load_tickerdata_file(testdatadir, ticker_interval='1m',
|
||||
pair='UNITTEST/BTC', timerange=timerange)
|
||||
datalen = len(pair)
|
||||
|
||||
base = 0.001
|
||||
if what == 'raise':
|
||||
data = [
|
||||
[
|
||||
pair[x][0], # Keep old dates
|
||||
x * base, # But replace O,H,L,C
|
||||
x * base + 0.0001,
|
||||
x * base - 0.0001,
|
||||
x * base,
|
||||
pair[x][5], # Keep old volume
|
||||
] for x in range(0, datalen)
|
||||
]
|
||||
if what == 'lower':
|
||||
data = [
|
||||
[
|
||||
pair[x][0], # Keep old dates
|
||||
1 - x * base, # But replace O,H,L,C
|
||||
1 - x * base + 0.0001,
|
||||
1 - x * base - 0.0001,
|
||||
1 - x * base,
|
||||
pair[x][5] # Keep old volume
|
||||
] for x in range(0, datalen)
|
||||
]
|
||||
if what == 'sine':
|
||||
hz = 0.1 # frequency
|
||||
data = [
|
||||
[
|
||||
pair[x][0], # Keep old dates
|
||||
math.sin(x * hz) / 1000 + base, # But replace O,H,L,C
|
||||
math.sin(x * hz) / 1000 + base + 0.0001,
|
||||
math.sin(x * hz) / 1000 + base - 0.0001,
|
||||
math.sin(x * hz) / 1000 + base,
|
||||
pair[x][5] # Keep old volume
|
||||
] for x in range(0, datalen)
|
||||
]
|
||||
return {'UNITTEST/BTC': parse_ticker_dataframe(data, '1m', pair="UNITTEST/BTC",
|
||||
fill_missing=True)}
|
||||
|
||||
|
||||
def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None:
|
||||
patch_exchange(mocker)
|
||||
config['ticker_interval'] = '1m'
|
||||
backtesting = Backtesting(config)
|
||||
|
||||
data = load_data_test(contour, testdatadir)
|
||||
processed = backtesting.strategy.tickerdata_to_dataframe(data)
|
||||
min_date, max_date = get_timeframe(processed)
|
||||
assert isinstance(processed, dict)
|
||||
results = backtesting.backtest(
|
||||
{
|
||||
'stake_amount': config['stake_amount'],
|
||||
'processed': processed,
|
||||
'max_open_trades': 1,
|
||||
'position_stacking': False,
|
||||
'start_date': min_date,
|
||||
'end_date': max_date,
|
||||
}
|
||||
)
|
||||
# results :: <class 'pandas.core.frame.DataFrame'>
|
||||
assert len(results) == num_results
|
||||
|
||||
|
||||
def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False,
|
||||
timerange=None, exchange=None, live=False):
|
||||
tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||
pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata, '1m', pair="UNITTEST/BTC",
|
||||
fill_missing=True)}
|
||||
return pairdata
|
||||
|
||||
|
||||
# use for mock ccxt.fetch_ohlvc'
|
||||
def _load_pair_as_ticks(pair, tickfreq):
|
||||
ticks = history.load_tickerdata_file(None, ticker_interval=tickfreq, pair=pair)
|
||||
ticks = ticks[-201:]
|
||||
return ticks
|
||||
|
||||
|
||||
# FIX: fixturize this?
|
||||
def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC', record=None):
|
||||
data = history.load_data(datadir=datadir, ticker_interval='1m', pairs=[pair])
|
||||
data = trim_dictlist(data, -201)
|
||||
patch_exchange(mocker)
|
||||
backtesting = Backtesting(conf)
|
||||
processed = backtesting.strategy.tickerdata_to_dataframe(data)
|
||||
min_date, max_date = get_timeframe(processed)
|
||||
return {
|
||||
'stake_amount': conf['stake_amount'],
|
||||
'processed': processed,
|
||||
'max_open_trades': 10,
|
||||
'position_stacking': False,
|
||||
'record': record,
|
||||
'start_date': min_date,
|
||||
'end_date': max_date,
|
||||
}
|
||||
|
||||
|
||||
def _trend(signals, buy_value, sell_value):
|
||||
n = len(signals['low'])
|
||||
buy = np.zeros(n)
|
||||
sell = np.zeros(n)
|
||||
for i in range(0, len(signals['buy'])):
|
||||
if random.random() > 0.5: # Both buy and sell signals at same timeframe
|
||||
buy[i] = buy_value
|
||||
sell[i] = sell_value
|
||||
signals['buy'] = buy
|
||||
signals['sell'] = sell
|
||||
return signals
|
||||
|
||||
|
||||
def _trend_alternate(dataframe=None, metadata=None):
|
||||
signals = dataframe
|
||||
low = signals['low']
|
||||
n = len(low)
|
||||
buy = np.zeros(n)
|
||||
sell = np.zeros(n)
|
||||
for i in range(0, len(buy)):
|
||||
if i % 2 == 0:
|
||||
buy[i] = 1
|
||||
else:
|
||||
sell[i] = 1
|
||||
signals['buy'] = buy
|
||||
signals['sell'] = sell
|
||||
return dataframe
|
||||
|
||||
|
||||
# Unit tests
|
||||
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'backtesting'
|
||||
]
|
||||
|
||||
config = setup_configuration(get_args(args), RunMode.BACKTEST)
|
||||
assert 'max_open_trades' in config
|
||||
assert 'stake_currency' in config
|
||||
assert 'stake_amount' in config
|
||||
assert 'exchange' in config
|
||||
assert 'pair_whitelist' in config['exchange']
|
||||
assert 'datadir' in config
|
||||
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||
assert 'ticker_interval' in config
|
||||
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
|
||||
|
||||
assert 'position_stacking' not in config
|
||||
assert not log_has('Parameter --enable-position-stacking detected ...', caplog)
|
||||
|
||||
assert 'refresh_pairs' not in config
|
||||
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
|
||||
|
||||
assert 'timerange' not in config
|
||||
assert 'export' not in config
|
||||
assert 'runmode' in config
|
||||
assert config['runmode'] == RunMode.BACKTEST
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:DEPRECATED")
|
||||
def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.configuration.create_datadir',
|
||||
lambda c, x: x
|
||||
)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'--datadir', '/foo/bar',
|
||||
'backtesting',
|
||||
'--ticker-interval', '1m',
|
||||
'--enable-position-stacking',
|
||||
'--disable-max-market-positions',
|
||||
'--refresh-pairs-cached',
|
||||
'--timerange', ':100',
|
||||
'--export', '/bar/foo',
|
||||
'--export-filename', 'foo_bar.json'
|
||||
]
|
||||
|
||||
config = setup_configuration(get_args(args), RunMode.BACKTEST)
|
||||
assert 'max_open_trades' in config
|
||||
assert 'stake_currency' in config
|
||||
assert 'stake_amount' in config
|
||||
assert 'exchange' in config
|
||||
assert 'pair_whitelist' in config['exchange']
|
||||
assert 'datadir' in config
|
||||
assert config['runmode'] == RunMode.BACKTEST
|
||||
|
||||
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||
assert 'ticker_interval' in config
|
||||
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||
caplog)
|
||||
|
||||
assert 'position_stacking' in config
|
||||
assert log_has('Parameter --enable-position-stacking detected ...', caplog)
|
||||
|
||||
assert 'use_max_market_positions' in config
|
||||
assert log_has('Parameter --disable-max-market-positions detected ...', caplog)
|
||||
assert log_has('max_open_trades set to unlimited ...', caplog)
|
||||
|
||||
assert 'refresh_pairs' in config
|
||||
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
|
||||
|
||||
assert 'timerange' in config
|
||||
assert log_has('Parameter --timerange detected: {} ...'.format(config['timerange']), caplog)
|
||||
|
||||
assert 'export' in config
|
||||
assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog)
|
||||
assert 'exportfilename' in config
|
||||
assert log_has('Storing backtest results to {} ...'.format(config['exportfilename']), caplog)
|
||||
|
||||
|
||||
def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None:
|
||||
default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'backtesting'
|
||||
]
|
||||
|
||||
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
|
||||
setup_configuration(get_args(args), RunMode.BACKTEST)
|
||||
|
||||
|
||||
def test_start(mocker, fee, default_conf, caplog) -> None:
|
||||
start_mock = MagicMock()
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock)
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'backtesting'
|
||||
]
|
||||
args = get_args(args)
|
||||
start_backtesting(args)
|
||||
assert log_has('Starting freqtrade in Backtesting mode', caplog)
|
||||
assert start_mock.call_count == 1
|
||||
|
||||
|
||||
ORDER_TYPES = [
|
||||
{
|
||||
'buy': 'limit',
|
||||
'sell': 'limit',
|
||||
'stoploss': 'limit',
|
||||
'stoploss_on_exchange': False
|
||||
},
|
||||
{
|
||||
'buy': 'limit',
|
||||
'sell': 'limit',
|
||||
'stoploss': 'limit',
|
||||
'stoploss_on_exchange': True
|
||||
}]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("order_types", ORDER_TYPES)
|
||||
def test_backtesting_init(mocker, default_conf, order_types) -> None:
|
||||
"""
|
||||
Check that stoploss_on_exchange is set to False while backtesting
|
||||
since backtesting assumes a perfect stoploss anyway.
|
||||
"""
|
||||
default_conf["order_types"] = order_types
|
||||
patch_exchange(mocker)
|
||||
get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
|
||||
backtesting = Backtesting(default_conf)
|
||||
assert backtesting.config == default_conf
|
||||
assert backtesting.ticker_interval == '5m'
|
||||
assert callable(backtesting.strategy.tickerdata_to_dataframe)
|
||||
assert callable(backtesting.advise_buy)
|
||||
assert callable(backtesting.advise_sell)
|
||||
assert isinstance(backtesting.strategy.dp, DataProvider)
|
||||
get_fee.assert_called()
|
||||
assert backtesting.fee == 0.5
|
||||
assert not backtesting.strategy.order_types["stoploss_on_exchange"]
|
||||
|
||||
|
||||
def test_backtesting_init_no_ticker_interval(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Check that stoploss_on_exchange is set to False while backtesting
|
||||
since backtesting assumes a perfect stoploss anyway.
|
||||
"""
|
||||
patch_exchange(mocker)
|
||||
del default_conf['ticker_interval']
|
||||
default_conf['strategy_list'] = ['DefaultStrategy',
|
||||
'SampleStrategy']
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
|
||||
with pytest.raises(OperationalException):
|
||||
Backtesting(default_conf)
|
||||
log_has("Ticker-interval needs to be set in either configuration "
|
||||
"or as cli argument `--ticker-interval 5m`", caplog)
|
||||
|
||||
|
||||
def test_tickerdata_to_dataframe_bt(default_conf, mocker, testdatadir) -> None:
|
||||
patch_exchange(mocker)
|
||||
timerange = TimeRange(None, 'line', 0, -100)
|
||||
tick = history.load_tickerdata_file(testdatadir, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC",
|
||||
fill_missing=True)}
|
||||
|
||||
backtesting = Backtesting(default_conf)
|
||||
data = backtesting.strategy.tickerdata_to_dataframe(tickerlist)
|
||||
assert len(data['UNITTEST/BTC']) == 102
|
||||
|
||||
# Load strategy to compare the result between Backtesting function and strategy are the same
|
||||
strategy = DefaultStrategy(default_conf)
|
||||
data2 = strategy.tickerdata_to_dataframe(tickerlist)
|
||||
assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC'])
|
||||
|
||||
|
||||
def test_generate_text_table(default_conf, mocker):
|
||||
patch_exchange(mocker)
|
||||
default_conf['max_open_trades'] = 2
|
||||
backtesting = Backtesting(default_conf)
|
||||
|
||||
results = pd.DataFrame(
|
||||
{
|
||||
'pair': ['ETH/BTC', 'ETH/BTC'],
|
||||
'profit_percent': [0.1, 0.2],
|
||||
'profit_abs': [0.2, 0.4],
|
||||
'trade_duration': [10, 30],
|
||||
'profit': [2, 0],
|
||||
'loss': [0, 0]
|
||||
}
|
||||
)
|
||||
|
||||
result_str = (
|
||||
'| pair | buy count | avg profit % | cum profit % | '
|
||||
'tot profit BTC | tot profit % | avg duration | profit | loss |\n'
|
||||
'|:--------|------------:|---------------:|---------------:|'
|
||||
'-----------------:|---------------:|:---------------|---------:|-------:|\n'
|
||||
'| ETH/BTC | 2 | 15.00 | 30.00 | '
|
||||
'0.60000000 | 15.00 | 0:20:00 | 2 | 0 |\n'
|
||||
'| TOTAL | 2 | 15.00 | 30.00 | '
|
||||
'0.60000000 | 15.00 | 0:20:00 | 2 | 0 |'
|
||||
)
|
||||
assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str
|
||||
|
||||
|
||||
def test_generate_text_table_sell_reason(default_conf, mocker):
|
||||
patch_exchange(mocker)
|
||||
backtesting = Backtesting(default_conf)
|
||||
|
||||
results = pd.DataFrame(
|
||||
{
|
||||
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
|
||||
'profit_percent': [0.1, 0.2, 0.3],
|
||||
'profit_abs': [0.2, 0.4, 0.5],
|
||||
'trade_duration': [10, 30, 10],
|
||||
'profit': [2, 0, 0],
|
||||
'loss': [0, 0, 1],
|
||||
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
|
||||
}
|
||||
)
|
||||
|
||||
result_str = (
|
||||
'| Sell Reason | Count |\n'
|
||||
'|:--------------|--------:|\n'
|
||||
'| roi | 2 |\n'
|
||||
'| stop_loss | 1 |'
|
||||
)
|
||||
assert backtesting._generate_text_table_sell_reason(
|
||||
data={'ETH/BTC': {}}, results=results) == result_str
|
||||
|
||||
|
||||
def test_generate_text_table_strategyn(default_conf, mocker):
|
||||
"""
|
||||
Test Backtesting.generate_text_table_sell_reason() method
|
||||
"""
|
||||
patch_exchange(mocker)
|
||||
default_conf['max_open_trades'] = 2
|
||||
backtesting = Backtesting(default_conf)
|
||||
results = {}
|
||||
results['ETH/BTC'] = pd.DataFrame(
|
||||
{
|
||||
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
|
||||
'profit_percent': [0.1, 0.2, 0.3],
|
||||
'profit_abs': [0.2, 0.4, 0.5],
|
||||
'trade_duration': [10, 30, 10],
|
||||
'profit': [2, 0, 0],
|
||||
'loss': [0, 0, 1],
|
||||
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
|
||||
}
|
||||
)
|
||||
results['LTC/BTC'] = pd.DataFrame(
|
||||
{
|
||||
'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'],
|
||||
'profit_percent': [0.4, 0.2, 0.3],
|
||||
'profit_abs': [0.4, 0.4, 0.5],
|
||||
'trade_duration': [15, 30, 15],
|
||||
'profit': [4, 1, 0],
|
||||
'loss': [0, 0, 1],
|
||||
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
|
||||
}
|
||||
)
|
||||
|
||||
result_str = (
|
||||
'| Strategy | buy count | avg profit % | cum profit % '
|
||||
'| tot profit BTC | tot profit % | avg duration | profit | loss |\n'
|
||||
'|:-----------|------------:|---------------:|---------------:'
|
||||
'|-----------------:|---------------:|:---------------|---------:|-------:|\n'
|
||||
'| ETH/BTC | 3 | 20.00 | 60.00 '
|
||||
'| 1.10000000 | 30.00 | 0:17:00 | 3 | 0 |\n'
|
||||
'| LTC/BTC | 3 | 30.00 | 90.00 '
|
||||
'| 1.30000000 | 45.00 | 0:20:00 | 3 | 0 |'
|
||||
)
|
||||
print(backtesting._generate_text_table_strategy(all_results=results))
|
||||
assert backtesting._generate_text_table_strategy(all_results=results) == result_str
|
||||
|
||||
|
||||
def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None:
|
||||
def get_timeframe(input1):
|
||||
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
||||
|
||||
mocker.patch('freqtrade.data.history.load_data', mocked_load_data)
|
||||
mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe)
|
||||
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock())
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.optimize.backtesting.Backtesting',
|
||||
backtest=MagicMock(),
|
||||
_generate_text_table=MagicMock(return_value='1'),
|
||||
)
|
||||
|
||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||
default_conf['ticker_interval'] = '1m'
|
||||
default_conf['datadir'] = testdatadir
|
||||
default_conf['export'] = None
|
||||
default_conf['timerange'] = '-100'
|
||||
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting.start()
|
||||
# check the logs, that will contain the backtest result
|
||||
exists = [
|
||||
'Using stake_currency: BTC ...',
|
||||
'Using stake_amount: 0.001 ...',
|
||||
'Backtesting with data from 2017-11-14T21:17:00+00:00 '
|
||||
'up to 2017-11-14T22:59:00+00:00 (0 days)..'
|
||||
]
|
||||
for line in exists:
|
||||
assert log_has(line, caplog)
|
||||
|
||||
|
||||
def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None:
|
||||
def get_timeframe(input1):
|
||||
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
||||
|
||||
mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={}))
|
||||
mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe)
|
||||
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock())
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.optimize.backtesting.Backtesting',
|
||||
backtest=MagicMock(),
|
||||
_generate_text_table=MagicMock(return_value='1'),
|
||||
)
|
||||
|
||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||
default_conf['ticker_interval'] = "1m"
|
||||
default_conf['datadir'] = testdatadir
|
||||
default_conf['export'] = None
|
||||
default_conf['timerange'] = '20180101-20180102'
|
||||
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting.start()
|
||||
# check the logs, that will contain the backtest result
|
||||
|
||||
assert log_has('No data found. Terminating.', caplog)
|
||||
|
||||
|
||||
def test_backtest(default_conf, fee, mocker, testdatadir) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
patch_exchange(mocker)
|
||||
backtesting = Backtesting(default_conf)
|
||||
pair = 'UNITTEST/BTC'
|
||||
timerange = TimeRange(None, 'line', 0, -201)
|
||||
data = history.load_data(datadir=testdatadir, ticker_interval='5m', pairs=['UNITTEST/BTC'],
|
||||
timerange=timerange)
|
||||
data_processed = backtesting.strategy.tickerdata_to_dataframe(data)
|
||||
min_date, max_date = get_timeframe(data_processed)
|
||||
results = backtesting.backtest(
|
||||
{
|
||||
'stake_amount': default_conf['stake_amount'],
|
||||
'processed': data_processed,
|
||||
'max_open_trades': 10,
|
||||
'position_stacking': False,
|
||||
'start_date': min_date,
|
||||
'end_date': max_date,
|
||||
}
|
||||
)
|
||||
assert not results.empty
|
||||
assert len(results) == 2
|
||||
|
||||
expected = pd.DataFrame(
|
||||
{'pair': [pair, pair],
|
||||
'profit_percent': [0.0, 0.0],
|
||||
'profit_abs': [0.0, 0.0],
|
||||
'open_time': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime,
|
||||
Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True
|
||||
),
|
||||
'close_time': pd.to_datetime([Arrow(2018, 1, 29, 22, 35, 0).datetime,
|
||||
Arrow(2018, 1, 30, 4, 10, 0).datetime], utc=True),
|
||||
'open_index': [78, 184],
|
||||
'close_index': [125, 192],
|
||||
'trade_duration': [235, 40],
|
||||
'open_at_end': [False, False],
|
||||
'open_rate': [0.104445, 0.10302485],
|
||||
'close_rate': [0.104969, 0.103541],
|
||||
'sell_reason': [SellType.ROI, SellType.ROI]
|
||||
})
|
||||
pd.testing.assert_frame_equal(results, expected)
|
||||
data_pair = data_processed[pair]
|
||||
for _, t in results.iterrows():
|
||||
ln = data_pair.loc[data_pair["date"] == t["open_time"]]
|
||||
# Check open trade rate alignes to open rate
|
||||
assert ln is not None
|
||||
assert round(ln.iloc[0]["open"], 6) == round(t["open_rate"], 6)
|
||||
# check close trade rate alignes to close rate or is between high and low
|
||||
ln = data_pair.loc[data_pair["date"] == t["close_time"]]
|
||||
assert (round(ln.iloc[0]["open"], 6) == round(t["close_rate"], 6) or
|
||||
round(ln.iloc[0]["low"], 6) < round(
|
||||
t["close_rate"], 6) < round(ln.iloc[0]["high"], 6))
|
||||
|
||||
|
||||
def test_backtest_1min_ticker_interval(default_conf, fee, mocker, testdatadir) -> None:
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
patch_exchange(mocker)
|
||||
backtesting = Backtesting(default_conf)
|
||||
|
||||
# Run a backtesting for an exiting 1min ticker_interval
|
||||
timerange = TimeRange(None, 'line', 0, -200)
|
||||
data = history.load_data(datadir=testdatadir, ticker_interval='1m', pairs=['UNITTEST/BTC'],
|
||||
timerange=timerange)
|
||||
processed = backtesting.strategy.tickerdata_to_dataframe(data)
|
||||
min_date, max_date = get_timeframe(processed)
|
||||
results = backtesting.backtest(
|
||||
{
|
||||
'stake_amount': default_conf['stake_amount'],
|
||||
'processed': processed,
|
||||
'max_open_trades': 1,
|
||||
'position_stacking': False,
|
||||
'start_date': min_date,
|
||||
'end_date': max_date,
|
||||
}
|
||||
)
|
||||
assert not results.empty
|
||||
assert len(results) == 1
|
||||
|
||||
|
||||
def test_processed(default_conf, mocker, testdatadir) -> None:
|
||||
patch_exchange(mocker)
|
||||
backtesting = Backtesting(default_conf)
|
||||
|
||||
dict_of_tickerrows = load_data_test('raise', testdatadir)
|
||||
dataframes = backtesting.strategy.tickerdata_to_dataframe(dict_of_tickerrows)
|
||||
dataframe = dataframes['UNITTEST/BTC']
|
||||
cols = dataframe.columns
|
||||
# assert the dataframe got some of the indicator columns
|
||||
for col in ['close', 'high', 'low', 'open', 'date',
|
||||
'ema50', 'ao', 'macd', 'plus_dm']:
|
||||
assert col in cols
|
||||
|
||||
|
||||
def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir) -> None:
|
||||
# TODO: Evaluate usefullness of this, the patterns and buy-signls are unrealistic
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
tests = [['raise', 19], ['lower', 0], ['sine', 35]]
|
||||
# We need to enable sell-signal - otherwise it sells on ROI!!
|
||||
default_conf['experimental'] = {"use_sell_signal": True}
|
||||
|
||||
for [contour, numres] in tests:
|
||||
simple_backtest(default_conf, contour, numres, mocker, testdatadir)
|
||||
|
||||
|
||||
def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir):
|
||||
# Override the default buy trend function in our default_strategy
|
||||
def fun(dataframe=None, pair=None):
|
||||
buy_value = 1
|
||||
sell_value = 1
|
||||
return _trend(dataframe, buy_value, sell_value)
|
||||
|
||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting.advise_buy = fun # Override
|
||||
backtesting.advise_sell = fun # Override
|
||||
results = backtesting.backtest(backtest_conf)
|
||||
assert results.empty
|
||||
|
||||
|
||||
def test_backtest_only_sell(mocker, default_conf, testdatadir):
|
||||
# Override the default buy trend function in our default_strategy
|
||||
def fun(dataframe=None, pair=None):
|
||||
buy_value = 0
|
||||
sell_value = 1
|
||||
return _trend(dataframe, buy_value, sell_value)
|
||||
|
||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir)
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting.advise_buy = fun # Override
|
||||
backtesting.advise_sell = fun # Override
|
||||
results = backtesting.backtest(backtest_conf)
|
||||
assert results.empty
|
||||
|
||||
|
||||
def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir):
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch('freqtrade.optimize.backtesting.file_dump_json', MagicMock())
|
||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf,
|
||||
pair='UNITTEST/BTC', datadir=testdatadir)
|
||||
# We need to enable sell-signal - otherwise it sells on ROI!!
|
||||
default_conf['experimental'] = {"use_sell_signal": True}
|
||||
default_conf['ticker_interval'] = '1m'
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting.advise_buy = _trend_alternate # Override
|
||||
backtesting.advise_sell = _trend_alternate # Override
|
||||
results = backtesting.backtest(backtest_conf)
|
||||
backtesting._store_backtest_result("test_.json", results)
|
||||
# 200 candles in backtest data
|
||||
# won't buy on first (shifted by 1)
|
||||
# 100 buys signals
|
||||
assert len(results) == 100
|
||||
# One trade was force-closed at the end
|
||||
assert len(results.loc[results.open_at_end]) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pair", ['ADA/BTC', 'LTC/BTC'])
|
||||
@pytest.mark.parametrize("tres", [0, 20, 30])
|
||||
def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir):
|
||||
|
||||
def _trend_alternate_hold(dataframe=None, metadata=None):
|
||||
"""
|
||||
Buy every xth candle - sell every other xth -2 (hold on to pairs a bit)
|
||||
"""
|
||||
if metadata['pair'] in('ETH/BTC', 'LTC/BTC'):
|
||||
multi = 20
|
||||
else:
|
||||
multi = 18
|
||||
dataframe['buy'] = np.where(dataframe.index % multi == 0, 1, 0)
|
||||
dataframe['sell'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0)
|
||||
return dataframe
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
patch_exchange(mocker)
|
||||
|
||||
pairs = ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC', 'NXT/BTC']
|
||||
data = history.load_data(datadir=testdatadir, ticker_interval='5m', pairs=pairs)
|
||||
# Only use 500 lines to increase performance
|
||||
data = trim_dictlist(data, -500)
|
||||
|
||||
# Remove data for one pair from the beginning of the data
|
||||
data[pair] = data[pair][tres:].reset_index()
|
||||
# We need to enable sell-signal - otherwise it sells on ROI!!
|
||||
default_conf['experimental'] = {"use_sell_signal": True}
|
||||
default_conf['ticker_interval'] = '5m'
|
||||
|
||||
backtesting = Backtesting(default_conf)
|
||||
backtesting.advise_buy = _trend_alternate_hold # Override
|
||||
backtesting.advise_sell = _trend_alternate_hold # Override
|
||||
|
||||
data_processed = backtesting.strategy.tickerdata_to_dataframe(data)
|
||||
min_date, max_date = get_timeframe(data_processed)
|
||||
backtest_conf = {
|
||||
'stake_amount': default_conf['stake_amount'],
|
||||
'processed': data_processed,
|
||||
'max_open_trades': 3,
|
||||
'position_stacking': False,
|
||||
'start_date': min_date,
|
||||
'end_date': max_date,
|
||||
}
|
||||
|
||||
results = backtesting.backtest(backtest_conf)
|
||||
|
||||
# Make sure we have parallel trades
|
||||
assert len(evaluate_result_multi(results, '5min', 2)) > 0
|
||||
# make sure we don't have trades with more than configured max_open_trades
|
||||
assert len(evaluate_result_multi(results, '5min', 3)) == 0
|
||||
|
||||
backtest_conf = {
|
||||
'stake_amount': default_conf['stake_amount'],
|
||||
'processed': data_processed,
|
||||
'max_open_trades': 1,
|
||||
'position_stacking': False,
|
||||
'start_date': min_date,
|
||||
'end_date': max_date,
|
||||
}
|
||||
results = backtesting.backtest(backtest_conf)
|
||||
assert len(evaluate_result_multi(results, '5min', 1)) == 0
|
||||
|
||||
|
||||
def test_backtest_record(default_conf, fee, mocker):
|
||||
names = []
|
||||
records = []
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
mocker.patch(
|
||||
'freqtrade.optimize.backtesting.file_dump_json',
|
||||
new=lambda n, r: (names.append(n), records.append(r))
|
||||
)
|
||||
|
||||
backtesting = Backtesting(default_conf)
|
||||
results = pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC",
|
||||
"UNITTEST/BTC", "UNITTEST/BTC"],
|
||||
"profit_percent": [0.003312, 0.010801, 0.013803, 0.002780],
|
||||
"profit_abs": [0.000003, 0.000011, 0.000014, 0.000003],
|
||||
"open_time": [Arrow(2017, 11, 14, 19, 32, 00).datetime,
|
||||
Arrow(2017, 11, 14, 21, 36, 00).datetime,
|
||||
Arrow(2017, 11, 14, 22, 12, 00).datetime,
|
||||
Arrow(2017, 11, 14, 22, 44, 00).datetime],
|
||||
"close_time": [Arrow(2017, 11, 14, 21, 35, 00).datetime,
|
||||
Arrow(2017, 11, 14, 22, 10, 00).datetime,
|
||||
Arrow(2017, 11, 14, 22, 43, 00).datetime,
|
||||
Arrow(2017, 11, 14, 22, 58, 00).datetime],
|
||||
"open_rate": [0.002543, 0.003003, 0.003089, 0.003214],
|
||||
"close_rate": [0.002546, 0.003014, 0.003103, 0.003217],
|
||||
"open_index": [1, 119, 153, 185],
|
||||
"close_index": [118, 151, 184, 199],
|
||||
"trade_duration": [123, 34, 31, 14],
|
||||
"open_at_end": [False, False, False, True],
|
||||
"sell_reason": [SellType.ROI, SellType.STOP_LOSS,
|
||||
SellType.ROI, SellType.FORCE_SELL]
|
||||
})
|
||||
backtesting._store_backtest_result("backtest-result.json", results)
|
||||
assert len(results) == 4
|
||||
# Assert file_dump_json was only called once
|
||||
assert names == ['backtest-result.json']
|
||||
records = records[0]
|
||||
# Ensure records are of correct type
|
||||
assert len(records) == 4
|
||||
|
||||
# reset test to test with strategy name
|
||||
names = []
|
||||
records = []
|
||||
backtesting._store_backtest_result(Path("backtest-result.json"), results, "DefStrat")
|
||||
assert len(results) == 4
|
||||
# Assert file_dump_json was only called once
|
||||
assert names == [Path('backtest-result-DefStrat.json')]
|
||||
records = records[0]
|
||||
# Ensure records are of correct type
|
||||
assert len(records) == 4
|
||||
|
||||
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
|
||||
# Below follows just a typecheck of the schema/type of trade-records
|
||||
oix = None
|
||||
for (pair, profit, date_buy, date_sell, buy_index, dur,
|
||||
openr, closer, open_at_end, sell_reason) in records:
|
||||
assert pair == 'UNITTEST/BTC'
|
||||
assert isinstance(profit, float)
|
||||
# FIX: buy/sell should be converted to ints
|
||||
assert isinstance(date_buy, float)
|
||||
assert isinstance(date_sell, float)
|
||||
assert isinstance(openr, float)
|
||||
assert isinstance(closer, float)
|
||||
assert isinstance(open_at_end, bool)
|
||||
assert isinstance(sell_reason, str)
|
||||
isinstance(buy_index, pd._libs.tslib.Timestamp)
|
||||
if oix:
|
||||
assert buy_index > oix
|
||||
oix = buy_index
|
||||
assert dur > 0
|
||||
|
||||
|
||||
def test_backtest_start_timerange(default_conf, mocker, caplog):
|
||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||
|
||||
async def load_pairs(pair, timeframe, since):
|
||||
return _load_pair_as_ticks(pair, timeframe)
|
||||
|
||||
api_mock = MagicMock()
|
||||
api_mock.fetch_ohlcv = load_pairs
|
||||
|
||||
patch_exchange(mocker, api_mock)
|
||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock())
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'--datadir', 'freqtrade/tests/testdata',
|
||||
'backtesting',
|
||||
'--ticker-interval', '1m',
|
||||
'--timerange', '-100',
|
||||
'--enable-position-stacking',
|
||||
'--disable-max-market-positions'
|
||||
]
|
||||
args = get_args(args)
|
||||
start_backtesting(args)
|
||||
# check the logs, that will contain the backtest result
|
||||
exists = [
|
||||
'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
||||
'Parameter --timerange detected: -100 ...',
|
||||
'Using data directory: freqtrade/tests/testdata ...',
|
||||
'Using stake_currency: BTC ...',
|
||||
'Using stake_amount: 0.001 ...',
|
||||
'Backtesting with data from 2017-11-14T21:17:00+00:00 '
|
||||
'up to 2017-11-14T22:58:00+00:00 (0 days)..',
|
||||
'Parameter --enable-position-stacking detected ...'
|
||||
]
|
||||
|
||||
for line in exists:
|
||||
assert log_has(line, caplog)
|
||||
|
||||
|
||||
def test_backtest_start_multi_strat(default_conf, mocker, caplog):
|
||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||
|
||||
async def load_pairs(pair, timeframe, since):
|
||||
return _load_pair_as_ticks(pair, timeframe)
|
||||
api_mock = MagicMock()
|
||||
api_mock.fetch_ohlcv = load_pairs
|
||||
|
||||
patch_exchange(mocker, api_mock)
|
||||
backtestmock = MagicMock()
|
||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
||||
gen_table_mock = MagicMock()
|
||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', gen_table_mock)
|
||||
gen_strattable_mock = MagicMock()
|
||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table_strategy',
|
||||
gen_strattable_mock)
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'--datadir', 'freqtrade/tests/testdata',
|
||||
'backtesting',
|
||||
'--ticker-interval', '1m',
|
||||
'--timerange', '-100',
|
||||
'--enable-position-stacking',
|
||||
'--disable-max-market-positions',
|
||||
'--strategy-list',
|
||||
'DefaultStrategy',
|
||||
'SampleStrategy',
|
||||
]
|
||||
args = get_args(args)
|
||||
start_backtesting(args)
|
||||
# 2 backtests, 4 tables
|
||||
assert backtestmock.call_count == 2
|
||||
assert gen_table_mock.call_count == 4
|
||||
assert gen_strattable_mock.call_count == 1
|
||||
|
||||
# check the logs, that will contain the backtest result
|
||||
exists = [
|
||||
'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
||||
'Parameter --timerange detected: -100 ...',
|
||||
'Using data directory: freqtrade/tests/testdata ...',
|
||||
'Using stake_currency: BTC ...',
|
||||
'Using stake_amount: 0.001 ...',
|
||||
'Backtesting with data from 2017-11-14T21:17:00+00:00 '
|
||||
'up to 2017-11-14T22:58:00+00:00 (0 days)..',
|
||||
'Parameter --enable-position-stacking detected ...',
|
||||
'Running backtesting for Strategy DefaultStrategy',
|
||||
'Running backtesting for Strategy SampleStrategy',
|
||||
]
|
||||
|
||||
for line in exists:
|
||||
assert log_has(line, caplog)
|
121
tests/optimize/test_edge_cli.py
Normal file
121
tests/optimize/test_edge_cli.py
Normal file
@@ -0,0 +1,121 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103, C0330
|
||||
# pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.edge import PairInfo
|
||||
from freqtrade.optimize import setup_configuration, start_edge
|
||||
from freqtrade.optimize.edge_cli import EdgeCli
|
||||
from freqtrade.state import RunMode
|
||||
from freqtrade.tests.conftest import (get_args, log_has, log_has_re,
|
||||
patch_exchange,
|
||||
patched_configuration_load_config_file)
|
||||
|
||||
|
||||
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'edge'
|
||||
]
|
||||
|
||||
config = setup_configuration(get_args(args), RunMode.EDGE)
|
||||
assert config['runmode'] == RunMode.EDGE
|
||||
|
||||
assert 'max_open_trades' in config
|
||||
assert 'stake_currency' in config
|
||||
assert 'stake_amount' in config
|
||||
assert 'exchange' in config
|
||||
assert 'pair_whitelist' in config['exchange']
|
||||
assert 'datadir' in config
|
||||
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||
assert 'ticker_interval' in config
|
||||
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
|
||||
|
||||
assert 'refresh_pairs' not in config
|
||||
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
|
||||
|
||||
assert 'timerange' not in config
|
||||
assert 'stoploss_range' not in config
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:DEPRECATED")
|
||||
def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> None:
|
||||
patched_configuration_load_config_file(mocker, edge_conf)
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.configuration.create_datadir',
|
||||
lambda c, x: x
|
||||
)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'--datadir', '/foo/bar',
|
||||
'edge',
|
||||
'--ticker-interval', '1m',
|
||||
'--refresh-pairs-cached',
|
||||
'--timerange', ':100',
|
||||
'--stoplosses=-0.01,-0.10,-0.001'
|
||||
]
|
||||
|
||||
config = setup_configuration(get_args(args), RunMode.EDGE)
|
||||
assert 'max_open_trades' in config
|
||||
assert 'stake_currency' in config
|
||||
assert 'stake_amount' in config
|
||||
assert 'exchange' in config
|
||||
assert 'pair_whitelist' in config['exchange']
|
||||
assert 'datadir' in config
|
||||
assert config['runmode'] == RunMode.EDGE
|
||||
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||
assert 'ticker_interval' in config
|
||||
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||
caplog)
|
||||
|
||||
assert 'refresh_pairs' in config
|
||||
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
|
||||
assert 'timerange' in config
|
||||
assert log_has('Parameter --timerange detected: {} ...'.format(config['timerange']), caplog)
|
||||
|
||||
|
||||
def test_start(mocker, fee, edge_conf, caplog) -> None:
|
||||
start_mock = MagicMock()
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.optimize.edge_cli.EdgeCli.start', start_mock)
|
||||
patched_configuration_load_config_file(mocker, edge_conf)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'edge'
|
||||
]
|
||||
args = get_args(args)
|
||||
start_edge(args)
|
||||
assert log_has('Starting freqtrade in Edge mode', caplog)
|
||||
assert start_mock.call_count == 1
|
||||
|
||||
|
||||
def test_edge_init(mocker, edge_conf) -> None:
|
||||
patch_exchange(mocker)
|
||||
edge_conf['stake_amount'] = 20
|
||||
edge_cli = EdgeCli(edge_conf)
|
||||
assert edge_cli.config == edge_conf
|
||||
assert edge_cli.config['stake_amount'] == 'unlimited'
|
||||
assert callable(edge_cli.edge.calculate)
|
||||
|
||||
|
||||
def test_generate_edge_table(edge_conf, mocker):
|
||||
patch_exchange(mocker)
|
||||
edge_cli = EdgeCli(edge_conf)
|
||||
|
||||
results = {}
|
||||
results['ETH/BTC'] = PairInfo(-0.01, 0.60, 2, 1, 3, 10, 60)
|
||||
|
||||
assert edge_cli._generate_edge_table(results).count(':|') == 7
|
||||
assert edge_cli._generate_edge_table(results).count('| ETH/BTC |') == 1
|
||||
assert edge_cli._generate_edge_table(results).count(
|
||||
'| risk reward ratio | required risk reward | expectancy |') == 1
|
875
tests/optimize/test_hyperopt.py
Normal file
875
tests/optimize/test_hyperopt.py
Normal file
@@ -0,0 +1,875 @@
|
||||
# pragma pylint: disable=missing-docstring,W0212,C0103
|
||||
import os
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
from arrow import Arrow
|
||||
from filelock import Timeout
|
||||
from pathlib import Path
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.data.converter import parse_ticker_dataframe
|
||||
from freqtrade.data.history import load_tickerdata_file
|
||||
from freqtrade.optimize import setup_configuration, start_hyperopt
|
||||
from freqtrade.optimize.default_hyperopt import DefaultHyperOpts
|
||||
from freqtrade.optimize.default_hyperopt_loss import DefaultHyperOptLoss
|
||||
from freqtrade.optimize.hyperopt import Hyperopt
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver
|
||||
from freqtrade.state import RunMode
|
||||
from freqtrade.strategy.interface import SellType
|
||||
from freqtrade.tests.conftest import (get_args, log_has, log_has_re,
|
||||
patch_exchange,
|
||||
patched_configuration_load_config_file)
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def hyperopt(default_conf, mocker):
|
||||
default_conf.update({'spaces': ['all']})
|
||||
patch_exchange(mocker)
|
||||
return Hyperopt(default_conf)
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def hyperopt_results():
|
||||
return pd.DataFrame(
|
||||
{
|
||||
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
|
||||
'profit_percent': [0.1, 0.2, 0.3],
|
||||
'profit_abs': [0.2, 0.4, 0.5],
|
||||
'trade_duration': [10, 30, 10],
|
||||
'profit': [2, 0, 0],
|
||||
'loss': [0, 0, 1],
|
||||
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Functions for recurrent object patching
|
||||
def create_trials(mocker, hyperopt) -> None:
|
||||
"""
|
||||
When creating trials, mock the hyperopt Trials so that *by default*
|
||||
- we don't create any pickle'd files in the filesystem
|
||||
- we might have a pickle'd file so make sure that we return
|
||||
false when looking for it
|
||||
"""
|
||||
hyperopt.trials_file = Path('freqtrade/tests/optimize/ut_trials.pickle')
|
||||
|
||||
mocker.patch.object(Path, "is_file", MagicMock(return_value=False))
|
||||
stat_mock = MagicMock()
|
||||
stat_mock.st_size = PropertyMock(return_value=1)
|
||||
mocker.patch.object(Path, "stat", MagicMock(return_value=False))
|
||||
|
||||
mocker.patch.object(Path, "unlink", MagicMock(return_value=True))
|
||||
mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
|
||||
|
||||
return [{'loss': 1, 'result': 'foo', 'params': {}}]
|
||||
|
||||
|
||||
def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'hyperopt'
|
||||
]
|
||||
|
||||
config = setup_configuration(get_args(args), RunMode.HYPEROPT)
|
||||
assert 'max_open_trades' in config
|
||||
assert 'stake_currency' in config
|
||||
assert 'stake_amount' in config
|
||||
assert 'exchange' in config
|
||||
assert 'pair_whitelist' in config['exchange']
|
||||
assert 'datadir' in config
|
||||
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||
assert 'ticker_interval' in config
|
||||
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
|
||||
|
||||
assert 'position_stacking' not in config
|
||||
assert not log_has('Parameter --enable-position-stacking detected ...', caplog)
|
||||
|
||||
assert 'refresh_pairs' not in config
|
||||
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
|
||||
|
||||
assert 'timerange' not in config
|
||||
assert 'runmode' in config
|
||||
assert config['runmode'] == RunMode.HYPEROPT
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:DEPRECATED")
|
||||
def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.configuration.create_datadir',
|
||||
lambda c, x: x
|
||||
)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'--datadir', '/foo/bar',
|
||||
'hyperopt',
|
||||
'--ticker-interval', '1m',
|
||||
'--timerange', ':100',
|
||||
'--refresh-pairs-cached',
|
||||
'--enable-position-stacking',
|
||||
'--disable-max-market-positions',
|
||||
'--epochs', '1000',
|
||||
'--spaces', 'all',
|
||||
'--print-all'
|
||||
]
|
||||
|
||||
config = setup_configuration(get_args(args), RunMode.HYPEROPT)
|
||||
assert 'max_open_trades' in config
|
||||
assert 'stake_currency' in config
|
||||
assert 'stake_amount' in config
|
||||
assert 'exchange' in config
|
||||
assert 'pair_whitelist' in config['exchange']
|
||||
assert 'datadir' in config
|
||||
assert config['runmode'] == RunMode.HYPEROPT
|
||||
|
||||
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||
assert 'ticker_interval' in config
|
||||
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||
caplog)
|
||||
|
||||
assert 'position_stacking' in config
|
||||
assert log_has('Parameter --enable-position-stacking detected ...', caplog)
|
||||
|
||||
assert 'use_max_market_positions' in config
|
||||
assert log_has('Parameter --disable-max-market-positions detected ...', caplog)
|
||||
assert log_has('max_open_trades set to unlimited ...', caplog)
|
||||
|
||||
assert 'refresh_pairs' in config
|
||||
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
|
||||
|
||||
assert 'timerange' in config
|
||||
assert log_has('Parameter --timerange detected: {} ...'.format(config['timerange']), caplog)
|
||||
|
||||
assert 'epochs' in config
|
||||
assert log_has('Parameter --epochs detected ... Will run Hyperopt with for 1000 epochs ...',
|
||||
caplog)
|
||||
|
||||
assert 'spaces' in config
|
||||
assert log_has('Parameter -s/--spaces detected: {}'.format(config['spaces']), caplog)
|
||||
assert 'print_all' in config
|
||||
assert log_has('Parameter --print-all detected ...', caplog)
|
||||
|
||||
|
||||
def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
hyperopts = DefaultHyperOpts
|
||||
delattr(hyperopts, 'populate_buy_trend')
|
||||
delattr(hyperopts, 'populate_sell_trend')
|
||||
mocker.patch(
|
||||
'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver._load_hyperopt',
|
||||
MagicMock(return_value=hyperopts)
|
||||
)
|
||||
x = HyperOptResolver(default_conf, ).hyperopt
|
||||
assert not hasattr(x, 'populate_buy_trend')
|
||||
assert not hasattr(x, 'populate_sell_trend')
|
||||
assert log_has("Custom Hyperopt does not provide populate_sell_trend. "
|
||||
"Using populate_sell_trend from DefaultStrategy.", caplog)
|
||||
assert log_has("Custom Hyperopt does not provide populate_buy_trend. "
|
||||
"Using populate_buy_trend from DefaultStrategy.", caplog)
|
||||
assert hasattr(x, "ticker_interval")
|
||||
|
||||
|
||||
def test_hyperoptresolver_wrongname(mocker, default_conf, caplog) -> None:
|
||||
default_conf.update({'hyperopt': "NonExistingHyperoptClass"})
|
||||
|
||||
with pytest.raises(OperationalException, match=r'Impossible to load Hyperopt.*'):
|
||||
HyperOptResolver(default_conf, ).hyperopt
|
||||
|
||||
|
||||
def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None:
|
||||
|
||||
hl = DefaultHyperOptLoss
|
||||
mocker.patch(
|
||||
'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver._load_hyperoptloss',
|
||||
MagicMock(return_value=hl)
|
||||
)
|
||||
x = HyperOptLossResolver(default_conf, ).hyperoptloss
|
||||
assert hasattr(x, "hyperopt_loss_function")
|
||||
|
||||
|
||||
def test_hyperoptlossresolver_wrongname(mocker, default_conf, caplog) -> None:
|
||||
default_conf.update({'hyperopt_loss': "NonExistingLossClass"})
|
||||
|
||||
with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'):
|
||||
HyperOptLossResolver(default_conf, ).hyperopt
|
||||
|
||||
|
||||
def test_start(mocker, default_conf, caplog) -> None:
|
||||
start_mock = MagicMock()
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
|
||||
patch_exchange(mocker)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'hyperopt',
|
||||
'--epochs', '5'
|
||||
]
|
||||
args = get_args(args)
|
||||
start_hyperopt(args)
|
||||
|
||||
import pprint
|
||||
pprint.pprint(caplog.record_tuples)
|
||||
|
||||
assert log_has('Starting freqtrade in Hyperopt mode', caplog)
|
||||
assert start_mock.call_count == 1
|
||||
|
||||
|
||||
def test_start_no_data(mocker, default_conf, caplog) -> None:
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock(return_value={}))
|
||||
mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.get_timeframe',
|
||||
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
|
||||
)
|
||||
|
||||
patch_exchange(mocker)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'hyperopt',
|
||||
'--epochs', '5'
|
||||
]
|
||||
args = get_args(args)
|
||||
start_hyperopt(args)
|
||||
|
||||
import pprint
|
||||
pprint.pprint(caplog.record_tuples)
|
||||
|
||||
assert log_has('No data found. Terminating.', caplog)
|
||||
|
||||
|
||||
def test_start_filelock(mocker, default_conf, caplog) -> None:
|
||||
start_mock = MagicMock(side_effect=Timeout(Hyperopt.get_lock_filename(default_conf)))
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
|
||||
patch_exchange(mocker)
|
||||
|
||||
args = [
|
||||
'--config', 'config.json',
|
||||
'hyperopt',
|
||||
'--epochs', '5'
|
||||
]
|
||||
args = get_args(args)
|
||||
start_hyperopt(args)
|
||||
assert log_has("Another running instance of freqtrade Hyperopt detected.", caplog)
|
||||
|
||||
|
||||
def test_loss_calculation_prefer_correct_trade_count(default_conf, hyperopt_results) -> None:
|
||||
hl = HyperOptLossResolver(default_conf).hyperoptloss
|
||||
correct = hl.hyperopt_loss_function(hyperopt_results, 600)
|
||||
over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100)
|
||||
under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100)
|
||||
assert over > correct
|
||||
assert under > correct
|
||||
|
||||
|
||||
def test_loss_calculation_prefer_shorter_trades(default_conf, hyperopt_results) -> None:
|
||||
resultsb = hyperopt_results.copy()
|
||||
resultsb.loc[1, 'trade_duration'] = 20
|
||||
|
||||
hl = HyperOptLossResolver(default_conf).hyperoptloss
|
||||
longer = hl.hyperopt_loss_function(hyperopt_results, 100)
|
||||
shorter = hl.hyperopt_loss_function(resultsb, 100)
|
||||
assert shorter < longer
|
||||
|
||||
|
||||
def test_loss_calculation_has_limited_profit(default_conf, hyperopt_results) -> None:
|
||||
results_over = hyperopt_results.copy()
|
||||
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
|
||||
results_under = hyperopt_results.copy()
|
||||
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
|
||||
|
||||
hl = HyperOptLossResolver(default_conf).hyperoptloss
|
||||
correct = hl.hyperopt_loss_function(hyperopt_results, 600)
|
||||
over = hl.hyperopt_loss_function(results_over, 600)
|
||||
under = hl.hyperopt_loss_function(results_under, 600)
|
||||
assert over < correct
|
||||
assert under > correct
|
||||
|
||||
|
||||
def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
|
||||
results_over = hyperopt_results.copy()
|
||||
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
|
||||
results_under = hyperopt_results.copy()
|
||||
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
|
||||
|
||||
default_conf.update({'hyperopt_loss': 'SharpeHyperOptLoss'})
|
||||
hl = HyperOptLossResolver(default_conf).hyperoptloss
|
||||
correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results),
|
||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||
over = hl.hyperopt_loss_function(results_over, len(hyperopt_results),
|
||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||
under = hl.hyperopt_loss_function(results_under, len(hyperopt_results),
|
||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||
assert over < correct
|
||||
assert under > correct
|
||||
|
||||
|
||||
def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
|
||||
results_over = hyperopt_results.copy()
|
||||
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
|
||||
results_under = hyperopt_results.copy()
|
||||
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
|
||||
|
||||
default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'})
|
||||
hl = HyperOptLossResolver(default_conf).hyperoptloss
|
||||
correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results),
|
||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||
over = hl.hyperopt_loss_function(results_over, len(hyperopt_results),
|
||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||
under = hl.hyperopt_loss_function(results_under, len(hyperopt_results),
|
||||
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||
assert over < correct
|
||||
assert under > correct
|
||||
|
||||
|
||||
def test_log_results_if_loss_improves(hyperopt, capsys) -> None:
|
||||
hyperopt.current_best_loss = 2
|
||||
hyperopt.total_epochs = 2
|
||||
hyperopt.log_results(
|
||||
{
|
||||
'loss': 1,
|
||||
'current_epoch': 1,
|
||||
'results_explanation': 'foo.',
|
||||
'is_initial_point': False
|
||||
}
|
||||
)
|
||||
out, err = capsys.readouterr()
|
||||
assert ' 2/2: foo. Objective: 1.00000' in out
|
||||
|
||||
|
||||
def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None:
|
||||
hyperopt.current_best_loss = 2
|
||||
hyperopt.log_results(
|
||||
{
|
||||
'loss': 3,
|
||||
}
|
||||
)
|
||||
assert caplog.record_tuples == []
|
||||
|
||||
|
||||
def test_save_trials_saves_trials(mocker, hyperopt, caplog) -> None:
|
||||
trials = create_trials(mocker, hyperopt)
|
||||
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
|
||||
hyperopt.trials = trials
|
||||
hyperopt.save_trials()
|
||||
|
||||
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
||||
assert log_has("Saving 1 evaluations to '{}'".format(trials_file), caplog)
|
||||
mock_dump.assert_called_once()
|
||||
|
||||
|
||||
def test_read_trials_returns_trials_file(mocker, hyperopt, caplog) -> None:
|
||||
trials = create_trials(mocker, hyperopt)
|
||||
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials)
|
||||
hyperopt_trial = hyperopt.read_trials()
|
||||
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
||||
assert log_has("Reading Trials from '{}'".format(trials_file), caplog)
|
||||
assert hyperopt_trial == trials
|
||||
mock_load.assert_called_once()
|
||||
|
||||
|
||||
def test_roi_table_generation(hyperopt) -> None:
|
||||
params = {
|
||||
'roi_t1': 5,
|
||||
'roi_t2': 10,
|
||||
'roi_t3': 15,
|
||||
'roi_p1': 1,
|
||||
'roi_p2': 2,
|
||||
'roi_p3': 3,
|
||||
}
|
||||
|
||||
assert hyperopt.custom_hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
|
||||
|
||||
|
||||
def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
|
||||
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||
mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.get_timeframe',
|
||||
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
|
||||
)
|
||||
|
||||
parallel = mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
|
||||
MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result',
|
||||
'params': {'buy': {}, 'sell': {}, 'roi': {}, 'stoploss': 0.0}}])
|
||||
)
|
||||
patch_exchange(mocker)
|
||||
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': 'all',
|
||||
'hyperopt_jobs': 1, })
|
||||
|
||||
hyperopt = Hyperopt(default_conf)
|
||||
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
|
||||
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
||||
|
||||
hyperopt.start()
|
||||
|
||||
parallel.assert_called_once()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out
|
||||
assert dumper.called
|
||||
# Should be called twice, once for tickerdata, once to save evaluations
|
||||
assert dumper.call_count == 2
|
||||
assert hasattr(hyperopt.backtesting, "advise_sell")
|
||||
assert hasattr(hyperopt.backtesting, "advise_buy")
|
||||
assert hasattr(hyperopt, "max_open_trades")
|
||||
assert hyperopt.max_open_trades == default_conf['max_open_trades']
|
||||
assert hasattr(hyperopt, "position_stacking")
|
||||
|
||||
|
||||
def test_format_results(hyperopt):
|
||||
# Test with BTC as stake_currency
|
||||
trades = [
|
||||
('ETH/BTC', 2, 2, 123),
|
||||
('LTC/BTC', 1, 1, 123),
|
||||
('XPR/BTC', -1, -2, -246)
|
||||
]
|
||||
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
|
||||
df = pd.DataFrame.from_records(trades, columns=labels)
|
||||
|
||||
result = hyperopt.format_results(df)
|
||||
assert result.find(' 66.67%')
|
||||
assert result.find('Total profit 1.00000000 BTC')
|
||||
assert result.find('2.0000Σ %')
|
||||
|
||||
# Test with EUR as stake_currency
|
||||
trades = [
|
||||
('ETH/EUR', 2, 2, 123),
|
||||
('LTC/EUR', 1, 1, 123),
|
||||
('XPR/EUR', -1, -2, -246)
|
||||
]
|
||||
df = pd.DataFrame.from_records(trades, columns=labels)
|
||||
result = hyperopt.format_results(df)
|
||||
assert result.find('Total profit 1.00000000 EUR')
|
||||
|
||||
|
||||
def test_has_space(hyperopt):
|
||||
hyperopt.config.update({'spaces': ['buy', 'roi']})
|
||||
assert hyperopt.has_space('roi')
|
||||
assert hyperopt.has_space('buy')
|
||||
assert not hyperopt.has_space('stoploss')
|
||||
|
||||
hyperopt.config.update({'spaces': ['all']})
|
||||
assert hyperopt.has_space('buy')
|
||||
|
||||
|
||||
def test_populate_indicators(hyperopt, testdatadir) -> None:
|
||||
tick = load_tickerdata_file(testdatadir, 'UNITTEST/BTC', '1m')
|
||||
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC",
|
||||
fill_missing=True)}
|
||||
dataframes = hyperopt.backtesting.strategy.tickerdata_to_dataframe(tickerlist)
|
||||
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
|
||||
{'pair': 'UNITTEST/BTC'})
|
||||
|
||||
# Check if some indicators are generated. We will not test all of them
|
||||
assert 'adx' in dataframe
|
||||
assert 'mfi' in dataframe
|
||||
assert 'rsi' in dataframe
|
||||
|
||||
|
||||
def test_buy_strategy_generator(hyperopt, testdatadir) -> None:
|
||||
tick = load_tickerdata_file(testdatadir, 'UNITTEST/BTC', '1m')
|
||||
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC",
|
||||
fill_missing=True)}
|
||||
dataframes = hyperopt.backtesting.strategy.tickerdata_to_dataframe(tickerlist)
|
||||
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
|
||||
{'pair': 'UNITTEST/BTC'})
|
||||
|
||||
populate_buy_trend = hyperopt.custom_hyperopt.buy_strategy_generator(
|
||||
{
|
||||
'adx-value': 20,
|
||||
'fastd-value': 20,
|
||||
'mfi-value': 20,
|
||||
'rsi-value': 20,
|
||||
'adx-enabled': True,
|
||||
'fastd-enabled': True,
|
||||
'mfi-enabled': True,
|
||||
'rsi-enabled': True,
|
||||
'trigger': 'bb_lower'
|
||||
}
|
||||
)
|
||||
result = populate_buy_trend(dataframe, {'pair': 'UNITTEST/BTC'})
|
||||
# Check if some indicators are generated. We will not test all of them
|
||||
assert 'buy' in result
|
||||
assert 1 in result['buy']
|
||||
|
||||
|
||||
def test_generate_optimizer(mocker, default_conf) -> None:
|
||||
default_conf.update({'config': 'config.json.example'})
|
||||
default_conf.update({'timerange': None})
|
||||
default_conf.update({'spaces': 'all'})
|
||||
default_conf.update({'hyperopt_min_trades': 1})
|
||||
|
||||
trades = [
|
||||
('POWR/BTC', 0.023117, 0.000233, 100)
|
||||
]
|
||||
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
|
||||
backtest_result = pd.DataFrame.from_records(trades, columns=labels)
|
||||
|
||||
mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.Backtesting.backtest',
|
||||
MagicMock(return_value=backtest_result)
|
||||
)
|
||||
mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.get_timeframe',
|
||||
MagicMock(return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13)))
|
||||
)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock())
|
||||
|
||||
optimizer_param = {
|
||||
'adx-value': 0,
|
||||
'fastd-value': 35,
|
||||
'mfi-value': 0,
|
||||
'rsi-value': 0,
|
||||
'adx-enabled': False,
|
||||
'fastd-enabled': True,
|
||||
'mfi-enabled': False,
|
||||
'rsi-enabled': False,
|
||||
'trigger': 'macd_cross_signal',
|
||||
'sell-adx-value': 0,
|
||||
'sell-fastd-value': 75,
|
||||
'sell-mfi-value': 0,
|
||||
'sell-rsi-value': 0,
|
||||
'sell-adx-enabled': False,
|
||||
'sell-fastd-enabled': True,
|
||||
'sell-mfi-enabled': False,
|
||||
'sell-rsi-enabled': False,
|
||||
'sell-trigger': 'macd_cross_signal',
|
||||
'roi_t1': 60.0,
|
||||
'roi_t2': 30.0,
|
||||
'roi_t3': 20.0,
|
||||
'roi_p1': 0.01,
|
||||
'roi_p2': 0.01,
|
||||
'roi_p3': 0.1,
|
||||
'stoploss': -0.4,
|
||||
}
|
||||
response_expected = {
|
||||
'loss': 1.9840569076926293,
|
||||
'results_explanation': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC '
|
||||
'( 2.31Σ%). Avg duration 100.0 mins.',
|
||||
'params': optimizer_param,
|
||||
'total_profit': 0.00023300
|
||||
}
|
||||
|
||||
hyperopt = Hyperopt(default_conf)
|
||||
generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values()))
|
||||
assert generate_optimizer_value == response_expected
|
||||
|
||||
|
||||
def test_clean_hyperopt(mocker, default_conf, caplog):
|
||||
patch_exchange(mocker)
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': 'all',
|
||||
'hyperopt_jobs': 1,
|
||||
})
|
||||
mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True))
|
||||
unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock())
|
||||
h = Hyperopt(default_conf)
|
||||
|
||||
assert unlinkmock.call_count == 2
|
||||
assert log_has(f"Removing `{h.tickerdata_pickle}`.", caplog)
|
||||
|
||||
|
||||
def test_continue_hyperopt(mocker, default_conf, caplog):
|
||||
patch_exchange(mocker)
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': 'all',
|
||||
'hyperopt_jobs': 1,
|
||||
'hyperopt_continue': True
|
||||
})
|
||||
mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True))
|
||||
unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock())
|
||||
Hyperopt(default_conf)
|
||||
|
||||
assert unlinkmock.call_count == 0
|
||||
assert log_has(f"Continuing on previous hyperopt results.", caplog)
|
||||
|
||||
|
||||
def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:
|
||||
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||
mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.get_timeframe',
|
||||
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
|
||||
)
|
||||
|
||||
parallel = mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
|
||||
MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}])
|
||||
)
|
||||
patch_exchange(mocker)
|
||||
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': 'all',
|
||||
'hyperopt_jobs': 1,
|
||||
'print_json': True,
|
||||
})
|
||||
|
||||
hyperopt = Hyperopt(default_conf)
|
||||
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
|
||||
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
||||
|
||||
hyperopt.start()
|
||||
|
||||
parallel.assert_called_once()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
assert '{"params":{"mfi-value":null,"fastd-value":null,"adx-value":null,"rsi-value":null,"mfi-enabled":null,"fastd-enabled":null,"adx-enabled":null,"rsi-enabled":null,"trigger":null,"sell-mfi-value":null,"sell-fastd-value":null,"sell-adx-value":null,"sell-rsi-value":null,"sell-mfi-enabled":null,"sell-fastd-enabled":null,"sell-adx-enabled":null,"sell-rsi-enabled":null,"sell-trigger":null},"minimal_roi":{},"stoploss":null}' in out # noqa: E501
|
||||
assert dumper.called
|
||||
# Should be called twice, once for tickerdata, once to save evaluations
|
||||
assert dumper.call_count == 2
|
||||
|
||||
|
||||
def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> None:
|
||||
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||
mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.get_timeframe',
|
||||
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
|
||||
)
|
||||
|
||||
parallel = mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
|
||||
MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}])
|
||||
)
|
||||
patch_exchange(mocker)
|
||||
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': 'roi stoploss',
|
||||
'hyperopt_jobs': 1,
|
||||
'print_json': True,
|
||||
})
|
||||
|
||||
hyperopt = Hyperopt(default_conf)
|
||||
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
|
||||
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
||||
|
||||
hyperopt.start()
|
||||
|
||||
parallel.assert_called_once()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
assert '{"minimal_roi":{},"stoploss":null}' in out
|
||||
assert dumper.called
|
||||
# Should be called twice, once for tickerdata, once to save evaluations
|
||||
assert dumper.call_count == 2
|
||||
|
||||
|
||||
def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys) -> None:
|
||||
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||
mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.get_timeframe',
|
||||
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
|
||||
)
|
||||
|
||||
parallel = mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
|
||||
MagicMock(return_value=[{
|
||||
'loss': 1, 'results_explanation': 'foo result', 'params': {'stoploss': 0.0}}])
|
||||
)
|
||||
patch_exchange(mocker)
|
||||
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': 'roi stoploss',
|
||||
'hyperopt_jobs': 1, })
|
||||
|
||||
hyperopt = Hyperopt(default_conf)
|
||||
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
|
||||
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
||||
|
||||
del hyperopt.custom_hyperopt.__class__.buy_strategy_generator
|
||||
del hyperopt.custom_hyperopt.__class__.sell_strategy_generator
|
||||
del hyperopt.custom_hyperopt.__class__.indicator_space
|
||||
del hyperopt.custom_hyperopt.__class__.sell_indicator_space
|
||||
|
||||
hyperopt.start()
|
||||
|
||||
parallel.assert_called_once()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out
|
||||
assert dumper.called
|
||||
# Should be called twice, once for tickerdata, once to save evaluations
|
||||
assert dumper.call_count == 2
|
||||
assert hasattr(hyperopt.backtesting, "advise_sell")
|
||||
assert hasattr(hyperopt.backtesting, "advise_buy")
|
||||
assert hasattr(hyperopt, "max_open_trades")
|
||||
assert hyperopt.max_open_trades == default_conf['max_open_trades']
|
||||
assert hasattr(hyperopt, "position_stacking")
|
||||
|
||||
|
||||
def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) -> None:
|
||||
mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||
mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.get_timeframe',
|
||||
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
|
||||
)
|
||||
|
||||
patch_exchange(mocker)
|
||||
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': 'all',
|
||||
'hyperopt_jobs': 1, })
|
||||
|
||||
hyperopt = Hyperopt(default_conf)
|
||||
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
|
||||
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
||||
|
||||
del hyperopt.custom_hyperopt.__class__.buy_strategy_generator
|
||||
del hyperopt.custom_hyperopt.__class__.sell_strategy_generator
|
||||
del hyperopt.custom_hyperopt.__class__.indicator_space
|
||||
del hyperopt.custom_hyperopt.__class__.sell_indicator_space
|
||||
|
||||
with pytest.raises(OperationalException, match=r"The 'buy' space is included into *"):
|
||||
hyperopt.start()
|
||||
|
||||
|
||||
def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None:
|
||||
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||
mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.get_timeframe',
|
||||
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
|
||||
)
|
||||
|
||||
parallel = mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
|
||||
MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}])
|
||||
)
|
||||
patch_exchange(mocker)
|
||||
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': 'buy',
|
||||
'hyperopt_jobs': 1, })
|
||||
|
||||
hyperopt = Hyperopt(default_conf)
|
||||
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
|
||||
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
||||
|
||||
# TODO: sell_strategy_generator() is actually not called because
|
||||
# run_optimizer_parallel() is mocked
|
||||
del hyperopt.custom_hyperopt.__class__.sell_strategy_generator
|
||||
del hyperopt.custom_hyperopt.__class__.sell_indicator_space
|
||||
|
||||
hyperopt.start()
|
||||
|
||||
parallel.assert_called_once()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out
|
||||
assert dumper.called
|
||||
# Should be called twice, once for tickerdata, once to save evaluations
|
||||
assert dumper.call_count == 2
|
||||
assert hasattr(hyperopt.backtesting, "advise_sell")
|
||||
assert hasattr(hyperopt.backtesting, "advise_buy")
|
||||
assert hasattr(hyperopt, "max_open_trades")
|
||||
assert hyperopt.max_open_trades == default_conf['max_open_trades']
|
||||
assert hasattr(hyperopt, "position_stacking")
|
||||
|
||||
|
||||
def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None:
|
||||
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||
mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.get_timeframe',
|
||||
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
|
||||
)
|
||||
|
||||
parallel = mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
|
||||
MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}])
|
||||
)
|
||||
patch_exchange(mocker)
|
||||
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': 'sell',
|
||||
'hyperopt_jobs': 1, })
|
||||
|
||||
hyperopt = Hyperopt(default_conf)
|
||||
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
|
||||
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
||||
|
||||
# TODO: buy_strategy_generator() is actually not called because
|
||||
# run_optimizer_parallel() is mocked
|
||||
del hyperopt.custom_hyperopt.__class__.buy_strategy_generator
|
||||
del hyperopt.custom_hyperopt.__class__.indicator_space
|
||||
|
||||
hyperopt.start()
|
||||
|
||||
parallel.assert_called_once()
|
||||
|
||||
out, err = capsys.readouterr()
|
||||
assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out
|
||||
assert dumper.called
|
||||
# Should be called twice, once for tickerdata, once to save evaluations
|
||||
assert dumper.call_count == 2
|
||||
assert hasattr(hyperopt.backtesting, "advise_sell")
|
||||
assert hasattr(hyperopt.backtesting, "advise_buy")
|
||||
assert hasattr(hyperopt, "max_open_trades")
|
||||
assert hyperopt.max_open_trades == default_conf['max_open_trades']
|
||||
assert hasattr(hyperopt, "position_stacking")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method,space", [
|
||||
('buy_strategy_generator', 'buy'),
|
||||
('indicator_space', 'buy'),
|
||||
('sell_strategy_generator', 'sell'),
|
||||
('sell_indicator_space', 'sell'),
|
||||
])
|
||||
def test_simplified_interface_failed(mocker, default_conf, caplog, capsys, method, space) -> None:
|
||||
mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
|
||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||
mocker.patch(
|
||||
'freqtrade.optimize.hyperopt.get_timeframe',
|
||||
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
|
||||
)
|
||||
|
||||
patch_exchange(mocker)
|
||||
|
||||
default_conf.update({'config': 'config.json.example',
|
||||
'epochs': 1,
|
||||
'timerange': None,
|
||||
'spaces': space,
|
||||
'hyperopt_jobs': 1, })
|
||||
|
||||
hyperopt = Hyperopt(default_conf)
|
||||
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
|
||||
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
||||
|
||||
delattr(hyperopt.custom_hyperopt.__class__, method)
|
||||
|
||||
with pytest.raises(OperationalException, match=f"The '{space}' space is included into *"):
|
||||
hyperopt.start()
|
0
tests/pairlist/__init__.py
Normal file
0
tests/pairlist/__init__.py
Normal file
176
tests/pairlist/test_pairlist.py
Normal file
176
tests/pairlist/test_pairlist.py
Normal file
@@ -0,0 +1,176 @@
|
||||
# pragma pylint: disable=missing-docstring,C0103,protected-access
|
||||
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.constants import AVAILABLE_PAIRLISTS
|
||||
from freqtrade.resolvers import PairListResolver
|
||||
from freqtrade.tests.conftest import get_patched_freqtradebot
|
||||
import pytest
|
||||
|
||||
# whitelist, blacklist
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def whitelist_conf(default_conf):
|
||||
default_conf['stake_currency'] = 'BTC'
|
||||
default_conf['exchange']['pair_whitelist'] = [
|
||||
'ETH/BTC',
|
||||
'TKN/BTC',
|
||||
'TRST/BTC',
|
||||
'SWT/BTC',
|
||||
'BCC/BTC'
|
||||
]
|
||||
default_conf['exchange']['pair_blacklist'] = [
|
||||
'BLK/BTC'
|
||||
]
|
||||
default_conf['pairlist'] = {'method': 'StaticPairList',
|
||||
'config': {'number_assets': 3}
|
||||
}
|
||||
|
||||
return default_conf
|
||||
|
||||
|
||||
def test_load_pairlist_noexist(mocker, markets, default_conf):
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
||||
with pytest.raises(OperationalException,
|
||||
match=r"Impossible to load Pairlist 'NonexistingPairList'. "
|
||||
r"This class does not exist or contains Python code errors."):
|
||||
PairListResolver('NonexistingPairList', freqtradebot, default_conf).pairlist
|
||||
|
||||
|
||||
def test_refresh_market_pair_not_in_whitelist(mocker, markets, whitelist_conf):
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
||||
freqtradebot.pairlists.refresh_pairlist()
|
||||
# List ordered by BaseVolume
|
||||
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||
# Ensure all except those in whitelist are removed
|
||||
assert set(whitelist) == set(freqtradebot.pairlists.whitelist)
|
||||
# Ensure config dict hasn't been changed
|
||||
assert (whitelist_conf['exchange']['pair_whitelist'] ==
|
||||
freqtradebot.config['exchange']['pair_whitelist'])
|
||||
|
||||
|
||||
def test_refresh_pairlists(mocker, markets, whitelist_conf):
|
||||
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
||||
freqtradebot.pairlists.refresh_pairlist()
|
||||
# List ordered by BaseVolume
|
||||
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||
# Ensure all except those in whitelist are removed
|
||||
assert set(whitelist) == set(freqtradebot.pairlists.whitelist)
|
||||
assert whitelist_conf['exchange']['pair_blacklist'] == freqtradebot.pairlists.blacklist
|
||||
|
||||
|
||||
def test_refresh_pairlist_dynamic(mocker, markets, tickers, whitelist_conf):
|
||||
whitelist_conf['pairlist'] = {'method': 'VolumePairList',
|
||||
'config': {'number_assets': 5}
|
||||
}
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
markets=PropertyMock(return_value=markets),
|
||||
get_tickers=tickers,
|
||||
exchange_has=MagicMock(return_value=True)
|
||||
)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
|
||||
# argument: use the whitelist dynamically by exchange-volume
|
||||
whitelist = ['ETH/BTC', 'TKN/BTC', 'BTT/BTC']
|
||||
freqtradebot.pairlists.refresh_pairlist()
|
||||
|
||||
assert whitelist == freqtradebot.pairlists.whitelist
|
||||
|
||||
whitelist_conf['pairlist'] = {'method': 'VolumePairList',
|
||||
'config': {}
|
||||
}
|
||||
with pytest.raises(OperationalException,
|
||||
match=r'`number_assets` not specified. Please check your configuration '
|
||||
r'for "pairlist.config.number_assets"'):
|
||||
PairListResolver('VolumePairList', freqtradebot, whitelist_conf).pairlist
|
||||
|
||||
|
||||
def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
||||
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets_empty))
|
||||
|
||||
# argument: use the whitelist dynamically by exchange-volume
|
||||
whitelist = []
|
||||
whitelist_conf['exchange']['pair_whitelist'] = []
|
||||
freqtradebot.pairlists.refresh_pairlist()
|
||||
pairslist = whitelist_conf['exchange']['pair_whitelist']
|
||||
|
||||
assert set(whitelist) == set(pairslist)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("precision_filter,base_currency,key,whitelist_result", [
|
||||
(False, "BTC", "quoteVolume", ['ETH/BTC', 'TKN/BTC', 'BTT/BTC']),
|
||||
(False, "BTC", "bidVolume", ['BTT/BTC', 'TKN/BTC', 'ETH/BTC']),
|
||||
(False, "USDT", "quoteVolume", ['ETH/USDT', 'LTC/USDT']),
|
||||
(False, "ETH", "quoteVolume", []), # this replaces tests that were removed from test_exchange
|
||||
(True, "BTC", "quoteVolume", ["ETH/BTC", "TKN/BTC"]),
|
||||
(True, "BTC", "bidVolume", ["TKN/BTC", "ETH/BTC"])
|
||||
])
|
||||
def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, markets, tickers, base_currency, key,
|
||||
whitelist_result, precision_filter) -> None:
|
||||
whitelist_conf['pairlist']['method'] = 'VolumePairList'
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, p, r: round(r, 8))
|
||||
|
||||
freqtrade.pairlists._precision_filter = precision_filter
|
||||
freqtrade.config['stake_currency'] = base_currency
|
||||
whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency=base_currency, key=key)
|
||||
assert whitelist == whitelist_result
|
||||
|
||||
|
||||
def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
|
||||
default_conf['pairlist'] = {'method': 'VolumePairList',
|
||||
'config': {'number_assets': 10}
|
||||
}
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False))
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
get_patched_freqtradebot(mocker, default_conf)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS)
|
||||
def test_pairlist_class(mocker, whitelist_conf, markets, pairlist):
|
||||
whitelist_conf['pairlist']['method'] = pairlist
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
|
||||
assert freqtrade.pairlists.name == pairlist
|
||||
assert pairlist in freqtrade.pairlists.short_desc()
|
||||
assert isinstance(freqtrade.pairlists.whitelist, list)
|
||||
assert isinstance(freqtrade.pairlists.blacklist, list)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS)
|
||||
@pytest.mark.parametrize("whitelist,log_message", [
|
||||
(['ETH/BTC', 'TKN/BTC'], ""),
|
||||
(['ETH/BTC', 'TKN/BTC', 'TRX/ETH'], "is not compatible with exchange"), # TRX/ETH wrong stake
|
||||
(['ETH/BTC', 'TKN/BTC', 'BCH/BTC'], "is not compatible with exchange"), # BCH/BTC not available
|
||||
(['ETH/BTC', 'TKN/BTC', 'BLK/BTC'], "is not compatible with exchange"), # BLK/BTC in blacklist
|
||||
(['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], "Market is not active") # LTC/BTC is inactive
|
||||
])
|
||||
def test_validate_whitelist(mocker, whitelist_conf, markets, pairlist, whitelist, caplog,
|
||||
log_message):
|
||||
whitelist_conf['pairlist']['method'] = pairlist
|
||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||
caplog.clear()
|
||||
|
||||
new_whitelist = freqtrade.pairlists._validate_whitelist(whitelist)
|
||||
|
||||
assert set(new_whitelist) == set(['ETH/BTC', 'TKN/BTC'])
|
||||
assert log_message in caplog.text
|
0
tests/rpc/__init__.py
Normal file
0
tests/rpc/__init__.py
Normal file
212
tests/rpc/test_fiat_convert.py
Normal file
212
tests/rpc/test_fiat_convert.py
Normal file
@@ -0,0 +1,212 @@
|
||||
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors,
|
||||
# pragma pylint: disable=protected-access, C0103
|
||||
|
||||
import time
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from freqtrade.rpc.fiat_convert import CryptoFiat, CryptoToFiatConverter
|
||||
from freqtrade.tests.conftest import log_has
|
||||
|
||||
|
||||
def test_pair_convertion_object():
|
||||
pair_convertion = CryptoFiat(
|
||||
crypto_symbol='btc',
|
||||
fiat_symbol='usd',
|
||||
price=12345.0
|
||||
)
|
||||
|
||||
# Check the cache duration is 6 hours
|
||||
assert pair_convertion.CACHE_DURATION == 6 * 60 * 60
|
||||
|
||||
# Check a regular usage
|
||||
assert pair_convertion.crypto_symbol == 'BTC'
|
||||
assert pair_convertion.fiat_symbol == 'USD'
|
||||
assert pair_convertion.price == 12345.0
|
||||
assert pair_convertion.is_expired() is False
|
||||
|
||||
# Update the expiration time (- 2 hours) and check the behavior
|
||||
pair_convertion._expiration = time.time() - 2 * 60 * 60
|
||||
assert pair_convertion.is_expired() is True
|
||||
|
||||
# Check set price behaviour
|
||||
time_reference = time.time() + pair_convertion.CACHE_DURATION
|
||||
pair_convertion.set_price(price=30000.123)
|
||||
assert pair_convertion.is_expired() is False
|
||||
assert pair_convertion._expiration >= time_reference
|
||||
assert pair_convertion.price == 30000.123
|
||||
|
||||
|
||||
def test_fiat_convert_is_supported(mocker):
|
||||
fiat_convert = CryptoToFiatConverter()
|
||||
assert fiat_convert._is_supported_fiat(fiat='USD') is True
|
||||
assert fiat_convert._is_supported_fiat(fiat='usd') is True
|
||||
assert fiat_convert._is_supported_fiat(fiat='abc') is False
|
||||
assert fiat_convert._is_supported_fiat(fiat='ABC') is False
|
||||
|
||||
|
||||
def test_fiat_convert_add_pair(mocker):
|
||||
|
||||
fiat_convert = CryptoToFiatConverter()
|
||||
|
||||
pair_len = len(fiat_convert._pairs)
|
||||
assert pair_len == 0
|
||||
|
||||
fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='usd', price=12345.0)
|
||||
pair_len = len(fiat_convert._pairs)
|
||||
assert pair_len == 1
|
||||
assert fiat_convert._pairs[0].crypto_symbol == 'BTC'
|
||||
assert fiat_convert._pairs[0].fiat_symbol == 'USD'
|
||||
assert fiat_convert._pairs[0].price == 12345.0
|
||||
|
||||
fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='Eur', price=13000.2)
|
||||
pair_len = len(fiat_convert._pairs)
|
||||
assert pair_len == 2
|
||||
assert fiat_convert._pairs[1].crypto_symbol == 'BTC'
|
||||
assert fiat_convert._pairs[1].fiat_symbol == 'EUR'
|
||||
assert fiat_convert._pairs[1].price == 13000.2
|
||||
|
||||
|
||||
def test_fiat_convert_find_price(mocker):
|
||||
fiat_convert = CryptoToFiatConverter()
|
||||
|
||||
with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'):
|
||||
fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='ABC')
|
||||
|
||||
assert fiat_convert.get_price(crypto_symbol='XRP', fiat_symbol='USD') == 0.0
|
||||
|
||||
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
|
||||
return_value=12345.0)
|
||||
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 12345.0
|
||||
assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 12345.0
|
||||
|
||||
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
|
||||
return_value=13000.2)
|
||||
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 13000.2
|
||||
|
||||
|
||||
def test_fiat_convert_unsupported_crypto(mocker, caplog):
|
||||
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[])
|
||||
fiat_convert = CryptoToFiatConverter()
|
||||
assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0
|
||||
assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog)
|
||||
|
||||
|
||||
def test_fiat_convert_get_price(mocker):
|
||||
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
|
||||
return_value=28000.0)
|
||||
|
||||
fiat_convert = CryptoToFiatConverter()
|
||||
|
||||
with pytest.raises(ValueError, match=r'The fiat US DOLLAR is not supported.'):
|
||||
fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='US Dollar')
|
||||
|
||||
# Check the value return by the method
|
||||
pair_len = len(fiat_convert._pairs)
|
||||
assert pair_len == 0
|
||||
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 28000.0
|
||||
assert fiat_convert._pairs[0].crypto_symbol == 'BTC'
|
||||
assert fiat_convert._pairs[0].fiat_symbol == 'USD'
|
||||
assert fiat_convert._pairs[0].price == 28000.0
|
||||
assert fiat_convert._pairs[0]._expiration != 0
|
||||
assert len(fiat_convert._pairs) == 1
|
||||
|
||||
# Verify the cached is used
|
||||
fiat_convert._pairs[0].price = 9867.543
|
||||
expiration = fiat_convert._pairs[0]._expiration
|
||||
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 9867.543
|
||||
assert fiat_convert._pairs[0]._expiration == expiration
|
||||
|
||||
# Verify the cache expiration
|
||||
expiration = time.time() - 2 * 60 * 60
|
||||
fiat_convert._pairs[0]._expiration = expiration
|
||||
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 28000.0
|
||||
assert fiat_convert._pairs[0]._expiration is not expiration
|
||||
|
||||
|
||||
def test_fiat_convert_same_currencies(mocker):
|
||||
fiat_convert = CryptoToFiatConverter()
|
||||
|
||||
assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='USD') == 1.0
|
||||
|
||||
|
||||
def test_fiat_convert_two_FIAT(mocker):
|
||||
fiat_convert = CryptoToFiatConverter()
|
||||
|
||||
assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='EUR') == 0.0
|
||||
|
||||
|
||||
def test_loadcryptomap(mocker):
|
||||
|
||||
fiat_convert = CryptoToFiatConverter()
|
||||
assert len(fiat_convert._cryptomap) == 2
|
||||
|
||||
assert fiat_convert._cryptomap["BTC"] == "1"
|
||||
|
||||
|
||||
def test_fiat_init_network_exception(mocker):
|
||||
# Because CryptoToFiatConverter is a Singleton we reset the listings
|
||||
listmock = MagicMock(side_effect=RequestException)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.fiat_convert.Market',
|
||||
listings=listmock,
|
||||
)
|
||||
# with pytest.raises(RequestEsxception):
|
||||
fiat_convert = CryptoToFiatConverter()
|
||||
fiat_convert._cryptomap = {}
|
||||
fiat_convert._load_cryptomap()
|
||||
|
||||
length_cryptomap = len(fiat_convert._cryptomap)
|
||||
assert length_cryptomap == 0
|
||||
|
||||
|
||||
def test_fiat_convert_without_network(mocker):
|
||||
# Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap
|
||||
|
||||
fiat_convert = CryptoToFiatConverter()
|
||||
|
||||
cmc_temp = CryptoToFiatConverter._coinmarketcap
|
||||
CryptoToFiatConverter._coinmarketcap = None
|
||||
|
||||
assert fiat_convert._coinmarketcap is None
|
||||
assert fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='USD') == 0.0
|
||||
CryptoToFiatConverter._coinmarketcap = cmc_temp
|
||||
|
||||
|
||||
def test_fiat_invalid_response(mocker, caplog):
|
||||
# Because CryptoToFiatConverter is a Singleton we reset the listings
|
||||
listmock = MagicMock(return_value="{'novalidjson':DEADBEEFf}")
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.fiat_convert.Market',
|
||||
listings=listmock,
|
||||
)
|
||||
# with pytest.raises(RequestEsxception):
|
||||
fiat_convert = CryptoToFiatConverter()
|
||||
fiat_convert._cryptomap = {}
|
||||
fiat_convert._load_cryptomap()
|
||||
|
||||
length_cryptomap = len(fiat_convert._cryptomap)
|
||||
assert length_cryptomap == 0
|
||||
assert log_has('Could not load FIAT Cryptocurrency map for the following problem: TypeError',
|
||||
caplog)
|
||||
|
||||
|
||||
def test_convert_amount(mocker):
|
||||
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0)
|
||||
|
||||
fiat_convert = CryptoToFiatConverter()
|
||||
result = fiat_convert.convert_amount(
|
||||
crypto_amount=1.23,
|
||||
crypto_symbol="BTC",
|
||||
fiat_symbol="USD"
|
||||
)
|
||||
assert result == 15184.35
|
||||
|
||||
result = fiat_convert.convert_amount(
|
||||
crypto_amount=1.23,
|
||||
crypto_symbol="BTC",
|
||||
fiat_symbol="BTC"
|
||||
)
|
||||
assert result == 1.23
|
813
tests/rpc/test_rpc.py
Normal file
813
tests/rpc/test_rpc.py
Normal file
@@ -0,0 +1,813 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
# pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import ANY, MagicMock, PropertyMock
|
||||
|
||||
import pytest
|
||||
from numpy import isnan
|
||||
|
||||
from freqtrade import DependencyException, TemporaryError
|
||||
from freqtrade.edge import PairInfo
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc import RPC, RPCException
|
||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||
from freqtrade.state import State
|
||||
from freqtrade.tests.conftest import patch_exchange, patch_get_signal
|
||||
|
||||
|
||||
# Functions for recurrent object patching
|
||||
def prec_satoshi(a, b) -> float:
|
||||
"""
|
||||
:return: True if A and B differs less than one satoshi.
|
||||
"""
|
||||
return abs(a - b) < 0.00000001
|
||||
|
||||
|
||||
# Unit tests
|
||||
def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
_load_markets=MagicMock(return_value={}),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.RUNNING
|
||||
with pytest.raises(RPCException, match=r'.*no active trade*'):
|
||||
rpc._rpc_trade_status()
|
||||
|
||||
freqtradebot.create_trades()
|
||||
results = rpc._rpc_trade_status()
|
||||
assert {
|
||||
'trade_id': 1,
|
||||
'pair': 'ETH/BTC',
|
||||
'base_currency': 'BTC',
|
||||
'open_date': ANY,
|
||||
'open_date_hum': ANY,
|
||||
'close_date': None,
|
||||
'close_date_hum': None,
|
||||
'open_rate': 1.099e-05,
|
||||
'close_rate': None,
|
||||
'current_rate': 1.098e-05,
|
||||
'amount': 90.99181074,
|
||||
'stake_amount': 0.001,
|
||||
'close_profit': None,
|
||||
'current_profit': -0.59,
|
||||
'stop_loss': 0.0,
|
||||
'initial_stop_loss': 0.0,
|
||||
'initial_stop_loss_pct': None,
|
||||
'stop_loss_pct': None,
|
||||
'open_order': '(limit buy rem=0.00000000)'
|
||||
} == results[0]
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker',
|
||||
MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
|
||||
# invalidate ticker cache
|
||||
rpc._freqtrade.exchange._cached_ticker = {}
|
||||
results = rpc._rpc_trade_status()
|
||||
assert isnan(results[0]['current_profit'])
|
||||
assert isnan(results[0]['current_rate'])
|
||||
assert {
|
||||
'trade_id': 1,
|
||||
'pair': 'ETH/BTC',
|
||||
'base_currency': 'BTC',
|
||||
'open_date': ANY,
|
||||
'open_date_hum': ANY,
|
||||
'close_date': None,
|
||||
'close_date_hum': None,
|
||||
'open_rate': 1.099e-05,
|
||||
'close_rate': None,
|
||||
'current_rate': ANY,
|
||||
'amount': 90.99181074,
|
||||
'stake_amount': 0.001,
|
||||
'close_profit': None,
|
||||
'current_profit': ANY,
|
||||
'stop_loss': 0.0,
|
||||
'initial_stop_loss': 0.0,
|
||||
'initial_stop_loss_pct': None,
|
||||
'stop_loss_pct': None,
|
||||
'open_order': '(limit buy rem=0.00000000)'
|
||||
} == results[0]
|
||||
|
||||
|
||||
def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.RUNNING
|
||||
with pytest.raises(RPCException, match=r'.*no active order*'):
|
||||
rpc._rpc_status_table()
|
||||
|
||||
freqtradebot.create_trades()
|
||||
result = rpc._rpc_status_table()
|
||||
assert 'instantly' in result['Since'].all()
|
||||
assert 'ETH/BTC' in result['Pair'].all()
|
||||
assert '-0.59%' in result['Profit'].all()
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker',
|
||||
MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
|
||||
# invalidate ticker cache
|
||||
rpc._freqtrade.exchange._cached_ticker = {}
|
||||
result = rpc._rpc_status_table()
|
||||
assert 'instantly' in result['Since'].all()
|
||||
assert 'ETH/BTC' in result['Pair'].all()
|
||||
assert 'nan%' in result['Profit'].all()
|
||||
|
||||
|
||||
def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
stake_currency = default_conf['stake_currency']
|
||||
fiat_display_currency = default_conf['fiat_display_currency']
|
||||
|
||||
rpc = RPC(freqtradebot)
|
||||
rpc._fiat_converter = CryptoToFiatConverter()
|
||||
# Create some test data
|
||||
freqtradebot.create_trades()
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
|
||||
# Simulate buy & sell
|
||||
trade.update(limit_buy_order)
|
||||
trade.update(limit_sell_order)
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
|
||||
# Try valid data
|
||||
update.message.text = '/daily 2'
|
||||
days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
|
||||
assert len(days) == 7
|
||||
for day in days:
|
||||
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
|
||||
assert (day[1] == '0.00000000 BTC' or
|
||||
day[1] == '0.00006217 BTC')
|
||||
|
||||
assert (day[2] == '0.000 USD' or
|
||||
day[2] == '0.933 USD')
|
||||
# ensure first day is current date
|
||||
assert str(days[0][0]) == str(datetime.utcnow().date())
|
||||
|
||||
# Try invalid data
|
||||
with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
|
||||
rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency)
|
||||
|
||||
|
||||
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.fiat_convert.Market',
|
||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||
)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
stake_currency = default_conf['stake_currency']
|
||||
fiat_display_currency = default_conf['fiat_display_currency']
|
||||
|
||||
rpc = RPC(freqtradebot)
|
||||
rpc._fiat_converter = CryptoToFiatConverter()
|
||||
|
||||
with pytest.raises(RPCException, match=r'.*no closed trade*'):
|
||||
rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||
|
||||
# Create some test data
|
||||
freqtradebot.create_trades()
|
||||
trade = Trade.query.first()
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
trade.update(limit_buy_order)
|
||||
|
||||
# Update the ticker with a market going up
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_ticker=ticker_sell_up
|
||||
)
|
||||
trade.update(limit_sell_order)
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
|
||||
freqtradebot.create_trades()
|
||||
trade = Trade.query.first()
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
trade.update(limit_buy_order)
|
||||
|
||||
# Update the ticker with a market going up
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_ticker=ticker_sell_up
|
||||
)
|
||||
trade.update(limit_sell_order)
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
|
||||
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
|
||||
assert prec_satoshi(stats['profit_closed_percent'], 6.2)
|
||||
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
|
||||
assert prec_satoshi(stats['profit_all_coin'], 5.632e-05)
|
||||
assert prec_satoshi(stats['profit_all_percent'], 2.81)
|
||||
assert prec_satoshi(stats['profit_all_fiat'], 0.8448)
|
||||
assert stats['trade_count'] == 2
|
||||
assert stats['first_trade_date'] == 'just now'
|
||||
assert stats['latest_trade_date'] == 'just now'
|
||||
assert stats['avg_duration'] == '0:00:00'
|
||||
assert stats['best_pair'] == 'ETH/BTC'
|
||||
assert prec_satoshi(stats['best_rate'], 6.2)
|
||||
|
||||
# Test non-available pair
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker',
|
||||
MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
|
||||
# invalidate ticker cache
|
||||
rpc._freqtrade.exchange._cached_ticker = {}
|
||||
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||
assert stats['trade_count'] == 2
|
||||
assert stats['first_trade_date'] == 'just now'
|
||||
assert stats['latest_trade_date'] == 'just now'
|
||||
assert stats['avg_duration'] == '0:00:00'
|
||||
assert stats['best_pair'] == 'ETH/BTC'
|
||||
assert prec_satoshi(stats['best_rate'], 6.2)
|
||||
assert isnan(stats['profit_all_coin'])
|
||||
|
||||
|
||||
# Test that rpc_trade_statistics can handle trades that lacks
|
||||
# trade.open_rate (it is set to None)
|
||||
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
|
||||
ticker_sell_up, limit_buy_order, limit_sell_order):
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.fiat_convert.Market',
|
||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||
)
|
||||
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
|
||||
return_value=15000.0)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
stake_currency = default_conf['stake_currency']
|
||||
fiat_display_currency = default_conf['fiat_display_currency']
|
||||
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
freqtradebot.create_trades()
|
||||
trade = Trade.query.first()
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
trade.update(limit_buy_order)
|
||||
# Update the ticker with a market going up
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_ticker=ticker_sell_up,
|
||||
get_fee=fee
|
||||
)
|
||||
trade.update(limit_sell_order)
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
|
||||
for trade in Trade.query.order_by(Trade.id).all():
|
||||
trade.open_rate = None
|
||||
|
||||
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||
assert prec_satoshi(stats['profit_closed_coin'], 0)
|
||||
assert prec_satoshi(stats['profit_closed_percent'], 0)
|
||||
assert prec_satoshi(stats['profit_closed_fiat'], 0)
|
||||
assert prec_satoshi(stats['profit_all_coin'], 0)
|
||||
assert prec_satoshi(stats['profit_all_percent'], 0)
|
||||
assert prec_satoshi(stats['profit_all_fiat'], 0)
|
||||
assert stats['trade_count'] == 1
|
||||
assert stats['first_trade_date'] == 'just now'
|
||||
assert stats['latest_trade_date'] == 'just now'
|
||||
assert stats['avg_duration'] == '0:00:00'
|
||||
assert stats['best_pair'] == 'ETH/BTC'
|
||||
assert prec_satoshi(stats['best_rate'], 6.2)
|
||||
|
||||
|
||||
def test_rpc_balance_handle_error(default_conf, mocker):
|
||||
mock_balance = {
|
||||
'BTC': {
|
||||
'free': 10.0,
|
||||
'total': 12.0,
|
||||
'used': 2.0,
|
||||
},
|
||||
'ETH': {
|
||||
'free': 1.0,
|
||||
'total': 5.0,
|
||||
'used': 4.0,
|
||||
}
|
||||
}
|
||||
# ETH will be skipped due to mocked Error below
|
||||
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.fiat_convert.Market',
|
||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||
)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_balances=MagicMock(return_value=mock_balance),
|
||||
get_ticker=MagicMock(side_effect=TemporaryError('Could not load ticker due to xxx'))
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
rpc._fiat_converter = CryptoToFiatConverter()
|
||||
|
||||
result = rpc._rpc_balance(default_conf['fiat_display_currency'])
|
||||
assert prec_satoshi(result['total'], 12)
|
||||
assert prec_satoshi(result['value'], 180000)
|
||||
assert 'USD' == result['symbol']
|
||||
assert result['currencies'] == [{
|
||||
'currency': 'BTC',
|
||||
'free': 10.0,
|
||||
'balance': 12.0,
|
||||
'used': 2.0,
|
||||
'est_btc': 12.0,
|
||||
}]
|
||||
assert result['total'] == 12.0
|
||||
|
||||
|
||||
def test_rpc_balance_handle(default_conf, mocker):
|
||||
mock_balance = {
|
||||
'BTC': {
|
||||
'free': 10.0,
|
||||
'total': 12.0,
|
||||
'used': 2.0,
|
||||
},
|
||||
'ETH': {
|
||||
'free': 1.0,
|
||||
'total': 5.0,
|
||||
'used': 4.0,
|
||||
},
|
||||
'PAX': {
|
||||
'free': 5.0,
|
||||
'total': 10.0,
|
||||
'used': 5.0,
|
||||
}
|
||||
}
|
||||
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.fiat_convert.Market',
|
||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||
)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_balances=MagicMock(return_value=mock_balance),
|
||||
get_ticker=MagicMock(
|
||||
side_effect=lambda p, r: {'bid': 100} if p == "BTC/PAX" else {'bid': 0.01}),
|
||||
get_valid_pair_combination=MagicMock(
|
||||
side_effect=lambda a, b: f"{b}/{a}" if a == "PAX" else f"{a}/{b}")
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
rpc._fiat_converter = CryptoToFiatConverter()
|
||||
|
||||
result = rpc._rpc_balance(default_conf['fiat_display_currency'])
|
||||
assert prec_satoshi(result['total'], 12.15)
|
||||
assert prec_satoshi(result['value'], 182250)
|
||||
assert 'USD' == result['symbol']
|
||||
assert result['currencies'] == [
|
||||
{'currency': 'BTC',
|
||||
'free': 10.0,
|
||||
'balance': 12.0,
|
||||
'used': 2.0,
|
||||
'est_btc': 12.0,
|
||||
},
|
||||
{'free': 1.0,
|
||||
'balance': 5.0,
|
||||
'currency': 'ETH',
|
||||
'est_btc': 0.05,
|
||||
'used': 4.0
|
||||
},
|
||||
{'free': 5.0,
|
||||
'balance': 10.0,
|
||||
'currency': 'PAX',
|
||||
'est_btc': 0.1,
|
||||
'used': 5.0}
|
||||
]
|
||||
assert result['total'] == 12.15
|
||||
|
||||
|
||||
def test_rpc_start(mocker, default_conf) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_ticker=MagicMock()
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
freqtradebot.state = State.STOPPED
|
||||
|
||||
result = rpc._rpc_start()
|
||||
assert {'status': 'starting trader ...'} == result
|
||||
assert freqtradebot.state == State.RUNNING
|
||||
|
||||
result = rpc._rpc_start()
|
||||
assert {'status': 'already running'} == result
|
||||
assert freqtradebot.state == State.RUNNING
|
||||
|
||||
|
||||
def test_rpc_stop(mocker, default_conf) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_ticker=MagicMock()
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
freqtradebot.state = State.RUNNING
|
||||
|
||||
result = rpc._rpc_stop()
|
||||
assert {'status': 'stopping trader ...'} == result
|
||||
assert freqtradebot.state == State.STOPPED
|
||||
|
||||
result = rpc._rpc_stop()
|
||||
|
||||
assert {'status': 'already stopped'} == result
|
||||
assert freqtradebot.state == State.STOPPED
|
||||
|
||||
|
||||
def test_rpc_stopbuy(mocker, default_conf) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_ticker=MagicMock()
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
freqtradebot.state = State.RUNNING
|
||||
|
||||
assert freqtradebot.config['max_open_trades'] != 0
|
||||
result = rpc._rpc_stopbuy()
|
||||
assert {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} == result
|
||||
assert freqtradebot.config['max_open_trades'] == 0
|
||||
|
||||
|
||||
def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
|
||||
cancel_order_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_ticker=ticker,
|
||||
cancel_order=cancel_order_mock,
|
||||
get_order=MagicMock(
|
||||
return_value={
|
||||
'status': 'closed',
|
||||
'type': 'limit',
|
||||
'side': 'buy'
|
||||
}
|
||||
),
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
||||
rpc._rpc_forcesell(None)
|
||||
|
||||
freqtradebot.state = State.RUNNING
|
||||
with pytest.raises(RPCException, match=r'.*invalid argument*'):
|
||||
rpc._rpc_forcesell(None)
|
||||
|
||||
msg = rpc._rpc_forcesell('all')
|
||||
assert msg == {'result': 'Created sell orders for all open trades.'}
|
||||
|
||||
freqtradebot.create_trades()
|
||||
msg = rpc._rpc_forcesell('all')
|
||||
assert msg == {'result': 'Created sell orders for all open trades.'}
|
||||
|
||||
msg = rpc._rpc_forcesell('1')
|
||||
assert msg == {'result': 'Created sell order for trade 1.'}
|
||||
|
||||
freqtradebot.state = State.STOPPED
|
||||
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
||||
rpc._rpc_forcesell(None)
|
||||
|
||||
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
||||
rpc._rpc_forcesell('all')
|
||||
|
||||
freqtradebot.state = State.RUNNING
|
||||
assert cancel_order_mock.call_count == 0
|
||||
# make an limit-buy open trade
|
||||
trade = Trade.query.filter(Trade.id == '1').first()
|
||||
filled_amount = trade.amount / 2
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.get_order',
|
||||
return_value={
|
||||
'status': 'open',
|
||||
'type': 'limit',
|
||||
'side': 'buy',
|
||||
'filled': filled_amount
|
||||
}
|
||||
)
|
||||
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
|
||||
# and trade amount is updated
|
||||
rpc._rpc_forcesell('1')
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert trade.amount == filled_amount
|
||||
|
||||
freqtradebot.create_trades()
|
||||
trade = Trade.query.filter(Trade.id == '2').first()
|
||||
amount = trade.amount
|
||||
# make an limit-buy open trade, if there is no 'filled', don't sell it
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.get_order',
|
||||
return_value={
|
||||
'status': 'open',
|
||||
'type': 'limit',
|
||||
'side': 'buy',
|
||||
'filled': None
|
||||
}
|
||||
)
|
||||
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
|
||||
msg = rpc._rpc_forcesell('2')
|
||||
assert msg == {'result': 'Created sell order for trade 2.'}
|
||||
assert cancel_order_mock.call_count == 2
|
||||
assert trade.amount == amount
|
||||
|
||||
freqtradebot.create_trades()
|
||||
# make an limit-sell open trade
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.get_order',
|
||||
return_value={
|
||||
'status': 'open',
|
||||
'type': 'limit',
|
||||
'side': 'sell'
|
||||
}
|
||||
)
|
||||
msg = rpc._rpc_forcesell('3')
|
||||
assert msg == {'result': 'Created sell order for trade 3.'}
|
||||
# status quo, no exchange calls
|
||||
assert cancel_order_mock.call_count == 2
|
||||
|
||||
|
||||
def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||
limit_sell_order, markets, mocker) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
# Create some test data
|
||||
freqtradebot.create_trades()
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
trade.update(limit_buy_order)
|
||||
|
||||
# Simulate fulfilled LIMIT_SELL order for trade
|
||||
trade.update(limit_sell_order)
|
||||
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
res = rpc._rpc_performance()
|
||||
assert len(res) == 1
|
||||
assert res[0]['pair'] == 'ETH/BTC'
|
||||
assert res[0]['count'] == 1
|
||||
assert prec_satoshi(res[0]['profit'], 6.2)
|
||||
|
||||
|
||||
def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
|
||||
counts = rpc._rpc_count()
|
||||
assert counts["current"] == 0
|
||||
|
||||
# Create some test data
|
||||
freqtradebot.create_trades()
|
||||
counts = rpc._rpc_count()
|
||||
assert counts["current"] == 1
|
||||
|
||||
|
||||
def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order) -> None:
|
||||
default_conf['forcebuy_enable'] = True
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
buy_mm = MagicMock(return_value={'id': limit_buy_order['id']})
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
buy=buy_mm
|
||||
)
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
pair = 'ETH/BTC'
|
||||
trade = rpc._rpc_forcebuy(pair, None)
|
||||
assert isinstance(trade, Trade)
|
||||
assert trade.pair == pair
|
||||
assert trade.open_rate == ticker()['ask']
|
||||
|
||||
# Test buy duplicate
|
||||
with pytest.raises(RPCException, match=r'position for ETH/BTC already open - id: 1'):
|
||||
rpc._rpc_forcebuy(pair, 0.0001)
|
||||
pair = 'XRP/BTC'
|
||||
trade = rpc._rpc_forcebuy(pair, 0.0001)
|
||||
assert isinstance(trade, Trade)
|
||||
assert trade.pair == pair
|
||||
assert trade.open_rate == 0.0001
|
||||
|
||||
# Test buy pair not with stakes
|
||||
with pytest.raises(RPCException, match=r'Wrong pair selected. Please pairs with stake.*'):
|
||||
rpc._rpc_forcebuy('XRP/ETH', 0.0001)
|
||||
pair = 'XRP/BTC'
|
||||
|
||||
# Test not buying
|
||||
default_conf['stake_amount'] = 0.0000001
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
pair = 'TKN/BTC'
|
||||
trade = rpc._rpc_forcebuy(pair, None)
|
||||
assert trade is None
|
||||
|
||||
|
||||
def test_rpcforcebuy_stopped(mocker, default_conf) -> None:
|
||||
default_conf['forcebuy_enable'] = True
|
||||
default_conf['initial_state'] = 'stopped'
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
pair = 'ETH/BTC'
|
||||
with pytest.raises(RPCException, match=r'trader is not running'):
|
||||
rpc._rpc_forcebuy(pair, None)
|
||||
|
||||
|
||||
def test_rpcforcebuy_disabled(mocker, default_conf) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtradebot, (True, False))
|
||||
rpc = RPC(freqtradebot)
|
||||
pair = 'ETH/BTC'
|
||||
with pytest.raises(RPCException, match=r'Forcebuy not enabled.'):
|
||||
rpc._rpc_forcebuy(pair, None)
|
||||
|
||||
|
||||
def test_rpc_whitelist(mocker, default_conf) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
rpc = RPC(freqtradebot)
|
||||
ret = rpc._rpc_whitelist()
|
||||
assert ret['method'] == 'StaticPairList'
|
||||
assert ret['whitelist'] == default_conf['exchange']['pair_whitelist']
|
||||
|
||||
|
||||
def test_rpc_whitelist_dynamic(mocker, default_conf) -> None:
|
||||
patch_exchange(mocker)
|
||||
default_conf['pairlist'] = {'method': 'VolumePairList',
|
||||
'config': {'number_assets': 4}
|
||||
}
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
rpc = RPC(freqtradebot)
|
||||
ret = rpc._rpc_whitelist()
|
||||
assert ret['method'] == 'VolumePairList'
|
||||
assert ret['length'] == 4
|
||||
assert ret['whitelist'] == default_conf['exchange']['pair_whitelist']
|
||||
|
||||
|
||||
def test_rpc_blacklist(mocker, default_conf) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
rpc = RPC(freqtradebot)
|
||||
ret = rpc._rpc_blacklist(None)
|
||||
assert ret['method'] == 'StaticPairList'
|
||||
assert len(ret['blacklist']) == 2
|
||||
assert ret['blacklist'] == default_conf['exchange']['pair_blacklist']
|
||||
assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC']
|
||||
|
||||
ret = rpc._rpc_blacklist(["ETH/BTC"])
|
||||
assert ret['method'] == 'StaticPairList'
|
||||
assert len(ret['blacklist']) == 3
|
||||
assert ret['blacklist'] == default_conf['exchange']['pair_blacklist']
|
||||
assert ret['blacklist'] == ['DOGE/BTC', 'HOT/BTC', 'ETH/BTC']
|
||||
|
||||
|
||||
def test_rpc_edge_disabled(mocker, default_conf) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
freqtradebot = FreqtradeBot(default_conf)
|
||||
rpc = RPC(freqtradebot)
|
||||
with pytest.raises(RPCException, match=r'Edge is not enabled.'):
|
||||
rpc._rpc_edge()
|
||||
|
||||
|
||||
def test_rpc_edge_enabled(mocker, edge_conf) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||
mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
|
||||
return_value={
|
||||
'E/F': PairInfo(-0.02, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||
}
|
||||
))
|
||||
freqtradebot = FreqtradeBot(edge_conf)
|
||||
|
||||
rpc = RPC(freqtradebot)
|
||||
ret = rpc._rpc_edge()
|
||||
|
||||
assert len(ret) == 1
|
||||
assert ret[0]['Pair'] == 'E/F'
|
||||
assert ret[0]['Winrate'] == 0.66
|
||||
assert ret[0]['Expectancy'] == 1.71
|
||||
assert ret[0]['Stoploss'] == -0.02
|
556
tests/rpc/test_rpc_apiserver.py
Normal file
556
tests/rpc/test_rpc_apiserver.py
Normal file
@@ -0,0 +1,556 @@
|
||||
"""
|
||||
Unit test file for rpc/api_server.py
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import ANY, MagicMock, PropertyMock
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from requests.auth import _basic_auth_str
|
||||
|
||||
from freqtrade.__init__ import __version__
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc.api_server import BASE_URI, ApiServer
|
||||
from freqtrade.state import State
|
||||
from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has,
|
||||
patch_get_signal)
|
||||
|
||||
|
||||
_TEST_USER = "FreqTrader"
|
||||
_TEST_PASS = "SuperSecurePassword1!"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def botclient(default_conf, mocker):
|
||||
default_conf.update({"api_server": {"enabled": True,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": "8080",
|
||||
"username": _TEST_USER,
|
||||
"password": _TEST_PASS,
|
||||
}})
|
||||
|
||||
ftbot = get_patched_freqtradebot(mocker, default_conf)
|
||||
mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock())
|
||||
apiserver = ApiServer(ftbot)
|
||||
yield ftbot, apiserver.app.test_client()
|
||||
# Cleanup ... ?
|
||||
|
||||
|
||||
def client_post(client, url, data={}):
|
||||
return client.post(url,
|
||||
content_type="application/json",
|
||||
data=data,
|
||||
headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS)})
|
||||
|
||||
|
||||
def client_get(client, url):
|
||||
return client.get(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS)})
|
||||
|
||||
|
||||
def assert_response(response, expected_code=200):
|
||||
assert response.status_code == expected_code
|
||||
assert response.content_type == "application/json"
|
||||
|
||||
|
||||
def test_api_not_found(botclient):
|
||||
ftbot, client = botclient
|
||||
|
||||
rc = client_post(client, f"{BASE_URI}/invalid_url")
|
||||
assert_response(rc, 404)
|
||||
assert rc.json == {"status": "error",
|
||||
"reason": f"There's no API call for http://localhost{BASE_URI}/invalid_url.",
|
||||
"code": 404
|
||||
}
|
||||
|
||||
|
||||
def test_api_unauthorized(botclient):
|
||||
ftbot, client = botclient
|
||||
# Don't send user/pass information
|
||||
rc = client.get(f"{BASE_URI}/version")
|
||||
assert_response(rc, 401)
|
||||
assert rc.json == {'error': 'Unauthorized'}
|
||||
|
||||
# Change only username
|
||||
ftbot.config['api_server']['username'] = "Ftrader"
|
||||
rc = client_get(client, f"{BASE_URI}/version")
|
||||
assert_response(rc, 401)
|
||||
assert rc.json == {'error': 'Unauthorized'}
|
||||
|
||||
# Change only password
|
||||
ftbot.config['api_server']['username'] = _TEST_USER
|
||||
ftbot.config['api_server']['password'] = "WrongPassword"
|
||||
rc = client_get(client, f"{BASE_URI}/version")
|
||||
assert_response(rc, 401)
|
||||
assert rc.json == {'error': 'Unauthorized'}
|
||||
|
||||
ftbot.config['api_server']['username'] = "Ftrader"
|
||||
ftbot.config['api_server']['password'] = "WrongPassword"
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/version")
|
||||
assert_response(rc, 401)
|
||||
assert rc.json == {'error': 'Unauthorized'}
|
||||
|
||||
|
||||
def test_api_stop_workflow(botclient):
|
||||
ftbot, client = botclient
|
||||
assert ftbot.state == State.RUNNING
|
||||
rc = client_post(client, f"{BASE_URI}/stop")
|
||||
assert_response(rc)
|
||||
assert rc.json == {'status': 'stopping trader ...'}
|
||||
assert ftbot.state == State.STOPPED
|
||||
|
||||
# Stop bot again
|
||||
rc = client_post(client, f"{BASE_URI}/stop")
|
||||
assert_response(rc)
|
||||
assert rc.json == {'status': 'already stopped'}
|
||||
|
||||
# Start bot
|
||||
rc = client_post(client, f"{BASE_URI}/start")
|
||||
assert_response(rc)
|
||||
assert rc.json == {'status': 'starting trader ...'}
|
||||
assert ftbot.state == State.RUNNING
|
||||
|
||||
# Call start again
|
||||
rc = client_post(client, f"{BASE_URI}/start")
|
||||
assert_response(rc)
|
||||
assert rc.json == {'status': 'already running'}
|
||||
|
||||
|
||||
def test_api__init__(default_conf, mocker):
|
||||
"""
|
||||
Test __init__() method
|
||||
"""
|
||||
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
|
||||
mocker.patch('freqtrade.rpc.api_server.ApiServer.run', MagicMock())
|
||||
|
||||
apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf))
|
||||
assert apiserver._config == default_conf
|
||||
|
||||
|
||||
def test_api_run(default_conf, mocker, caplog):
|
||||
default_conf.update({"api_server": {"enabled": True,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": "8080"}})
|
||||
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
|
||||
mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock())
|
||||
|
||||
server_mock = MagicMock()
|
||||
mocker.patch('freqtrade.rpc.api_server.make_server', server_mock)
|
||||
|
||||
apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf))
|
||||
|
||||
assert apiserver._config == default_conf
|
||||
apiserver.run()
|
||||
assert server_mock.call_count == 1
|
||||
assert server_mock.call_args_list[0][0][0] == "127.0.0.1"
|
||||
assert server_mock.call_args_list[0][0][1] == "8080"
|
||||
assert isinstance(server_mock.call_args_list[0][0][2], Flask)
|
||||
assert hasattr(apiserver, "srv")
|
||||
|
||||
assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog)
|
||||
assert log_has("Starting Local Rest Server.", caplog)
|
||||
|
||||
# Test binding to public
|
||||
caplog.clear()
|
||||
server_mock.reset_mock()
|
||||
apiserver._config.update({"api_server": {"enabled": True,
|
||||
"listen_ip_address": "0.0.0.0",
|
||||
"listen_port": "8089",
|
||||
"password": "",
|
||||
}})
|
||||
apiserver.run()
|
||||
|
||||
assert server_mock.call_count == 1
|
||||
assert server_mock.call_args_list[0][0][0] == "0.0.0.0"
|
||||
assert server_mock.call_args_list[0][0][1] == "8089"
|
||||
assert isinstance(server_mock.call_args_list[0][0][2], Flask)
|
||||
assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog)
|
||||
assert log_has("Starting Local Rest Server.", caplog)
|
||||
assert log_has("SECURITY WARNING - Local Rest Server listening to external connections",
|
||||
caplog)
|
||||
assert log_has("SECURITY WARNING - This is insecure please set to your loopback,"
|
||||
"e.g 127.0.0.1 in config.json", caplog)
|
||||
assert log_has("SECURITY WARNING - No password for local REST Server defined. "
|
||||
"Please make sure that this is intentional!", caplog)
|
||||
|
||||
# Test crashing flask
|
||||
caplog.clear()
|
||||
mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock(side_effect=Exception))
|
||||
apiserver.run()
|
||||
assert log_has("Api server failed to start.", caplog)
|
||||
|
||||
|
||||
def test_api_cleanup(default_conf, mocker, caplog):
|
||||
default_conf.update({"api_server": {"enabled": True,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": "8080"}})
|
||||
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
|
||||
mocker.patch('freqtrade.rpc.api_server.threading.Thread', MagicMock())
|
||||
mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock())
|
||||
|
||||
apiserver = ApiServer(get_patched_freqtradebot(mocker, default_conf))
|
||||
apiserver.run()
|
||||
stop_mock = MagicMock()
|
||||
stop_mock.shutdown = MagicMock()
|
||||
apiserver.srv = stop_mock
|
||||
|
||||
apiserver.cleanup()
|
||||
assert stop_mock.shutdown.call_count == 1
|
||||
assert log_has("Stopping API Server", caplog)
|
||||
|
||||
|
||||
def test_api_reloadconf(botclient):
|
||||
ftbot, client = botclient
|
||||
|
||||
rc = client_post(client, f"{BASE_URI}/reload_conf")
|
||||
assert_response(rc)
|
||||
assert rc.json == {'status': 'reloading config ...'}
|
||||
assert ftbot.state == State.RELOAD_CONF
|
||||
|
||||
|
||||
def test_api_stopbuy(botclient):
|
||||
ftbot, client = botclient
|
||||
assert ftbot.config['max_open_trades'] != 0
|
||||
|
||||
rc = client_post(client, f"{BASE_URI}/stopbuy")
|
||||
assert_response(rc)
|
||||
assert rc.json == {'status': 'No more buy will occur from now. Run /reload_conf to reset.'}
|
||||
assert ftbot.config['max_open_trades'] == 0
|
||||
|
||||
|
||||
def test_api_balance(botclient, mocker, rpc_balance):
|
||||
ftbot, client = botclient
|
||||
|
||||
def mock_ticker(symbol, refresh):
|
||||
if symbol == 'BTC/USDT':
|
||||
return {
|
||||
'bid': 10000.00,
|
||||
'ask': 10000.00,
|
||||
'last': 10000.00,
|
||||
}
|
||||
elif symbol == 'XRP/BTC':
|
||||
return {
|
||||
'bid': 0.00001,
|
||||
'ask': 0.00001,
|
||||
'last': 0.00001,
|
||||
}
|
||||
return {
|
||||
'bid': 0.1,
|
||||
'ask': 0.1,
|
||||
'last': 0.1,
|
||||
}
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination',
|
||||
side_effect=lambda a, b: f"{a}/{b}")
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/balance")
|
||||
assert_response(rc)
|
||||
assert "currencies" in rc.json
|
||||
assert len(rc.json["currencies"]) == 5
|
||||
assert rc.json['currencies'][0] == {
|
||||
'currency': 'BTC',
|
||||
'free': 12.0,
|
||||
'balance': 12.0,
|
||||
'used': 0.0,
|
||||
'est_btc': 12.0,
|
||||
}
|
||||
|
||||
|
||||
def test_api_count(botclient, mocker, ticker, fee, markets):
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot, (True, False))
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
rc = client_get(client, f"{BASE_URI}/count")
|
||||
assert_response(rc)
|
||||
|
||||
assert rc.json["current"] == 0
|
||||
assert rc.json["max"] == 1.0
|
||||
|
||||
# Create some test data
|
||||
ftbot.create_trades()
|
||||
rc = client_get(client, f"{BASE_URI}/count")
|
||||
assert_response(rc)
|
||||
assert rc.json["current"] == 1.0
|
||||
assert rc.json["max"] == 1.0
|
||||
|
||||
|
||||
def test_api_daily(botclient, mocker, ticker, fee, markets):
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot, (True, False))
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
rc = client_get(client, f"{BASE_URI}/daily")
|
||||
assert_response(rc)
|
||||
assert len(rc.json) == 7
|
||||
assert rc.json[0][0] == str(datetime.utcnow().date())
|
||||
|
||||
|
||||
def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot, (True, False))
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
rc = client_get(client, f"{BASE_URI}/edge")
|
||||
assert_response(rc, 502)
|
||||
assert rc.json == {"error": "Error querying _edge: Edge is not enabled."}
|
||||
|
||||
|
||||
def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, limit_sell_order):
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot, (True, False))
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/profit")
|
||||
assert_response(rc, 502)
|
||||
assert len(rc.json) == 1
|
||||
assert rc.json == {"error": "Error querying _profit: no closed trade"}
|
||||
|
||||
ftbot.create_trades()
|
||||
trade = Trade.query.first()
|
||||
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
trade.update(limit_buy_order)
|
||||
rc = client_get(client, f"{BASE_URI}/profit")
|
||||
assert_response(rc, 502)
|
||||
assert rc.json == {"error": "Error querying _profit: no closed trade"}
|
||||
|
||||
trade.update(limit_sell_order)
|
||||
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/profit")
|
||||
assert_response(rc)
|
||||
assert rc.json == {'avg_duration': '0:00:00',
|
||||
'best_pair': 'ETH/BTC',
|
||||
'best_rate': 6.2,
|
||||
'first_trade_date': 'just now',
|
||||
'latest_trade_date': 'just now',
|
||||
'profit_all_coin': 6.217e-05,
|
||||
'profit_all_fiat': 0,
|
||||
'profit_all_percent': 6.2,
|
||||
'profit_closed_coin': 6.217e-05,
|
||||
'profit_closed_fiat': 0,
|
||||
'profit_closed_percent': 6.2,
|
||||
'trade_count': 1
|
||||
}
|
||||
|
||||
|
||||
def test_api_performance(botclient, mocker, ticker, fee):
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot, (True, False))
|
||||
|
||||
trade = Trade(
|
||||
pair='LTC/ETH',
|
||||
amount=1,
|
||||
exchange='binance',
|
||||
stake_amount=1,
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456",
|
||||
is_open=False,
|
||||
fee_close=fee.return_value,
|
||||
fee_open=fee.return_value,
|
||||
close_rate=0.265441,
|
||||
|
||||
)
|
||||
trade.close_profit = trade.calc_profit_percent()
|
||||
Trade.session.add(trade)
|
||||
|
||||
trade = Trade(
|
||||
pair='XRP/ETH',
|
||||
amount=5,
|
||||
stake_amount=1,
|
||||
exchange='binance',
|
||||
open_rate=0.412,
|
||||
open_order_id="123456",
|
||||
is_open=False,
|
||||
fee_close=fee.return_value,
|
||||
fee_open=fee.return_value,
|
||||
close_rate=0.391
|
||||
)
|
||||
trade.close_profit = trade.calc_profit_percent()
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/performance")
|
||||
assert_response(rc)
|
||||
assert len(rc.json) == 2
|
||||
assert rc.json == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61},
|
||||
{'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57}]
|
||||
|
||||
|
||||
def test_api_status(botclient, mocker, ticker, fee, markets):
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot, (True, False))
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/status")
|
||||
assert_response(rc, 502)
|
||||
assert rc.json == {'error': 'Error querying _status: no active trade'}
|
||||
|
||||
ftbot.create_trades()
|
||||
rc = client_get(client, f"{BASE_URI}/status")
|
||||
assert_response(rc)
|
||||
assert len(rc.json) == 1
|
||||
assert rc.json == [{'amount': 90.99181074,
|
||||
'base_currency': 'BTC',
|
||||
'close_date': None,
|
||||
'close_date_hum': None,
|
||||
'close_profit': None,
|
||||
'close_rate': None,
|
||||
'current_profit': -0.59,
|
||||
'current_rate': 1.098e-05,
|
||||
'initial_stop_loss': 0.0,
|
||||
'initial_stop_loss_pct': None,
|
||||
'open_date': ANY,
|
||||
'open_date_hum': 'just now',
|
||||
'open_order': '(limit buy rem=0.00000000)',
|
||||
'open_rate': 1.099e-05,
|
||||
'pair': 'ETH/BTC',
|
||||
'stake_amount': 0.001,
|
||||
'stop_loss': 0.0,
|
||||
'stop_loss_pct': None,
|
||||
'trade_id': 1}]
|
||||
|
||||
|
||||
def test_api_version(botclient):
|
||||
ftbot, client = botclient
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/version")
|
||||
assert_response(rc)
|
||||
assert rc.json == {"version": __version__}
|
||||
|
||||
|
||||
def test_api_blacklist(botclient, mocker):
|
||||
ftbot, client = botclient
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/blacklist")
|
||||
assert_response(rc)
|
||||
assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC"],
|
||||
"length": 2,
|
||||
"method": "StaticPairList"}
|
||||
|
||||
# Add ETH/BTC to blacklist
|
||||
rc = client_post(client, f"{BASE_URI}/blacklist",
|
||||
data='{"blacklist": ["ETH/BTC"]}')
|
||||
assert_response(rc)
|
||||
assert rc.json == {"blacklist": ["DOGE/BTC", "HOT/BTC", "ETH/BTC"],
|
||||
"length": 3,
|
||||
"method": "StaticPairList"}
|
||||
|
||||
|
||||
def test_api_whitelist(botclient):
|
||||
ftbot, client = botclient
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/whitelist")
|
||||
assert_response(rc)
|
||||
assert rc.json == {"whitelist": ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC'],
|
||||
"length": 4,
|
||||
"method": "StaticPairList"}
|
||||
|
||||
|
||||
def test_api_forcebuy(botclient, mocker, fee):
|
||||
ftbot, client = botclient
|
||||
|
||||
rc = client_post(client, f"{BASE_URI}/forcebuy",
|
||||
data='{"pair": "ETH/BTC"}')
|
||||
assert_response(rc, 502)
|
||||
assert rc.json == {"error": "Error querying _forcebuy: Forcebuy not enabled."}
|
||||
|
||||
# enable forcebuy
|
||||
ftbot.config["forcebuy_enable"] = True
|
||||
|
||||
fbuy_mock = MagicMock(return_value=None)
|
||||
mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock)
|
||||
rc = client_post(client, f"{BASE_URI}/forcebuy",
|
||||
data='{"pair": "ETH/BTC"}')
|
||||
assert_response(rc)
|
||||
assert rc.json == {"status": "Error buying pair ETH/BTC."}
|
||||
|
||||
# Test creating trae
|
||||
fbuy_mock = MagicMock(return_value=Trade(
|
||||
pair='ETH/ETH',
|
||||
amount=1,
|
||||
exchange='bittrex',
|
||||
stake_amount=1,
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456",
|
||||
open_date=datetime.utcnow(),
|
||||
is_open=False,
|
||||
fee_close=fee.return_value,
|
||||
fee_open=fee.return_value,
|
||||
close_rate=0.265441,
|
||||
))
|
||||
mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock)
|
||||
|
||||
rc = client_post(client, f"{BASE_URI}/forcebuy",
|
||||
data='{"pair": "ETH/BTC"}')
|
||||
assert_response(rc)
|
||||
assert rc.json == {'amount': 1,
|
||||
'close_date': None,
|
||||
'close_date_hum': None,
|
||||
'close_rate': 0.265441,
|
||||
'initial_stop_loss': None,
|
||||
'initial_stop_loss_pct': None,
|
||||
'open_date': ANY,
|
||||
'open_date_hum': 'just now',
|
||||
'open_rate': 0.245441,
|
||||
'pair': 'ETH/ETH',
|
||||
'stake_amount': 1,
|
||||
'stop_loss': None,
|
||||
'stop_loss_pct': None,
|
||||
'trade_id': None}
|
||||
|
||||
|
||||
def test_api_forcesell(botclient, mocker, ticker, fee, markets):
|
||||
ftbot, client = botclient
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_balances=MagicMock(return_value=ticker),
|
||||
get_ticker=ticker,
|
||||
get_fee=fee,
|
||||
markets=PropertyMock(return_value=markets)
|
||||
)
|
||||
patch_get_signal(ftbot, (True, False))
|
||||
|
||||
rc = client_post(client, f"{BASE_URI}/forcesell",
|
||||
data='{"tradeid": "1"}')
|
||||
assert_response(rc, 502)
|
||||
assert rc.json == {"error": "Error querying _forcesell: invalid argument"}
|
||||
|
||||
ftbot.create_trades()
|
||||
|
||||
rc = client_post(client, f"{BASE_URI}/forcesell",
|
||||
data='{"tradeid": "1"}')
|
||||
assert_response(rc)
|
||||
assert rc.json == {'result': 'Created sell order for trade 1.'}
|
182
tests/rpc/test_rpc_manager.py
Normal file
182
tests/rpc/test_rpc_manager.py
Normal file
@@ -0,0 +1,182 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
|
||||
import logging
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freqtrade.rpc import RPCMessageType, RPCManager
|
||||
from freqtrade.tests.conftest import log_has, get_patched_freqtradebot
|
||||
|
||||
|
||||
def test__init__(mocker, default_conf) -> None:
|
||||
default_conf['telegram']['enabled'] = False
|
||||
|
||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||
assert rpc_manager.registered_modules == []
|
||||
|
||||
|
||||
def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
default_conf['telegram']['enabled'] = False
|
||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||
|
||||
assert not log_has('Enabling rpc.telegram ...', caplog)
|
||||
assert rpc_manager.registered_modules == []
|
||||
|
||||
|
||||
def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||
|
||||
assert log_has('Enabling rpc.telegram ...', caplog)
|
||||
len_modules = len(rpc_manager.registered_modules)
|
||||
assert len_modules == 1
|
||||
assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
|
||||
|
||||
|
||||
def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
|
||||
default_conf['telegram']['enabled'] = False
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
rpc_manager = RPCManager(freqtradebot)
|
||||
rpc_manager.cleanup()
|
||||
|
||||
assert not log_has('Cleaning up rpc.telegram ...', caplog)
|
||||
assert telegram_mock.call_count == 0
|
||||
|
||||
|
||||
def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
rpc_manager = RPCManager(freqtradebot)
|
||||
|
||||
# Check we have Telegram as a registered modules
|
||||
assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
|
||||
|
||||
rpc_manager.cleanup()
|
||||
assert log_has('Cleaning up rpc.telegram ...', caplog)
|
||||
assert 'telegram' not in [mod.name for mod in rpc_manager.registered_modules]
|
||||
assert telegram_mock.call_count == 1
|
||||
|
||||
|
||||
def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
|
||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||
default_conf['telegram']['enabled'] = False
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
rpc_manager = RPCManager(freqtradebot)
|
||||
rpc_manager.send_msg({
|
||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||
'status': 'test'
|
||||
})
|
||||
|
||||
assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog)
|
||||
assert telegram_mock.call_count == 0
|
||||
|
||||
|
||||
def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
rpc_manager = RPCManager(freqtradebot)
|
||||
rpc_manager.send_msg({
|
||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||
'status': 'test'
|
||||
})
|
||||
|
||||
assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog)
|
||||
assert telegram_mock.call_count == 1
|
||||
|
||||
|
||||
def test_init_webhook_disabled(mocker, default_conf, caplog) -> None:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
default_conf['telegram']['enabled'] = False
|
||||
default_conf['webhook'] = {'enabled': False}
|
||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||
|
||||
assert not log_has('Enabling rpc.webhook ...', caplog)
|
||||
assert rpc_manager.registered_modules == []
|
||||
|
||||
|
||||
def test_init_webhook_enabled(mocker, default_conf, caplog) -> None:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
default_conf['telegram']['enabled'] = False
|
||||
default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"}
|
||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||
|
||||
assert log_has('Enabling rpc.webhook ...', caplog)
|
||||
assert len(rpc_manager.registered_modules) == 1
|
||||
assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules]
|
||||
|
||||
|
||||
def test_send_msg_webhook_CustomMessagetype(mocker, default_conf, caplog) -> None:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
default_conf['telegram']['enabled'] = False
|
||||
default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"}
|
||||
mocker.patch('freqtrade.rpc.webhook.Webhook.send_msg',
|
||||
MagicMock(side_effect=NotImplementedError))
|
||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||
|
||||
assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules]
|
||||
rpc_manager.send_msg({'type': RPCMessageType.CUSTOM_NOTIFICATION,
|
||||
'status': 'TestMessage'})
|
||||
assert log_has(
|
||||
"Message type RPCMessageType.CUSTOM_NOTIFICATION not implemented by handler webhook.",
|
||||
caplog)
|
||||
|
||||
|
||||
def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
rpc_manager = RPCManager(freqtradebot)
|
||||
rpc_manager.startup_messages(default_conf, freqtradebot.pairlists)
|
||||
|
||||
assert telegram_mock.call_count == 3
|
||||
assert "*Exchange:* `bittrex`" in telegram_mock.call_args_list[1][0][0]['status']
|
||||
|
||||
telegram_mock.reset_mock()
|
||||
default_conf['dry_run'] = True
|
||||
default_conf['whitelist'] = {'method': 'VolumePairList',
|
||||
'config': {'number_assets': 20}
|
||||
}
|
||||
|
||||
rpc_manager.startup_messages(default_conf, freqtradebot.pairlists)
|
||||
assert telegram_mock.call_count == 3
|
||||
assert "Dry run is enabled." in telegram_mock.call_args_list[0][0][0]['status']
|
||||
|
||||
|
||||
def test_init_apiserver_disabled(mocker, default_conf, caplog) -> None:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
run_mock = MagicMock()
|
||||
mocker.patch('freqtrade.rpc.api_server.ApiServer.run', run_mock)
|
||||
default_conf['telegram']['enabled'] = False
|
||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||
|
||||
assert not log_has('Enabling rpc.api_server', caplog)
|
||||
assert rpc_manager.registered_modules == []
|
||||
assert run_mock.call_count == 0
|
||||
|
||||
|
||||
def test_init_apiserver_enabled(mocker, default_conf, caplog) -> None:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
run_mock = MagicMock()
|
||||
mocker.patch('freqtrade.rpc.api_server.ApiServer.run', run_mock)
|
||||
|
||||
default_conf["telegram"]["enabled"] = False
|
||||
default_conf["api_server"] = {"enabled": True,
|
||||
"listen_ip_address": "127.0.0.1",
|
||||
"listen_port": "8080"}
|
||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||
|
||||
assert log_has('Enabling rpc.api_server', caplog)
|
||||
assert len(rpc_manager.registered_modules) == 1
|
||||
assert 'apiserver' in [mod.name for mod in rpc_manager.registered_modules]
|
||||
assert run_mock.call_count == 1
|
1447
tests/rpc/test_rpc_telegram.py
Normal file
1447
tests/rpc/test_rpc_telegram.py
Normal file
File diff suppressed because it is too large
Load Diff
170
tests/rpc/test_rpc_webhook.py
Normal file
170
tests/rpc/test_rpc_webhook.py
Normal file
@@ -0,0 +1,170 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103, protected-access
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from requests import RequestException
|
||||
|
||||
from freqtrade.rpc import RPCMessageType
|
||||
from freqtrade.rpc.webhook import Webhook
|
||||
from freqtrade.strategy.interface import SellType
|
||||
from freqtrade.tests.conftest import get_patched_freqtradebot, log_has
|
||||
|
||||
|
||||
def get_webhook_dict() -> dict:
|
||||
return {
|
||||
"enabled": True,
|
||||
"url": "https://maker.ifttt.com/trigger/freqtrade_test/with/key/c764udvJ5jfSlswVRukZZ2/",
|
||||
"webhookbuy": {
|
||||
"value1": "Buying {pair}",
|
||||
"value2": "limit {limit:8f}",
|
||||
"value3": "{stake_amount:8f} {stake_currency}"
|
||||
},
|
||||
"webhooksell": {
|
||||
"value1": "Selling {pair}",
|
||||
"value2": "limit {limit:8f}",
|
||||
"value3": "profit: {profit_amount:8f} {stake_currency}"
|
||||
},
|
||||
"webhookstatus": {
|
||||
"value1": "Status: {status}",
|
||||
"value2": "",
|
||||
"value3": ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test__init__(mocker, default_conf):
|
||||
default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"}
|
||||
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
|
||||
assert webhook._config == default_conf
|
||||
|
||||
|
||||
def test_send_msg(default_conf, mocker):
|
||||
default_conf["webhook"] = get_webhook_dict()
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
|
||||
msg = {
|
||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'limit': 0.005,
|
||||
'stake_amount': 0.8,
|
||||
'stake_amount_fiat': 500,
|
||||
'stake_currency': 'BTC',
|
||||
'fiat_currency': 'EUR'
|
||||
}
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||
webhook.send_msg(msg=msg)
|
||||
assert msg_mock.call_count == 1
|
||||
assert (msg_mock.call_args[0][0]["value1"] ==
|
||||
default_conf["webhook"]["webhookbuy"]["value1"].format(**msg))
|
||||
assert (msg_mock.call_args[0][0]["value2"] ==
|
||||
default_conf["webhook"]["webhookbuy"]["value2"].format(**msg))
|
||||
assert (msg_mock.call_args[0][0]["value3"] ==
|
||||
default_conf["webhook"]["webhookbuy"]["value3"].format(**msg))
|
||||
# Test sell
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||
msg = {
|
||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'gain': "profit",
|
||||
'limit': 0.005,
|
||||
'amount': 0.8,
|
||||
'order_type': 'limit',
|
||||
'open_rate': 0.004,
|
||||
'current_rate': 0.005,
|
||||
'profit_amount': 0.001,
|
||||
'profit_percent': 0.20,
|
||||
'stake_currency': 'BTC',
|
||||
'sell_reason': SellType.STOP_LOSS.value
|
||||
}
|
||||
webhook.send_msg(msg=msg)
|
||||
assert msg_mock.call_count == 1
|
||||
assert (msg_mock.call_args[0][0]["value1"] ==
|
||||
default_conf["webhook"]["webhooksell"]["value1"].format(**msg))
|
||||
assert (msg_mock.call_args[0][0]["value2"] ==
|
||||
default_conf["webhook"]["webhooksell"]["value2"].format(**msg))
|
||||
assert (msg_mock.call_args[0][0]["value3"] ==
|
||||
default_conf["webhook"]["webhooksell"]["value3"].format(**msg))
|
||||
|
||||
for msgtype in [RPCMessageType.STATUS_NOTIFICATION,
|
||||
RPCMessageType.WARNING_NOTIFICATION,
|
||||
RPCMessageType.CUSTOM_NOTIFICATION]:
|
||||
# Test notification
|
||||
msg = {
|
||||
'type': msgtype,
|
||||
'status': 'Unfilled sell order for BTC cancelled due to timeout'
|
||||
}
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||
webhook.send_msg(msg)
|
||||
assert msg_mock.call_count == 1
|
||||
assert (msg_mock.call_args[0][0]["value1"] ==
|
||||
default_conf["webhook"]["webhookstatus"]["value1"].format(**msg))
|
||||
assert (msg_mock.call_args[0][0]["value2"] ==
|
||||
default_conf["webhook"]["webhookstatus"]["value2"].format(**msg))
|
||||
assert (msg_mock.call_args[0][0]["value3"] ==
|
||||
default_conf["webhook"]["webhookstatus"]["value3"].format(**msg))
|
||||
|
||||
|
||||
def test_exception_send_msg(default_conf, mocker, caplog):
|
||||
default_conf["webhook"] = get_webhook_dict()
|
||||
default_conf["webhook"]["webhookbuy"] = None
|
||||
|
||||
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
|
||||
webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION})
|
||||
assert log_has(f"Message type {RPCMessageType.BUY_NOTIFICATION} not configured for webhooks",
|
||||
caplog)
|
||||
|
||||
default_conf["webhook"] = get_webhook_dict()
|
||||
default_conf["webhook"]["webhookbuy"]["value1"] = "{DEADBEEF:8f}"
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
|
||||
msg = {
|
||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'limit': 0.005,
|
||||
'order_type': 'limit',
|
||||
'stake_amount': 0.8,
|
||||
'stake_amount_fiat': 500,
|
||||
'stake_currency': 'BTC',
|
||||
'fiat_currency': 'EUR'
|
||||
}
|
||||
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)
|
||||
|
||||
|
||||
def test__send_msg(default_conf, mocker, caplog):
|
||||
default_conf["webhook"] = get_webhook_dict()
|
||||
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
|
||||
msg = {'value1': 'DEADBEEF',
|
||||
'value2': 'ALIVEBEEF',
|
||||
'value3': 'FREQTRADE'}
|
||||
post = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.post", post)
|
||||
webhook._send_msg(msg)
|
||||
|
||||
assert post.call_count == 1
|
||||
assert post.call_args[1] == {'data': msg}
|
||||
assert post.call_args[0] == (default_conf['webhook']['url'], )
|
||||
|
||||
post = MagicMock(side_effect=RequestException)
|
||||
mocker.patch("freqtrade.rpc.webhook.post", post)
|
||||
webhook._send_msg(msg)
|
||||
assert log_has('Could not call webhook url. Exception: ', caplog)
|
0
tests/strategy/__init__.py
Normal file
0
tests/strategy/__init__.py
Normal file
235
tests/strategy/legacy_strategy.py
Normal file
235
tests/strategy/legacy_strategy.py
Normal file
@@ -0,0 +1,235 @@
|
||||
|
||||
# --- Do not remove these libs ---
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from pandas import DataFrame
|
||||
# --------------------------------
|
||||
|
||||
# Add your lib to import here
|
||||
import talib.abstract as ta
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
import numpy # noqa
|
||||
|
||||
|
||||
# This class is a sample. Feel free to customize it.
|
||||
class TestStrategyLegacy(IStrategy):
|
||||
"""
|
||||
This is a test strategy using the legacy function headers, which will be
|
||||
removed in a future update.
|
||||
Please do not use this as a template, but refer to user_data/strategy/sample_strategy.py
|
||||
for a uptodate version of this template.
|
||||
|
||||
"""
|
||||
|
||||
# Minimal ROI designed for the strategy.
|
||||
# This attribute will be overridden if the config file contains "minimal_roi"
|
||||
minimal_roi = {
|
||||
"40": 0.0,
|
||||
"30": 0.01,
|
||||
"20": 0.02,
|
||||
"0": 0.04
|
||||
}
|
||||
|
||||
# Optimal stoploss designed for the strategy
|
||||
# This attribute will be overridden if the config file contains "stoploss"
|
||||
stoploss = -0.10
|
||||
|
||||
# Optimal ticker interval for the strategy
|
||||
ticker_interval = '5m'
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Adds several different TA indicators to the given DataFrame
|
||||
|
||||
Performance Note: For the best performance be frugal on the number of indicators
|
||||
you are using. Let uncomment only the indicator you are using in your strategies
|
||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
||||
"""
|
||||
|
||||
# Momentum Indicator
|
||||
# ------------------------------------
|
||||
|
||||
# ADX
|
||||
dataframe['adx'] = ta.ADX(dataframe)
|
||||
|
||||
"""
|
||||
# Awesome oscillator
|
||||
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
|
||||
|
||||
# Commodity Channel Index: values Oversold:<-100, Overbought:>100
|
||||
dataframe['cci'] = ta.CCI(dataframe)
|
||||
|
||||
# MACD
|
||||
macd = ta.MACD(dataframe)
|
||||
dataframe['macd'] = macd['macd']
|
||||
dataframe['macdsignal'] = macd['macdsignal']
|
||||
dataframe['macdhist'] = macd['macdhist']
|
||||
|
||||
# MFI
|
||||
dataframe['mfi'] = ta.MFI(dataframe)
|
||||
|
||||
# Minus Directional Indicator / Movement
|
||||
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
|
||||
# Plus Directional Indicator / Movement
|
||||
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
|
||||
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
|
||||
# ROC
|
||||
dataframe['roc'] = ta.ROC(dataframe)
|
||||
|
||||
# RSI
|
||||
dataframe['rsi'] = ta.RSI(dataframe)
|
||||
|
||||
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
|
||||
rsi = 0.1 * (dataframe['rsi'] - 50)
|
||||
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
|
||||
|
||||
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
|
||||
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
|
||||
|
||||
# Stoch
|
||||
stoch = ta.STOCH(dataframe)
|
||||
dataframe['slowd'] = stoch['slowd']
|
||||
dataframe['slowk'] = stoch['slowk']
|
||||
|
||||
# Stoch fast
|
||||
stoch_fast = ta.STOCHF(dataframe)
|
||||
dataframe['fastd'] = stoch_fast['fastd']
|
||||
dataframe['fastk'] = stoch_fast['fastk']
|
||||
|
||||
# Stoch RSI
|
||||
stoch_rsi = ta.STOCHRSI(dataframe)
|
||||
dataframe['fastd_rsi'] = stoch_rsi['fastd']
|
||||
dataframe['fastk_rsi'] = stoch_rsi['fastk']
|
||||
"""
|
||||
|
||||
# Overlap Studies
|
||||
# ------------------------------------
|
||||
|
||||
# Bollinger bands
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe['bb_lowerband'] = bollinger['lower']
|
||||
dataframe['bb_middleband'] = bollinger['mid']
|
||||
dataframe['bb_upperband'] = bollinger['upper']
|
||||
|
||||
"""
|
||||
# EMA - Exponential Moving Average
|
||||
dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
|
||||
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
||||
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
||||
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
||||
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
||||
|
||||
# SAR Parabol
|
||||
dataframe['sar'] = ta.SAR(dataframe)
|
||||
|
||||
# SMA - Simple Moving Average
|
||||
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
||||
"""
|
||||
|
||||
# TEMA - Triple Exponential Moving Average
|
||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||
|
||||
# Cycle Indicator
|
||||
# ------------------------------------
|
||||
# Hilbert Transform Indicator - SineWave
|
||||
hilbert = ta.HT_SINE(dataframe)
|
||||
dataframe['htsine'] = hilbert['sine']
|
||||
dataframe['htleadsine'] = hilbert['leadsine']
|
||||
|
||||
# Pattern Recognition - Bullish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Hammer: values [0, 100]
|
||||
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
|
||||
# Inverted Hammer: values [0, 100]
|
||||
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
|
||||
# Dragonfly Doji: values [0, 100]
|
||||
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
|
||||
# Piercing Line: values [0, 100]
|
||||
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
|
||||
# Morningstar: values [0, 100]
|
||||
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
|
||||
# Three White Soldiers: values [0, 100]
|
||||
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
|
||||
"""
|
||||
|
||||
# Pattern Recognition - Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Hanging Man: values [0, 100]
|
||||
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
|
||||
# Shooting Star: values [0, 100]
|
||||
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
|
||||
# Gravestone Doji: values [0, 100]
|
||||
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
|
||||
# Dark Cloud Cover: values [0, 100]
|
||||
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
|
||||
# Evening Doji Star: values [0, 100]
|
||||
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
|
||||
# Evening Star: values [0, 100]
|
||||
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
|
||||
"""
|
||||
|
||||
# Pattern Recognition - Bullish/Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Three Line Strike: values [0, -100, 100]
|
||||
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
|
||||
# Spinning Top: values [0, -100, 100]
|
||||
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
|
||||
# Engulfing: values [0, -100, 100]
|
||||
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
|
||||
# Harami: values [0, -100, 100]
|
||||
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
|
||||
# Three Outside Up/Down: values [0, -100, 100]
|
||||
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
|
||||
# Three Inside Up/Down: values [0, -100, 100]
|
||||
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
|
||||
"""
|
||||
|
||||
# Chart type
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Heikinashi stategy
|
||||
heikinashi = qtpylib.heikinashi(dataframe)
|
||||
dataframe['ha_open'] = heikinashi['open']
|
||||
dataframe['ha_close'] = heikinashi['close']
|
||||
dataframe['ha_high'] = heikinashi['high']
|
||||
dataframe['ha_low'] = heikinashi['low']
|
||||
"""
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the buy signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['adx'] > 30) &
|
||||
(dataframe['tema'] <= dataframe['bb_middleband']) &
|
||||
(dataframe['tema'] > dataframe['tema'].shift(1))
|
||||
),
|
||||
'buy'] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the sell signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe['adx'] > 70) &
|
||||
(dataframe['tema'] > dataframe['bb_middleband']) &
|
||||
(dataframe['tema'] < dataframe['tema'].shift(1))
|
||||
),
|
||||
'sell'] = 1
|
||||
return dataframe
|
36
tests/strategy/test_default_strategy.py
Normal file
36
tests/strategy/test_default_strategy.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.data.converter import parse_ticker_dataframe
|
||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def result():
|
||||
with open('freqtrade/tests/testdata/ETH_BTC-1m.json') as data_file:
|
||||
return parse_ticker_dataframe(json.load(data_file), '1m', pair="UNITTEST/BTC",
|
||||
fill_missing=True)
|
||||
|
||||
|
||||
def test_default_strategy_structure():
|
||||
assert hasattr(DefaultStrategy, 'minimal_roi')
|
||||
assert hasattr(DefaultStrategy, 'stoploss')
|
||||
assert hasattr(DefaultStrategy, 'ticker_interval')
|
||||
assert hasattr(DefaultStrategy, 'populate_indicators')
|
||||
assert hasattr(DefaultStrategy, 'populate_buy_trend')
|
||||
assert hasattr(DefaultStrategy, 'populate_sell_trend')
|
||||
|
||||
|
||||
def test_default_strategy(result):
|
||||
strategy = DefaultStrategy({})
|
||||
|
||||
metadata = {'pair': 'ETH/BTC'}
|
||||
assert type(strategy.minimal_roi) is dict
|
||||
assert type(strategy.stoploss) is float
|
||||
assert type(strategy.ticker_interval) is str
|
||||
indicators = strategy.populate_indicators(result, metadata)
|
||||
assert type(indicators) is DataFrame
|
||||
assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame
|
||||
assert type(strategy.populate_sell_trend(indicators, metadata)) is DataFrame
|
304
tests/strategy/test_interface.py
Normal file
304
tests/strategy/test_interface.py
Normal file
@@ -0,0 +1,304 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
|
||||
import logging
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data.converter import parse_ticker_dataframe
|
||||
from freqtrade.data.history import load_tickerdata_file
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.tests.conftest import get_patched_exchange, log_has
|
||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||
|
||||
# Avoid to reinit the same object again and again
|
||||
_STRATEGY = DefaultStrategy(config={})
|
||||
|
||||
|
||||
def test_returns_latest_buy_signal(mocker, default_conf, ticker_history):
|
||||
mocker.patch.object(
|
||||
_STRATEGY, '_analyze_ticker_internal',
|
||||
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
|
||||
)
|
||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False)
|
||||
|
||||
mocker.patch.object(
|
||||
_STRATEGY, '_analyze_ticker_internal',
|
||||
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
|
||||
)
|
||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True)
|
||||
|
||||
|
||||
def test_returns_latest_sell_signal(mocker, default_conf, ticker_history):
|
||||
mocker.patch.object(
|
||||
_STRATEGY, '_analyze_ticker_internal',
|
||||
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
|
||||
)
|
||||
|
||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True)
|
||||
|
||||
mocker.patch.object(
|
||||
_STRATEGY, '_analyze_ticker_internal',
|
||||
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
|
||||
)
|
||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False)
|
||||
|
||||
|
||||
def test_get_signal_empty(default_conf, mocker, caplog):
|
||||
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
|
||||
DataFrame())
|
||||
assert log_has('Empty ticker history for pair foo', caplog)
|
||||
caplog.clear()
|
||||
|
||||
assert (False, False) == _STRATEGY.get_signal('bar', default_conf['ticker_interval'],
|
||||
[])
|
||||
assert log_has('Empty ticker history for pair bar', caplog)
|
||||
|
||||
|
||||
def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_history):
|
||||
caplog.set_level(logging.INFO)
|
||||
mocker.patch.object(
|
||||
_STRATEGY, '_analyze_ticker_internal',
|
||||
side_effect=ValueError('xyz')
|
||||
)
|
||||
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
|
||||
ticker_history)
|
||||
assert log_has('Unable to analyze ticker for pair foo: xyz', caplog)
|
||||
|
||||
|
||||
def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ticker_history):
|
||||
caplog.set_level(logging.INFO)
|
||||
mocker.patch.object(
|
||||
_STRATEGY, '_analyze_ticker_internal',
|
||||
return_value=DataFrame([])
|
||||
)
|
||||
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
|
||||
ticker_history)
|
||||
assert log_has('Empty dataframe for pair xyz', caplog)
|
||||
|
||||
|
||||
def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history):
|
||||
caplog.set_level(logging.INFO)
|
||||
# default_conf defines a 5m interval. we check interval * 2 + 5m
|
||||
# this is necessary as the last candle is removed (partial candles) by default
|
||||
oldtime = arrow.utcnow().shift(minutes=-16)
|
||||
ticks = DataFrame([{'buy': 1, 'date': oldtime}])
|
||||
mocker.patch.object(
|
||||
_STRATEGY, '_analyze_ticker_internal',
|
||||
return_value=DataFrame(ticks)
|
||||
)
|
||||
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
|
||||
ticker_history)
|
||||
assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog)
|
||||
|
||||
|
||||
def test_get_signal_handles_exceptions(mocker, default_conf):
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
mocker.patch.object(
|
||||
_STRATEGY, 'analyze_ticker',
|
||||
side_effect=Exception('invalid ticker history ')
|
||||
)
|
||||
assert _STRATEGY.get_signal(exchange, 'ETH/BTC', '5m') == (False, False)
|
||||
|
||||
|
||||
def test_tickerdata_to_dataframe(default_conf, testdatadir) -> None:
|
||||
strategy = DefaultStrategy(default_conf)
|
||||
|
||||
timerange = TimeRange(None, 'line', 0, -100)
|
||||
tick = load_tickerdata_file(testdatadir, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC",
|
||||
fill_missing=True)}
|
||||
data = strategy.tickerdata_to_dataframe(tickerlist)
|
||||
assert len(data['UNITTEST/BTC']) == 102 # partial candle was removed
|
||||
|
||||
|
||||
def test_min_roi_reached(default_conf, fee) -> None:
|
||||
|
||||
# Use list to confirm sequence does not matter
|
||||
min_roi_list = [{20: 0.05, 55: 0.01, 0: 0.1},
|
||||
{0: 0.1, 20: 0.05, 55: 0.01}]
|
||||
for roi in min_roi_list:
|
||||
strategy = DefaultStrategy(default_conf)
|
||||
strategy.minimal_roi = roi
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
open_date=arrow.utcnow().shift(hours=-1).datetime,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
exchange='bittrex',
|
||||
open_rate=1,
|
||||
)
|
||||
|
||||
assert not strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-56).datetime)
|
||||
assert strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-56).datetime)
|
||||
|
||||
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime)
|
||||
assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-39).datetime)
|
||||
|
||||
assert not strategy.min_roi_reached(trade, -0.01, arrow.utcnow().shift(minutes=-1).datetime)
|
||||
assert strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-1).datetime)
|
||||
|
||||
|
||||
def test_min_roi_reached2(default_conf, fee) -> None:
|
||||
|
||||
# test with ROI raising after last interval
|
||||
min_roi_list = [{20: 0.07,
|
||||
30: 0.05,
|
||||
55: 0.30,
|
||||
0: 0.1
|
||||
},
|
||||
{0: 0.1,
|
||||
20: 0.07,
|
||||
30: 0.05,
|
||||
55: 0.30
|
||||
},
|
||||
]
|
||||
for roi in min_roi_list:
|
||||
strategy = DefaultStrategy(default_conf)
|
||||
strategy.minimal_roi = roi
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
open_date=arrow.utcnow().shift(hours=-1).datetime,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
exchange='bittrex',
|
||||
open_rate=1,
|
||||
)
|
||||
|
||||
assert not strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-56).datetime)
|
||||
assert strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-56).datetime)
|
||||
|
||||
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime)
|
||||
assert strategy.min_roi_reached(trade, 0.071, arrow.utcnow().shift(minutes=-39).datetime)
|
||||
|
||||
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-26).datetime)
|
||||
assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-26).datetime)
|
||||
|
||||
# Should not trigger with 20% profit since after 55 minutes only 30% is active.
|
||||
assert not strategy.min_roi_reached(trade, 0.20, arrow.utcnow().shift(minutes=-2).datetime)
|
||||
assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime)
|
||||
|
||||
|
||||
def test_min_roi_reached3(default_conf, fee) -> None:
|
||||
|
||||
# test for issue #1948
|
||||
min_roi = {20: 0.07,
|
||||
30: 0.05,
|
||||
55: 0.30,
|
||||
}
|
||||
strategy = DefaultStrategy(default_conf)
|
||||
strategy.minimal_roi = min_roi
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
open_date=arrow.utcnow().shift(hours=-1).datetime,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
exchange='bittrex',
|
||||
open_rate=1,
|
||||
)
|
||||
|
||||
assert not strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-56).datetime)
|
||||
assert not strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-56).datetime)
|
||||
|
||||
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime)
|
||||
assert strategy.min_roi_reached(trade, 0.071, arrow.utcnow().shift(minutes=-39).datetime)
|
||||
|
||||
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-26).datetime)
|
||||
assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-26).datetime)
|
||||
|
||||
# Should not trigger with 20% profit since after 55 minutes only 30% is active.
|
||||
assert not strategy.min_roi_reached(trade, 0.20, arrow.utcnow().shift(minutes=-2).datetime)
|
||||
assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime)
|
||||
|
||||
|
||||
def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
ind_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||
buy_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||
sell_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.strategy.interface.IStrategy',
|
||||
advise_indicators=ind_mock,
|
||||
advise_buy=buy_mock,
|
||||
advise_sell=sell_mock,
|
||||
|
||||
)
|
||||
strategy = DefaultStrategy({})
|
||||
strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
|
||||
assert ind_mock.call_count == 1
|
||||
assert buy_mock.call_count == 1
|
||||
assert buy_mock.call_count == 1
|
||||
|
||||
assert log_has('TA Analysis Launched', caplog)
|
||||
assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
|
||||
caplog.clear()
|
||||
|
||||
strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
|
||||
# No analysis happens as process_only_new_candles is true
|
||||
assert ind_mock.call_count == 2
|
||||
assert buy_mock.call_count == 2
|
||||
assert buy_mock.call_count == 2
|
||||
assert log_has('TA Analysis Launched', caplog)
|
||||
assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
|
||||
|
||||
|
||||
def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) -> None:
|
||||
caplog.set_level(logging.DEBUG)
|
||||
ind_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||
buy_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||
sell_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.strategy.interface.IStrategy',
|
||||
advise_indicators=ind_mock,
|
||||
advise_buy=buy_mock,
|
||||
advise_sell=sell_mock,
|
||||
|
||||
)
|
||||
strategy = DefaultStrategy({})
|
||||
strategy.process_only_new_candles = True
|
||||
|
||||
ret = strategy._analyze_ticker_internal(ticker_history, {'pair': 'ETH/BTC'})
|
||||
assert 'high' in ret.columns
|
||||
assert 'low' in ret.columns
|
||||
assert 'close' in ret.columns
|
||||
assert isinstance(ret, DataFrame)
|
||||
assert ind_mock.call_count == 1
|
||||
assert buy_mock.call_count == 1
|
||||
assert buy_mock.call_count == 1
|
||||
assert log_has('TA Analysis Launched', caplog)
|
||||
assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
|
||||
caplog.clear()
|
||||
|
||||
ret = strategy._analyze_ticker_internal(ticker_history, {'pair': 'ETH/BTC'})
|
||||
# No analysis happens as process_only_new_candles is true
|
||||
assert ind_mock.call_count == 1
|
||||
assert buy_mock.call_count == 1
|
||||
assert buy_mock.call_count == 1
|
||||
# only skipped analyze adds buy and sell columns, otherwise it's all mocked
|
||||
assert 'buy' in ret.columns
|
||||
assert 'sell' in ret.columns
|
||||
assert ret['buy'].sum() == 0
|
||||
assert ret['sell'].sum() == 0
|
||||
assert not log_has('TA Analysis Launched', caplog)
|
||||
assert log_has('Skipping TA Analysis for already analyzed candle', caplog)
|
||||
|
||||
|
||||
def test_is_pair_locked(default_conf):
|
||||
strategy = DefaultStrategy(default_conf)
|
||||
# dict should be empty
|
||||
assert not strategy._pair_locked_until
|
||||
|
||||
pair = 'ETH/BTC'
|
||||
assert not strategy.is_pair_locked(pair)
|
||||
strategy.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime)
|
||||
# ETH/BTC locked for 4 minutes
|
||||
assert strategy.is_pair_locked(pair)
|
||||
|
||||
# XRP/BTC should not be locked now
|
||||
pair = 'XRP/BTC'
|
||||
assert not strategy.is_pair_locked(pair)
|
385
tests/strategy/test_strategy.py
Normal file
385
tests/strategy/test_strategy.py
Normal file
@@ -0,0 +1,385 @@
|
||||
# pragma pylint: disable=missing-docstring, protected-access, C0103
|
||||
import logging
|
||||
import tempfile
|
||||
import warnings
|
||||
from base64 import urlsafe_b64encode
|
||||
from os import path
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
from freqtrade.strategy.interface import IStrategy
|
||||
from freqtrade.tests.conftest import log_has, log_has_re
|
||||
|
||||
|
||||
def test_search_strategy():
|
||||
default_config = {}
|
||||
default_location = Path(__file__).parent.parent.parent.joinpath('strategy').resolve()
|
||||
|
||||
s, _ = StrategyResolver._search_object(
|
||||
directory=default_location,
|
||||
object_type=IStrategy,
|
||||
kwargs={'config': default_config},
|
||||
object_name='DefaultStrategy'
|
||||
)
|
||||
assert isinstance(s, IStrategy)
|
||||
|
||||
s, _ = StrategyResolver._search_object(
|
||||
directory=default_location,
|
||||
object_type=IStrategy,
|
||||
kwargs={'config': default_config},
|
||||
object_name='NotFoundStrategy'
|
||||
)
|
||||
assert s is None
|
||||
|
||||
|
||||
def test_load_strategy(default_conf, result):
|
||||
default_conf.update({'strategy': 'SampleStrategy'})
|
||||
resolver = StrategyResolver(default_conf)
|
||||
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
|
||||
|
||||
|
||||
def test_load_strategy_base64(result, caplog, default_conf):
|
||||
with open("user_data/strategies/sample_strategy.py", "rb") as file:
|
||||
encoded_string = urlsafe_b64encode(file.read()).decode("utf-8")
|
||||
default_conf.update({'strategy': 'SampleStrategy:{}'.format(encoded_string)})
|
||||
|
||||
resolver = StrategyResolver(default_conf)
|
||||
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
|
||||
# Make sure strategy was loaded from base64 (using temp directory)!!
|
||||
assert log_has_re(r"Using resolved strategy SampleStrategy from '"
|
||||
+ tempfile.gettempdir() + r"/.*/SampleStrategy\.py'\.\.\.", caplog)
|
||||
|
||||
|
||||
def test_load_strategy_invalid_directory(result, caplog, default_conf):
|
||||
resolver = StrategyResolver(default_conf)
|
||||
extra_dir = Path.cwd() / 'some/path'
|
||||
resolver._load_strategy('SampleStrategy', config=default_conf, extra_dir=extra_dir)
|
||||
|
||||
assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog)
|
||||
|
||||
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
|
||||
|
||||
|
||||
def test_load_not_found_strategy(default_conf):
|
||||
strategy = StrategyResolver(default_conf)
|
||||
with pytest.raises(OperationalException,
|
||||
match=r"Impossible to load Strategy 'NotFoundStrategy'. "
|
||||
r"This class does not exist or contains Python code errors."):
|
||||
strategy._load_strategy(strategy_name='NotFoundStrategy', config=default_conf)
|
||||
|
||||
|
||||
def test_strategy(result, default_conf):
|
||||
default_conf.update({'strategy': 'DefaultStrategy'})
|
||||
|
||||
resolver = StrategyResolver(default_conf)
|
||||
metadata = {'pair': 'ETH/BTC'}
|
||||
assert resolver.strategy.minimal_roi[0] == 0.04
|
||||
assert default_conf["minimal_roi"]['0'] == 0.04
|
||||
|
||||
assert resolver.strategy.stoploss == -0.10
|
||||
assert default_conf['stoploss'] == -0.10
|
||||
|
||||
assert resolver.strategy.ticker_interval == '5m'
|
||||
assert default_conf['ticker_interval'] == '5m'
|
||||
|
||||
df_indicators = resolver.strategy.advise_indicators(result, metadata=metadata)
|
||||
assert 'adx' in df_indicators
|
||||
|
||||
dataframe = resolver.strategy.advise_buy(df_indicators, metadata=metadata)
|
||||
assert 'buy' in dataframe.columns
|
||||
|
||||
dataframe = resolver.strategy.advise_sell(df_indicators, metadata=metadata)
|
||||
assert 'sell' in dataframe.columns
|
||||
|
||||
|
||||
def test_strategy_override_minimal_roi(caplog, default_conf):
|
||||
caplog.set_level(logging.INFO)
|
||||
default_conf.update({
|
||||
'strategy': 'DefaultStrategy',
|
||||
'minimal_roi': {
|
||||
"0": 0.5
|
||||
}
|
||||
})
|
||||
resolver = StrategyResolver(default_conf)
|
||||
|
||||
assert resolver.strategy.minimal_roi[0] == 0.5
|
||||
assert log_has("Override strategy 'minimal_roi' with value in config file: {'0': 0.5}.", caplog)
|
||||
|
||||
|
||||
def test_strategy_override_stoploss(caplog, default_conf):
|
||||
caplog.set_level(logging.INFO)
|
||||
default_conf.update({
|
||||
'strategy': 'DefaultStrategy',
|
||||
'stoploss': -0.5
|
||||
})
|
||||
resolver = StrategyResolver(default_conf)
|
||||
|
||||
assert resolver.strategy.stoploss == -0.5
|
||||
assert log_has("Override strategy 'stoploss' with value in config file: -0.5.", caplog)
|
||||
|
||||
|
||||
def test_strategy_override_trailing_stop(caplog, default_conf):
|
||||
caplog.set_level(logging.INFO)
|
||||
default_conf.update({
|
||||
'strategy': 'DefaultStrategy',
|
||||
'trailing_stop': True
|
||||
})
|
||||
resolver = StrategyResolver(default_conf)
|
||||
|
||||
assert resolver.strategy.trailing_stop
|
||||
assert isinstance(resolver.strategy.trailing_stop, bool)
|
||||
assert log_has("Override strategy 'trailing_stop' with value in config file: True.", caplog)
|
||||
|
||||
|
||||
def test_strategy_override_trailing_stop_positive(caplog, default_conf):
|
||||
caplog.set_level(logging.INFO)
|
||||
default_conf.update({
|
||||
'strategy': 'DefaultStrategy',
|
||||
'trailing_stop_positive': -0.1,
|
||||
'trailing_stop_positive_offset': -0.2
|
||||
|
||||
})
|
||||
resolver = StrategyResolver(default_conf)
|
||||
|
||||
assert resolver.strategy.trailing_stop_positive == -0.1
|
||||
assert log_has("Override strategy 'trailing_stop_positive' with value in config file: -0.1.",
|
||||
caplog)
|
||||
|
||||
assert resolver.strategy.trailing_stop_positive_offset == -0.2
|
||||
assert log_has("Override strategy 'trailing_stop_positive' with value in config file: -0.1.",
|
||||
caplog)
|
||||
|
||||
|
||||
def test_strategy_override_ticker_interval(caplog, default_conf):
|
||||
caplog.set_level(logging.INFO)
|
||||
|
||||
default_conf.update({
|
||||
'strategy': 'DefaultStrategy',
|
||||
'ticker_interval': 60,
|
||||
'stake_currency': 'ETH'
|
||||
})
|
||||
resolver = StrategyResolver(default_conf)
|
||||
|
||||
assert resolver.strategy.ticker_interval == 60
|
||||
assert resolver.strategy.stake_currency == 'ETH'
|
||||
assert log_has("Override strategy 'ticker_interval' with value in config file: 60.",
|
||||
caplog)
|
||||
|
||||
|
||||
def test_strategy_override_process_only_new_candles(caplog, default_conf):
|
||||
caplog.set_level(logging.INFO)
|
||||
|
||||
default_conf.update({
|
||||
'strategy': 'DefaultStrategy',
|
||||
'process_only_new_candles': True
|
||||
})
|
||||
resolver = StrategyResolver(default_conf)
|
||||
|
||||
assert resolver.strategy.process_only_new_candles
|
||||
assert log_has("Override strategy 'process_only_new_candles' with value in config file: True.",
|
||||
caplog)
|
||||
|
||||
|
||||
def test_strategy_override_order_types(caplog, default_conf):
|
||||
caplog.set_level(logging.INFO)
|
||||
|
||||
order_types = {
|
||||
'buy': 'market',
|
||||
'sell': 'limit',
|
||||
'stoploss': 'limit',
|
||||
'stoploss_on_exchange': True,
|
||||
}
|
||||
default_conf.update({
|
||||
'strategy': 'DefaultStrategy',
|
||||
'order_types': order_types
|
||||
})
|
||||
resolver = StrategyResolver(default_conf)
|
||||
|
||||
assert resolver.strategy.order_types
|
||||
for method in ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']:
|
||||
assert resolver.strategy.order_types[method] == order_types[method]
|
||||
|
||||
assert log_has("Override strategy 'order_types' with value in config file:"
|
||||
" {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit',"
|
||||
" 'stoploss_on_exchange': True}.", caplog)
|
||||
|
||||
default_conf.update({
|
||||
'strategy': 'DefaultStrategy',
|
||||
'order_types': {'buy': 'market'}
|
||||
})
|
||||
# Raise error for invalid configuration
|
||||
with pytest.raises(ImportError,
|
||||
match=r"Impossible to load Strategy 'DefaultStrategy'. "
|
||||
r"Order-types mapping is incomplete."):
|
||||
StrategyResolver(default_conf)
|
||||
|
||||
|
||||
def test_strategy_override_order_tif(caplog, default_conf):
|
||||
caplog.set_level(logging.INFO)
|
||||
|
||||
order_time_in_force = {
|
||||
'buy': 'fok',
|
||||
'sell': 'gtc',
|
||||
}
|
||||
|
||||
default_conf.update({
|
||||
'strategy': 'DefaultStrategy',
|
||||
'order_time_in_force': order_time_in_force
|
||||
})
|
||||
resolver = StrategyResolver(default_conf)
|
||||
|
||||
assert resolver.strategy.order_time_in_force
|
||||
for method in ['buy', 'sell']:
|
||||
assert resolver.strategy.order_time_in_force[method] == order_time_in_force[method]
|
||||
|
||||
assert log_has("Override strategy 'order_time_in_force' with value in config file:"
|
||||
" {'buy': 'fok', 'sell': 'gtc'}.", caplog)
|
||||
|
||||
default_conf.update({
|
||||
'strategy': 'DefaultStrategy',
|
||||
'order_time_in_force': {'buy': 'fok'}
|
||||
})
|
||||
# Raise error for invalid configuration
|
||||
with pytest.raises(ImportError,
|
||||
match=r"Impossible to load Strategy 'DefaultStrategy'. "
|
||||
r"Order-time-in-force mapping is incomplete."):
|
||||
StrategyResolver(default_conf)
|
||||
|
||||
|
||||
def test_strategy_override_use_sell_signal(caplog, default_conf):
|
||||
caplog.set_level(logging.INFO)
|
||||
default_conf.update({
|
||||
'strategy': 'DefaultStrategy',
|
||||
})
|
||||
resolver = StrategyResolver(default_conf)
|
||||
assert not resolver.strategy.use_sell_signal
|
||||
assert isinstance(resolver.strategy.use_sell_signal, bool)
|
||||
# must be inserted to configuration
|
||||
assert 'use_sell_signal' in default_conf['experimental']
|
||||
assert not default_conf['experimental']['use_sell_signal']
|
||||
|
||||
default_conf.update({
|
||||
'strategy': 'DefaultStrategy',
|
||||
'experimental': {
|
||||
'use_sell_signal': True,
|
||||
},
|
||||
})
|
||||
resolver = StrategyResolver(default_conf)
|
||||
|
||||
assert resolver.strategy.use_sell_signal
|
||||
assert isinstance(resolver.strategy.use_sell_signal, bool)
|
||||
assert log_has("Override strategy 'use_sell_signal' with value in config file: True.", caplog)
|
||||
|
||||
|
||||
def test_strategy_override_use_sell_profit_only(caplog, default_conf):
|
||||
caplog.set_level(logging.INFO)
|
||||
default_conf.update({
|
||||
'strategy': 'DefaultStrategy',
|
||||
})
|
||||
resolver = StrategyResolver(default_conf)
|
||||
assert not resolver.strategy.sell_profit_only
|
||||
assert isinstance(resolver.strategy.sell_profit_only, bool)
|
||||
# must be inserted to configuration
|
||||
assert 'sell_profit_only' in default_conf['experimental']
|
||||
assert not default_conf['experimental']['sell_profit_only']
|
||||
|
||||
default_conf.update({
|
||||
'strategy': 'DefaultStrategy',
|
||||
'experimental': {
|
||||
'sell_profit_only': True,
|
||||
},
|
||||
})
|
||||
resolver = StrategyResolver(default_conf)
|
||||
|
||||
assert resolver.strategy.sell_profit_only
|
||||
assert isinstance(resolver.strategy.sell_profit_only, bool)
|
||||
assert log_has("Override strategy 'sell_profit_only' with value in config file: True.", caplog)
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:deprecated")
|
||||
def test_deprecate_populate_indicators(result, default_conf):
|
||||
default_location = path.join(path.dirname(path.realpath(__file__)))
|
||||
default_conf.update({'strategy': 'TestStrategyLegacy',
|
||||
'strategy_path': default_location})
|
||||
resolver = StrategyResolver(default_conf)
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
# Cause all warnings to always be triggered.
|
||||
warnings.simplefilter("always")
|
||||
indicators = resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
|
||||
assert len(w) == 1
|
||||
assert issubclass(w[-1].category, DeprecationWarning)
|
||||
assert "deprecated - check out the Sample strategy to see the current function headers!" \
|
||||
in str(w[-1].message)
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
# Cause all warnings to always be triggered.
|
||||
warnings.simplefilter("always")
|
||||
resolver.strategy.advise_buy(indicators, {'pair': 'ETH/BTC'})
|
||||
assert len(w) == 1
|
||||
assert issubclass(w[-1].category, DeprecationWarning)
|
||||
assert "deprecated - check out the Sample strategy to see the current function headers!" \
|
||||
in str(w[-1].message)
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
# Cause all warnings to always be triggered.
|
||||
warnings.simplefilter("always")
|
||||
resolver.strategy.advise_sell(indicators, {'pair': 'ETH_BTC'})
|
||||
assert len(w) == 1
|
||||
assert issubclass(w[-1].category, DeprecationWarning)
|
||||
assert "deprecated - check out the Sample strategy to see the current function headers!" \
|
||||
in str(w[-1].message)
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:deprecated")
|
||||
def test_call_deprecated_function(result, monkeypatch, default_conf):
|
||||
default_location = path.join(path.dirname(path.realpath(__file__)))
|
||||
default_conf.update({'strategy': 'TestStrategyLegacy',
|
||||
'strategy_path': default_location})
|
||||
resolver = StrategyResolver(default_conf)
|
||||
metadata = {'pair': 'ETH/BTC'}
|
||||
|
||||
# Make sure we are using a legacy function
|
||||
assert resolver.strategy._populate_fun_len == 2
|
||||
assert resolver.strategy._buy_fun_len == 2
|
||||
assert resolver.strategy._sell_fun_len == 2
|
||||
assert resolver.strategy.INTERFACE_VERSION == 1
|
||||
|
||||
indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata)
|
||||
assert isinstance(indicator_df, DataFrame)
|
||||
assert 'adx' in indicator_df.columns
|
||||
|
||||
buydf = resolver.strategy.advise_buy(result, metadata=metadata)
|
||||
assert isinstance(buydf, DataFrame)
|
||||
assert 'buy' in buydf.columns
|
||||
|
||||
selldf = resolver.strategy.advise_sell(result, metadata=metadata)
|
||||
assert isinstance(selldf, DataFrame)
|
||||
assert 'sell' in selldf
|
||||
|
||||
|
||||
def test_strategy_interface_versioning(result, monkeypatch, default_conf):
|
||||
default_conf.update({'strategy': 'DefaultStrategy'})
|
||||
resolver = StrategyResolver(default_conf)
|
||||
metadata = {'pair': 'ETH/BTC'}
|
||||
|
||||
# Make sure we are using a legacy function
|
||||
assert resolver.strategy._populate_fun_len == 3
|
||||
assert resolver.strategy._buy_fun_len == 3
|
||||
assert resolver.strategy._sell_fun_len == 3
|
||||
assert resolver.strategy.INTERFACE_VERSION == 2
|
||||
|
||||
indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata)
|
||||
assert isinstance(indicator_df, DataFrame)
|
||||
assert 'adx' in indicator_df.columns
|
||||
|
||||
buydf = resolver.strategy.advise_buy(result, metadata=metadata)
|
||||
assert isinstance(buydf, DataFrame)
|
||||
assert 'buy' in buydf.columns
|
||||
|
||||
selldf = resolver.strategy.advise_sell(result, metadata=metadata)
|
||||
assert isinstance(selldf, DataFrame)
|
||||
assert 'sell' in selldf
|
196
tests/test_arguments.py
Normal file
196
tests/test_arguments.py
Normal file
@@ -0,0 +1,196 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
import argparse
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.configuration import Arguments
|
||||
from freqtrade.configuration.cli_options import check_int_positive
|
||||
|
||||
|
||||
# Parse common command-line-arguments. Used for all tools
|
||||
def test_parse_args_none() -> None:
|
||||
arguments = Arguments([])
|
||||
assert isinstance(arguments, Arguments)
|
||||
x = arguments.get_parsed_arg()
|
||||
assert isinstance(x, argparse.Namespace)
|
||||
assert isinstance(arguments.parser, argparse.ArgumentParser)
|
||||
|
||||
|
||||
def test_parse_args_defaults() -> None:
|
||||
args = Arguments([]).get_parsed_arg()
|
||||
assert args.config == ['config.json']
|
||||
assert args.strategy_path is None
|
||||
assert args.datadir is None
|
||||
assert args.verbosity == 0
|
||||
|
||||
|
||||
def test_parse_args_config() -> None:
|
||||
args = Arguments(['-c', '/dev/null']).get_parsed_arg()
|
||||
assert args.config == ['/dev/null']
|
||||
|
||||
args = Arguments(['--config', '/dev/null']).get_parsed_arg()
|
||||
assert args.config == ['/dev/null']
|
||||
|
||||
args = Arguments(['--config', '/dev/null',
|
||||
'--config', '/dev/zero'],).get_parsed_arg()
|
||||
assert args.config == ['/dev/null', '/dev/zero']
|
||||
|
||||
|
||||
def test_parse_args_db_url() -> None:
|
||||
args = Arguments(['--db-url', 'sqlite:///test.sqlite']).get_parsed_arg()
|
||||
assert args.db_url == 'sqlite:///test.sqlite'
|
||||
|
||||
|
||||
def test_parse_args_verbose() -> None:
|
||||
args = Arguments(['-v']).get_parsed_arg()
|
||||
assert args.verbosity == 1
|
||||
|
||||
args = Arguments(['--verbose']).get_parsed_arg()
|
||||
assert args.verbosity == 1
|
||||
|
||||
|
||||
def test_common_scripts_options() -> None:
|
||||
args = Arguments(['download-data', '-p', 'ETH/BTC', 'XRP/BTC']).get_parsed_arg()
|
||||
|
||||
assert args.pairs == ['ETH/BTC', 'XRP/BTC']
|
||||
assert hasattr(args, "func")
|
||||
|
||||
|
||||
def test_parse_args_version() -> None:
|
||||
with pytest.raises(SystemExit, match=r'0'):
|
||||
Arguments(['--version']).get_parsed_arg()
|
||||
|
||||
|
||||
def test_parse_args_invalid() -> None:
|
||||
with pytest.raises(SystemExit, match=r'2'):
|
||||
Arguments(['-c']).get_parsed_arg()
|
||||
|
||||
|
||||
def test_parse_args_strategy() -> None:
|
||||
args = Arguments(['--strategy', 'SomeStrategy']).get_parsed_arg()
|
||||
assert args.strategy == 'SomeStrategy'
|
||||
|
||||
|
||||
def test_parse_args_strategy_invalid() -> None:
|
||||
with pytest.raises(SystemExit, match=r'2'):
|
||||
Arguments(['--strategy']).get_parsed_arg()
|
||||
|
||||
|
||||
def test_parse_args_strategy_path() -> None:
|
||||
args = Arguments(['--strategy-path', '/some/path']).get_parsed_arg()
|
||||
assert args.strategy_path == '/some/path'
|
||||
|
||||
|
||||
def test_parse_args_strategy_path_invalid() -> None:
|
||||
with pytest.raises(SystemExit, match=r'2'):
|
||||
Arguments(['--strategy-path']).get_parsed_arg()
|
||||
|
||||
|
||||
def test_parse_args_backtesting_invalid() -> None:
|
||||
with pytest.raises(SystemExit, match=r'2'):
|
||||
Arguments(['backtesting --ticker-interval']).get_parsed_arg()
|
||||
|
||||
with pytest.raises(SystemExit, match=r'2'):
|
||||
Arguments(['backtesting --ticker-interval', 'abc']).get_parsed_arg()
|
||||
|
||||
|
||||
def test_parse_args_backtesting_custom() -> None:
|
||||
args = [
|
||||
'-c', 'test_conf.json',
|
||||
'backtesting',
|
||||
'--ticker-interval', '1m',
|
||||
'--refresh-pairs-cached',
|
||||
'--strategy-list',
|
||||
'DefaultStrategy',
|
||||
'SampleStrategy'
|
||||
]
|
||||
call_args = Arguments(args).get_parsed_arg()
|
||||
assert call_args.config == ['test_conf.json']
|
||||
assert call_args.verbosity == 0
|
||||
assert call_args.subparser == 'backtesting'
|
||||
assert call_args.func is not None
|
||||
assert call_args.ticker_interval == '1m'
|
||||
assert call_args.refresh_pairs is True
|
||||
assert type(call_args.strategy_list) is list
|
||||
assert len(call_args.strategy_list) == 2
|
||||
|
||||
|
||||
def test_parse_args_hyperopt_custom() -> None:
|
||||
args = [
|
||||
'-c', 'test_conf.json',
|
||||
'hyperopt',
|
||||
'--epochs', '20',
|
||||
'--spaces', 'buy'
|
||||
]
|
||||
call_args = Arguments(args).get_parsed_arg()
|
||||
assert call_args.config == ['test_conf.json']
|
||||
assert call_args.epochs == 20
|
||||
assert call_args.verbosity == 0
|
||||
assert call_args.subparser == 'hyperopt'
|
||||
assert call_args.spaces == ['buy']
|
||||
assert call_args.func is not None
|
||||
|
||||
|
||||
def test_download_data_options() -> None:
|
||||
args = [
|
||||
'--datadir', 'datadir/directory',
|
||||
'download-data',
|
||||
'--pairs-file', 'file_with_pairs',
|
||||
'--days', '30',
|
||||
'--exchange', 'binance'
|
||||
]
|
||||
args = Arguments(args).get_parsed_arg()
|
||||
|
||||
assert args.pairs_file == 'file_with_pairs'
|
||||
assert args.datadir == 'datadir/directory'
|
||||
assert args.days == 30
|
||||
assert args.exchange == 'binance'
|
||||
|
||||
|
||||
def test_plot_dataframe_options() -> None:
|
||||
args = [
|
||||
'-c', 'config.json.example',
|
||||
'plot-dataframe',
|
||||
'--indicators1', 'sma10', 'sma100',
|
||||
'--indicators2', 'macd', 'fastd', 'fastk',
|
||||
'--plot-limit', '30',
|
||||
'-p', 'UNITTEST/BTC',
|
||||
]
|
||||
pargs = Arguments(args).get_parsed_arg()
|
||||
|
||||
assert pargs.indicators1 == ["sma10", "sma100"]
|
||||
assert pargs.indicators2 == ["macd", "fastd", "fastk"]
|
||||
assert pargs.plot_limit == 30
|
||||
assert pargs.pairs == ["UNITTEST/BTC"]
|
||||
|
||||
|
||||
def test_plot_profit_options() -> None:
|
||||
args = [
|
||||
'plot-profit',
|
||||
'-p', 'UNITTEST/BTC',
|
||||
'--trade-source', 'DB',
|
||||
"--db-url", "sqlite:///whatever.sqlite",
|
||||
]
|
||||
pargs = Arguments(args).get_parsed_arg()
|
||||
|
||||
assert pargs.trade_source == "DB"
|
||||
assert pargs.pairs == ["UNITTEST/BTC"]
|
||||
assert pargs.db_url == "sqlite:///whatever.sqlite"
|
||||
|
||||
|
||||
def test_check_int_positive() -> None:
|
||||
assert check_int_positive("3") == 3
|
||||
assert check_int_positive("1") == 1
|
||||
assert check_int_positive("100") == 100
|
||||
|
||||
with pytest.raises(argparse.ArgumentTypeError):
|
||||
check_int_positive("-2")
|
||||
|
||||
with pytest.raises(argparse.ArgumentTypeError):
|
||||
check_int_positive("0")
|
||||
|
||||
with pytest.raises(argparse.ArgumentTypeError):
|
||||
check_int_positive("3.5")
|
||||
|
||||
with pytest.raises(argparse.ArgumentTypeError):
|
||||
check_int_positive("DeadBeef")
|
882
tests/test_configuration.py
Normal file
882
tests/test_configuration.py
Normal file
@@ -0,0 +1,882 @@
|
||||
# pragma pylint: disable=missing-docstring, protected-access, invalid-name
|
||||
import json
|
||||
import logging
|
||||
import warnings
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from jsonschema import Draft4Validator, ValidationError, validate
|
||||
|
||||
from freqtrade import OperationalException, constants
|
||||
from freqtrade.configuration import Arguments, Configuration, validate_config_consistency
|
||||
from freqtrade.configuration.check_exchange import check_exchange
|
||||
from freqtrade.configuration.config_validation import validate_config_schema
|
||||
from freqtrade.configuration.directory_operations import (create_datadir,
|
||||
create_userdata_dir)
|
||||
from freqtrade.configuration.load_config import load_config_file
|
||||
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
|
||||
from freqtrade.loggers import _set_loggers
|
||||
from freqtrade.state import RunMode
|
||||
from freqtrade.tests.conftest import (log_has, log_has_re,
|
||||
patched_configuration_load_config_file)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def all_conf():
|
||||
config_file = Path(__file__).parents[2] / "config_full.json.example"
|
||||
print(config_file)
|
||||
conf = load_config_file(str(config_file))
|
||||
return conf
|
||||
|
||||
|
||||
def test_load_config_invalid_pair(default_conf) -> None:
|
||||
default_conf['exchange']['pair_whitelist'].append('ETH-BTC')
|
||||
|
||||
with pytest.raises(ValidationError, match=r'.*does not match.*'):
|
||||
validate_config_schema(default_conf)
|
||||
|
||||
|
||||
def test_load_config_missing_attributes(default_conf) -> None:
|
||||
default_conf.pop('exchange')
|
||||
|
||||
with pytest.raises(ValidationError, match=r".*'exchange' is a required property.*"):
|
||||
validate_config_schema(default_conf)
|
||||
|
||||
|
||||
def test_load_config_incorrect_stake_amount(default_conf) -> None:
|
||||
default_conf['stake_amount'] = 'fake'
|
||||
|
||||
with pytest.raises(ValidationError, match=r".*'fake' does not match 'unlimited'.*"):
|
||||
validate_config_schema(default_conf)
|
||||
|
||||
|
||||
def test_load_config_file(default_conf, mocker, caplog) -> None:
|
||||
del default_conf['user_data_dir']
|
||||
file_mock = mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
validated_conf = load_config_file('somefile')
|
||||
assert file_mock.call_count == 1
|
||||
assert validated_conf.items() >= default_conf.items()
|
||||
|
||||
|
||||
def test__args_to_config(caplog):
|
||||
|
||||
arg_list = ['--strategy-path', 'TestTest']
|
||||
args = Arguments(arg_list).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
config = {}
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
# No warnings ...
|
||||
configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef")
|
||||
assert len(w) == 0
|
||||
assert log_has("DeadBeef", caplog)
|
||||
assert config['strategy_path'] == "TestTest"
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = {}
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
# Deprecation warnings!
|
||||
configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef",
|
||||
deprecated_msg="Going away soon!")
|
||||
assert len(w) == 1
|
||||
assert issubclass(w[-1].category, DeprecationWarning)
|
||||
assert "DEPRECATED: Going away soon!" in str(w[-1].message)
|
||||
assert log_has("DeadBeef", caplog)
|
||||
assert config['strategy_path'] == "TestTest"
|
||||
|
||||
|
||||
def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
|
||||
default_conf['max_open_trades'] = 0
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = Arguments([]).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
assert validated_conf['max_open_trades'] == 0
|
||||
assert 'internals' in validated_conf
|
||||
assert log_has('Validating configuration ...', caplog)
|
||||
|
||||
|
||||
def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None:
|
||||
conf1 = deepcopy(default_conf)
|
||||
conf2 = deepcopy(default_conf)
|
||||
del conf1['exchange']['key']
|
||||
del conf1['exchange']['secret']
|
||||
del conf2['exchange']['name']
|
||||
conf2['exchange']['pair_whitelist'] += ['NANO/BTC']
|
||||
|
||||
config_files = [conf1, conf2]
|
||||
|
||||
configsmock = MagicMock(side_effect=config_files)
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.configuration.load_config_file',
|
||||
configsmock
|
||||
)
|
||||
|
||||
arg_list = ['-c', 'test_conf.json', '--config', 'test2_conf.json', ]
|
||||
args = Arguments(arg_list).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
exchange_conf = default_conf['exchange']
|
||||
assert validated_conf['exchange']['name'] == exchange_conf['name']
|
||||
assert validated_conf['exchange']['key'] == exchange_conf['key']
|
||||
assert validated_conf['exchange']['secret'] == exchange_conf['secret']
|
||||
assert validated_conf['exchange']['pair_whitelist'] != conf1['exchange']['pair_whitelist']
|
||||
assert validated_conf['exchange']['pair_whitelist'] == conf2['exchange']['pair_whitelist']
|
||||
|
||||
assert 'internals' in validated_conf
|
||||
assert log_has('Validating configuration ...', caplog)
|
||||
|
||||
|
||||
def test_from_config(default_conf, mocker, caplog) -> None:
|
||||
conf1 = deepcopy(default_conf)
|
||||
conf2 = deepcopy(default_conf)
|
||||
del conf1['exchange']['key']
|
||||
del conf1['exchange']['secret']
|
||||
del conf2['exchange']['name']
|
||||
conf2['exchange']['pair_whitelist'] += ['NANO/BTC']
|
||||
conf2['fiat_display_currency'] = "EUR"
|
||||
config_files = [conf1, conf2]
|
||||
|
||||
configsmock = MagicMock(side_effect=config_files)
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.configuration.load_config_file',
|
||||
configsmock
|
||||
)
|
||||
|
||||
validated_conf = Configuration.from_files(['test_conf.json', 'test2_conf.json'])
|
||||
|
||||
exchange_conf = default_conf['exchange']
|
||||
assert validated_conf['exchange']['name'] == exchange_conf['name']
|
||||
assert validated_conf['exchange']['key'] == exchange_conf['key']
|
||||
assert validated_conf['exchange']['secret'] == exchange_conf['secret']
|
||||
assert validated_conf['exchange']['pair_whitelist'] != conf1['exchange']['pair_whitelist']
|
||||
assert validated_conf['exchange']['pair_whitelist'] == conf2['exchange']['pair_whitelist']
|
||||
assert validated_conf['fiat_display_currency'] == "EUR"
|
||||
assert 'internals' in validated_conf
|
||||
assert log_has('Validating configuration ...', caplog)
|
||||
|
||||
|
||||
def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> None:
|
||||
default_conf['max_open_trades'] = -1
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = Arguments([]).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
assert validated_conf['max_open_trades'] > 999999999
|
||||
assert validated_conf['max_open_trades'] == float('inf')
|
||||
assert log_has('Validating configuration ...', caplog)
|
||||
assert "runmode" in validated_conf
|
||||
assert validated_conf['runmode'] == RunMode.DRY_RUN
|
||||
|
||||
|
||||
def test_load_config_file_exception(mocker) -> None:
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.configuration.open',
|
||||
MagicMock(side_effect=FileNotFoundError('File not found'))
|
||||
)
|
||||
|
||||
with pytest.raises(OperationalException, match=r'.*Config file "somefile" not found!*'):
|
||||
load_config_file('somefile')
|
||||
|
||||
|
||||
def test_load_config(default_conf, mocker) -> None:
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = Arguments([]).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
assert validated_conf.get('strategy') == 'DefaultStrategy'
|
||||
assert validated_conf.get('strategy_path') is None
|
||||
assert 'edge' not in validated_conf
|
||||
|
||||
|
||||
def test_load_config_with_params(default_conf, mocker) -> None:
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
arglist = [
|
||||
'--strategy', 'TestStrategy',
|
||||
'--strategy-path', '/some/path',
|
||||
'--db-url', 'sqlite:///someurl',
|
||||
]
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
assert validated_conf.get('strategy') == 'TestStrategy'
|
||||
assert validated_conf.get('strategy_path') == '/some/path'
|
||||
assert validated_conf.get('db_url') == 'sqlite:///someurl'
|
||||
|
||||
# Test conf provided db_url prod
|
||||
conf = default_conf.copy()
|
||||
conf["dry_run"] = False
|
||||
conf["db_url"] = "sqlite:///path/to/db.sqlite"
|
||||
patched_configuration_load_config_file(mocker, conf)
|
||||
|
||||
arglist = [
|
||||
'--strategy', 'TestStrategy',
|
||||
'--strategy-path', '/some/path'
|
||||
]
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
assert validated_conf.get('db_url') == "sqlite:///path/to/db.sqlite"
|
||||
|
||||
# Test conf provided db_url dry_run
|
||||
conf = default_conf.copy()
|
||||
conf["dry_run"] = True
|
||||
conf["db_url"] = "sqlite:///path/to/db.sqlite"
|
||||
patched_configuration_load_config_file(mocker, conf)
|
||||
|
||||
arglist = [
|
||||
'--strategy', 'TestStrategy',
|
||||
'--strategy-path', '/some/path'
|
||||
]
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
assert validated_conf.get('db_url') == "sqlite:///path/to/db.sqlite"
|
||||
|
||||
# Test args provided db_url prod
|
||||
conf = default_conf.copy()
|
||||
conf["dry_run"] = False
|
||||
del conf["db_url"]
|
||||
patched_configuration_load_config_file(mocker, conf)
|
||||
|
||||
arglist = [
|
||||
'--strategy', 'TestStrategy',
|
||||
'--strategy-path', '/some/path'
|
||||
]
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
assert validated_conf.get('db_url') == DEFAULT_DB_PROD_URL
|
||||
assert "runmode" in validated_conf
|
||||
assert validated_conf['runmode'] == RunMode.LIVE
|
||||
|
||||
# Test args provided db_url dry_run
|
||||
conf = default_conf.copy()
|
||||
conf["dry_run"] = True
|
||||
conf["db_url"] = DEFAULT_DB_PROD_URL
|
||||
patched_configuration_load_config_file(mocker, conf)
|
||||
|
||||
arglist = [
|
||||
'--strategy', 'TestStrategy',
|
||||
'--strategy-path', '/some/path'
|
||||
]
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
assert validated_conf.get('db_url') == DEFAULT_DB_DRYRUN_URL
|
||||
|
||||
|
||||
def test_load_custom_strategy(default_conf, mocker) -> None:
|
||||
default_conf.update({
|
||||
'strategy': 'CustomStrategy',
|
||||
'strategy_path': '/tmp/strategies',
|
||||
})
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = Arguments([]).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
assert validated_conf.get('strategy') == 'CustomStrategy'
|
||||
assert validated_conf.get('strategy_path') == '/tmp/strategies'
|
||||
|
||||
|
||||
def test_show_info(default_conf, mocker, caplog) -> None:
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
arglist = [
|
||||
'--strategy', 'TestStrategy',
|
||||
'--db-url', 'sqlite:///tmp/testdb',
|
||||
]
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
configuration.get_config()
|
||||
|
||||
assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog)
|
||||
assert log_has('Dry run is enabled', caplog)
|
||||
|
||||
|
||||
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
arglist = [
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'backtesting'
|
||||
]
|
||||
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = configuration.get_config()
|
||||
assert 'max_open_trades' in config
|
||||
assert 'stake_currency' in config
|
||||
assert 'stake_amount' in config
|
||||
assert 'exchange' in config
|
||||
assert 'pair_whitelist' in config['exchange']
|
||||
assert 'datadir' in config
|
||||
assert 'user_data_dir' in config
|
||||
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||
assert 'ticker_interval' in config
|
||||
assert not log_has('Parameter -i/--ticker-interval detected ...', caplog)
|
||||
|
||||
assert 'position_stacking' not in config
|
||||
assert not log_has('Parameter --enable-position-stacking detected ...', caplog)
|
||||
|
||||
assert 'refresh_pairs' not in config
|
||||
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
|
||||
|
||||
assert 'timerange' not in config
|
||||
assert 'export' not in config
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:DEPRECATED")
|
||||
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.configuration.create_datadir',
|
||||
lambda c, x: x
|
||||
)
|
||||
mocker.patch(
|
||||
'freqtrade.configuration.configuration.create_userdata_dir',
|
||||
lambda x, *args, **kwargs: Path(x)
|
||||
)
|
||||
arglist = [
|
||||
'--config', 'config.json',
|
||||
'--strategy', 'DefaultStrategy',
|
||||
'--datadir', '/foo/bar',
|
||||
'--userdir', "/tmp/freqtrade",
|
||||
'backtesting',
|
||||
'--ticker-interval', '1m',
|
||||
'--enable-position-stacking',
|
||||
'--disable-max-market-positions',
|
||||
'--refresh-pairs-cached',
|
||||
'--timerange', ':100',
|
||||
'--export', '/bar/foo'
|
||||
]
|
||||
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = configuration.get_config()
|
||||
assert 'max_open_trades' in config
|
||||
assert 'stake_currency' in config
|
||||
assert 'stake_amount' in config
|
||||
assert 'exchange' in config
|
||||
assert 'pair_whitelist' in config['exchange']
|
||||
assert 'datadir' in config
|
||||
assert log_has('Using data directory: {} ...'.format("/foo/bar"), caplog)
|
||||
assert log_has('Using user-data directory: {} ...'.format("/tmp/freqtrade"), caplog)
|
||||
assert 'user_data_dir' in config
|
||||
|
||||
assert 'ticker_interval' in config
|
||||
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||
caplog)
|
||||
|
||||
assert 'position_stacking'in config
|
||||
assert log_has('Parameter --enable-position-stacking detected ...', caplog)
|
||||
|
||||
assert 'use_max_market_positions' in config
|
||||
assert log_has('Parameter --disable-max-market-positions detected ...', caplog)
|
||||
assert log_has('max_open_trades set to unlimited ...', caplog)
|
||||
|
||||
assert 'refresh_pairs'in config
|
||||
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
|
||||
assert 'timerange' in config
|
||||
assert log_has('Parameter --timerange detected: {} ...'.format(config['timerange']), caplog)
|
||||
|
||||
assert 'export' in config
|
||||
assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog)
|
||||
|
||||
|
||||
def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None:
|
||||
"""
|
||||
Test setup_configuration() function
|
||||
"""
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
arglist = [
|
||||
'--config', 'config.json',
|
||||
'backtesting',
|
||||
'--ticker-interval', '1m',
|
||||
'--export', '/bar/foo',
|
||||
'--strategy-list',
|
||||
'DefaultStrategy',
|
||||
'TestStrategy'
|
||||
]
|
||||
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args, RunMode.BACKTEST)
|
||||
config = configuration.get_config()
|
||||
assert config['runmode'] == RunMode.BACKTEST
|
||||
assert 'max_open_trades' in config
|
||||
assert 'stake_currency' in config
|
||||
assert 'stake_amount' in config
|
||||
assert 'exchange' in config
|
||||
assert 'pair_whitelist' in config['exchange']
|
||||
assert 'datadir' in config
|
||||
assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
|
||||
assert 'ticker_interval' in config
|
||||
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
|
||||
caplog)
|
||||
|
||||
assert 'strategy_list' in config
|
||||
assert log_has('Using strategy list of 2 Strategies', caplog)
|
||||
|
||||
assert 'position_stacking' not in config
|
||||
|
||||
assert 'use_max_market_positions' not in config
|
||||
|
||||
assert 'timerange' not in config
|
||||
|
||||
assert 'export' in config
|
||||
assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog)
|
||||
|
||||
|
||||
def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
arglist = [
|
||||
'hyperopt',
|
||||
'--epochs', '10',
|
||||
'--spaces', 'all',
|
||||
]
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args, RunMode.HYPEROPT)
|
||||
config = configuration.get_config()
|
||||
|
||||
assert 'epochs' in config
|
||||
assert int(config['epochs']) == 10
|
||||
assert log_has('Parameter --epochs detected ... Will run Hyperopt with for 10 epochs ...',
|
||||
caplog)
|
||||
|
||||
assert 'spaces' in config
|
||||
assert config['spaces'] == ['all']
|
||||
assert log_has("Parameter -s/--spaces detected: ['all']", caplog)
|
||||
assert "runmode" in config
|
||||
assert config['runmode'] == RunMode.HYPEROPT
|
||||
|
||||
|
||||
def test_check_exchange(default_conf, caplog) -> None:
|
||||
# Test an officially supported by Freqtrade team exchange
|
||||
default_conf['runmode'] = RunMode.DRY_RUN
|
||||
default_conf.get('exchange').update({'name': 'BITTREX'})
|
||||
assert check_exchange(default_conf)
|
||||
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
|
||||
caplog)
|
||||
caplog.clear()
|
||||
|
||||
# Test an officially supported by Freqtrade team exchange
|
||||
default_conf.get('exchange').update({'name': 'binance'})
|
||||
assert check_exchange(default_conf)
|
||||
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
|
||||
caplog)
|
||||
caplog.clear()
|
||||
|
||||
# Test an available exchange, supported by ccxt
|
||||
default_conf.get('exchange').update({'name': 'huobipro'})
|
||||
assert check_exchange(default_conf)
|
||||
assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported "
|
||||
r"by the Freqtrade development team\. .*", caplog)
|
||||
caplog.clear()
|
||||
|
||||
# Test a 'bad' exchange, which known to have serious problems
|
||||
default_conf.get('exchange').update({'name': 'bitmex'})
|
||||
with pytest.raises(OperationalException,
|
||||
match=r"Exchange .* is known to not work with the bot yet.*"):
|
||||
check_exchange(default_conf)
|
||||
caplog.clear()
|
||||
|
||||
# Test a 'bad' exchange with check_for_bad=False
|
||||
default_conf.get('exchange').update({'name': 'bitmex'})
|
||||
assert check_exchange(default_conf, False)
|
||||
assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported "
|
||||
r"by the Freqtrade development team\. .*", caplog)
|
||||
caplog.clear()
|
||||
|
||||
# Test an invalid exchange
|
||||
default_conf.get('exchange').update({'name': 'unknown_exchange'})
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match=r'.*Exchange "unknown_exchange" is not supported by ccxt '
|
||||
r'and therefore not available for the bot.*'
|
||||
):
|
||||
check_exchange(default_conf)
|
||||
|
||||
# Test no exchange...
|
||||
default_conf.get('exchange').update({'name': ''})
|
||||
default_conf['runmode'] = RunMode.PLOT
|
||||
assert check_exchange(default_conf)
|
||||
|
||||
|
||||
def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
# Prevent setting loggers
|
||||
mocker.patch('freqtrade.loggers._set_loggers', MagicMock)
|
||||
arglist = ['-vvv']
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
assert validated_conf.get('verbosity') == 3
|
||||
assert log_has('Verbosity set to 3', caplog)
|
||||
|
||||
|
||||
def test_set_loggers() -> None:
|
||||
# Reset Logging to Debug, otherwise this fails randomly as it's set globally
|
||||
logging.getLogger('requests').setLevel(logging.DEBUG)
|
||||
logging.getLogger("urllib3").setLevel(logging.DEBUG)
|
||||
logging.getLogger('ccxt.base.exchange').setLevel(logging.DEBUG)
|
||||
logging.getLogger('telegram').setLevel(logging.DEBUG)
|
||||
|
||||
previous_value1 = logging.getLogger('requests').level
|
||||
previous_value2 = logging.getLogger('ccxt.base.exchange').level
|
||||
previous_value3 = logging.getLogger('telegram').level
|
||||
|
||||
_set_loggers()
|
||||
|
||||
value1 = logging.getLogger('requests').level
|
||||
assert previous_value1 is not value1
|
||||
assert value1 is logging.INFO
|
||||
|
||||
value2 = logging.getLogger('ccxt.base.exchange').level
|
||||
assert previous_value2 is not value2
|
||||
assert value2 is logging.INFO
|
||||
|
||||
value3 = logging.getLogger('telegram').level
|
||||
assert previous_value3 is not value3
|
||||
assert value3 is logging.INFO
|
||||
|
||||
_set_loggers(verbosity=2)
|
||||
|
||||
assert logging.getLogger('requests').level is logging.DEBUG
|
||||
assert logging.getLogger('ccxt.base.exchange').level is logging.INFO
|
||||
assert logging.getLogger('telegram').level is logging.INFO
|
||||
|
||||
_set_loggers(verbosity=3)
|
||||
|
||||
assert logging.getLogger('requests').level is logging.DEBUG
|
||||
assert logging.getLogger('ccxt.base.exchange').level is logging.DEBUG
|
||||
assert logging.getLogger('telegram').level is logging.INFO
|
||||
|
||||
|
||||
def test_set_logfile(default_conf, mocker):
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
arglist = [
|
||||
'--logfile', 'test_file.log',
|
||||
]
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
assert validated_conf['logfile'] == "test_file.log"
|
||||
f = Path("test_file.log")
|
||||
assert f.is_file()
|
||||
f.unlink()
|
||||
|
||||
|
||||
def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None:
|
||||
default_conf['forcebuy_enable'] = True
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
|
||||
args = Arguments([]).get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
assert validated_conf.get('forcebuy_enable')
|
||||
assert log_has('`forcebuy` RPC message enabled.', caplog)
|
||||
|
||||
|
||||
def test_validate_default_conf(default_conf) -> None:
|
||||
validate(default_conf, constants.CONF_SCHEMA, Draft4Validator)
|
||||
|
||||
|
||||
def test_create_datadir(mocker, default_conf, caplog) -> None:
|
||||
mocker.patch.object(Path, "is_dir", MagicMock(return_value=False))
|
||||
md = mocker.patch.object(Path, 'mkdir', MagicMock())
|
||||
|
||||
create_datadir(default_conf, '/foo/bar')
|
||||
assert md.call_args[1]['parents'] is True
|
||||
assert log_has('Created data directory: /foo/bar', caplog)
|
||||
|
||||
|
||||
def test_create_userdata_dir(mocker, default_conf, caplog) -> None:
|
||||
mocker.patch.object(Path, "is_dir", MagicMock(return_value=False))
|
||||
md = mocker.patch.object(Path, 'mkdir', MagicMock())
|
||||
|
||||
x = create_userdata_dir('/tmp/bar', create_dir=True)
|
||||
assert md.call_count == 7
|
||||
assert md.call_args[1]['parents'] is False
|
||||
assert log_has('Created user-data directory: /tmp/bar', caplog)
|
||||
assert isinstance(x, Path)
|
||||
assert str(x) == "/tmp/bar"
|
||||
|
||||
|
||||
def test_create_userdata_dir_exists(mocker, default_conf, caplog) -> None:
|
||||
mocker.patch.object(Path, "is_dir", MagicMock(return_value=True))
|
||||
md = mocker.patch.object(Path, 'mkdir', MagicMock())
|
||||
|
||||
create_userdata_dir('/tmp/bar')
|
||||
assert md.call_count == 0
|
||||
|
||||
|
||||
def test_create_userdata_dir_exists_exception(mocker, default_conf, caplog) -> None:
|
||||
mocker.patch.object(Path, "is_dir", MagicMock(return_value=False))
|
||||
md = mocker.patch.object(Path, 'mkdir', MagicMock())
|
||||
|
||||
with pytest.raises(OperationalException, match=r'Directory `/tmp/bar` does not exist.*'):
|
||||
create_userdata_dir('/tmp/bar', create_dir=False)
|
||||
assert md.call_count == 0
|
||||
|
||||
|
||||
def test_validate_tsl(default_conf):
|
||||
default_conf['stoploss'] = 0.0
|
||||
with pytest.raises(OperationalException, match='The config stoploss needs to be different '
|
||||
'from 0 to avoid problems with sell orders.'):
|
||||
validate_config_consistency(default_conf)
|
||||
default_conf['stoploss'] = -0.10
|
||||
|
||||
default_conf['trailing_stop'] = True
|
||||
default_conf['trailing_stop_positive'] = 0
|
||||
default_conf['trailing_stop_positive_offset'] = 0
|
||||
|
||||
default_conf['trailing_only_offset_is_reached'] = True
|
||||
with pytest.raises(OperationalException,
|
||||
match=r'The config trailing_only_offset_is_reached needs '
|
||||
'trailing_stop_positive_offset to be more than 0 in your config.'):
|
||||
validate_config_consistency(default_conf)
|
||||
|
||||
default_conf['trailing_stop_positive_offset'] = 0.01
|
||||
default_conf['trailing_stop_positive'] = 0.015
|
||||
with pytest.raises(OperationalException,
|
||||
match=r'The config trailing_stop_positive_offset needs '
|
||||
'to be greater than trailing_stop_positive in your config.'):
|
||||
validate_config_consistency(default_conf)
|
||||
|
||||
default_conf['trailing_stop_positive'] = 0.01
|
||||
default_conf['trailing_stop_positive_offset'] = 0.015
|
||||
validate_config_consistency(default_conf)
|
||||
|
||||
# 0 trailing stop positive - results in "Order would trigger immediately"
|
||||
default_conf['trailing_stop_positive'] = 0
|
||||
default_conf['trailing_stop_positive_offset'] = 0.02
|
||||
default_conf['trailing_only_offset_is_reached'] = False
|
||||
with pytest.raises(OperationalException,
|
||||
match='The config trailing_stop_positive needs to be different from 0 '
|
||||
'to avoid problems with sell orders'):
|
||||
validate_config_consistency(default_conf)
|
||||
|
||||
|
||||
def test_validate_edge(edge_conf):
|
||||
edge_conf.update({"pairlist": {
|
||||
"method": "VolumePairList",
|
||||
}})
|
||||
|
||||
with pytest.raises(OperationalException,
|
||||
match="Edge and VolumePairList are incompatible, "
|
||||
"Edge will override whatever pairs VolumePairlist selects."):
|
||||
validate_config_consistency(edge_conf)
|
||||
|
||||
edge_conf.update({"pairlist": {
|
||||
"method": "StaticPairList",
|
||||
}})
|
||||
validate_config_consistency(edge_conf)
|
||||
|
||||
|
||||
def test_load_config_test_comments() -> None:
|
||||
"""
|
||||
Load config with comments
|
||||
"""
|
||||
config_file = Path(__file__).parents[0] / "config_test_comments.json"
|
||||
print(config_file)
|
||||
conf = load_config_file(str(config_file))
|
||||
|
||||
assert conf
|
||||
|
||||
|
||||
def test_load_config_default_exchange(all_conf) -> None:
|
||||
"""
|
||||
config['exchange'] subtree has required options in it
|
||||
so it cannot be omitted in the config
|
||||
"""
|
||||
del all_conf['exchange']
|
||||
|
||||
assert 'exchange' not in all_conf
|
||||
|
||||
with pytest.raises(ValidationError,
|
||||
match=r"'exchange' is a required property"):
|
||||
validate_config_schema(all_conf)
|
||||
|
||||
|
||||
def test_load_config_default_exchange_name(all_conf) -> None:
|
||||
"""
|
||||
config['exchange']['name'] option is required
|
||||
so it cannot be omitted in the config
|
||||
"""
|
||||
del all_conf['exchange']['name']
|
||||
|
||||
assert 'name' not in all_conf['exchange']
|
||||
|
||||
with pytest.raises(ValidationError,
|
||||
match=r"'name' is a required property"):
|
||||
validate_config_schema(all_conf)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("keys", [("exchange", "sandbox", False),
|
||||
("exchange", "key", ""),
|
||||
("exchange", "secret", ""),
|
||||
("exchange", "password", ""),
|
||||
])
|
||||
def test_load_config_default_subkeys(all_conf, keys) -> None:
|
||||
"""
|
||||
Test for parameters with default values in sub-paths
|
||||
so they can be omitted in the config and the default value
|
||||
should is added to the config.
|
||||
"""
|
||||
# Get first level key
|
||||
key = keys[0]
|
||||
# get second level key
|
||||
subkey = keys[1]
|
||||
|
||||
del all_conf[key][subkey]
|
||||
|
||||
assert subkey not in all_conf[key]
|
||||
|
||||
validate_config_schema(all_conf)
|
||||
assert subkey in all_conf[key]
|
||||
assert all_conf[key][subkey] == keys[2]
|
||||
|
||||
|
||||
def test_pairlist_resolving():
|
||||
arglist = [
|
||||
'download-data',
|
||||
'--pairs', 'ETH/BTC', 'XRP/BTC',
|
||||
'--exchange', 'binance'
|
||||
]
|
||||
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = configuration.get_config()
|
||||
|
||||
assert config['pairs'] == ['ETH/BTC', 'XRP/BTC']
|
||||
assert config['exchange']['name'] == 'binance'
|
||||
|
||||
|
||||
def test_pairlist_resolving_with_config(mocker, default_conf):
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
arglist = [
|
||||
'--config', 'config.json',
|
||||
'download-data',
|
||||
]
|
||||
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = configuration.get_config()
|
||||
|
||||
assert config['pairs'] == default_conf['exchange']['pair_whitelist']
|
||||
assert config['exchange']['name'] == default_conf['exchange']['name']
|
||||
|
||||
# Override pairs
|
||||
arglist = [
|
||||
'--config', 'config.json',
|
||||
'download-data',
|
||||
'--pairs', 'ETH/BTC', 'XRP/BTC',
|
||||
]
|
||||
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = configuration.get_config()
|
||||
|
||||
assert config['pairs'] == ['ETH/BTC', 'XRP/BTC']
|
||||
assert config['exchange']['name'] == default_conf['exchange']['name']
|
||||
|
||||
|
||||
def test_pairlist_resolving_with_config_pl(mocker, default_conf):
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
load_mock = mocker.patch("freqtrade.configuration.configuration.json_load",
|
||||
MagicMock(return_value=['XRP/BTC', 'ETH/BTC']))
|
||||
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
||||
mocker.patch.object(Path, "open", MagicMock(return_value=MagicMock()))
|
||||
|
||||
arglist = [
|
||||
'--config', 'config.json',
|
||||
'download-data',
|
||||
'--pairs-file', 'pairs.json',
|
||||
]
|
||||
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = configuration.get_config()
|
||||
|
||||
assert load_mock.call_count == 1
|
||||
assert config['pairs'] == ['ETH/BTC', 'XRP/BTC']
|
||||
assert config['exchange']['name'] == default_conf['exchange']['name']
|
||||
|
||||
|
||||
def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf):
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
mocker.patch("freqtrade.configuration.configuration.json_load",
|
||||
MagicMock(return_value=['XRP/BTC', 'ETH/BTC']))
|
||||
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
|
||||
|
||||
arglist = [
|
||||
'--config', 'config.json',
|
||||
'download-data',
|
||||
'--pairs-file', 'pairs.json',
|
||||
]
|
||||
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
|
||||
with pytest.raises(OperationalException, match=r"No pairs file found with path.*"):
|
||||
configuration = Configuration(args)
|
||||
configuration.get_config()
|
||||
|
||||
|
||||
def test_pairlist_resolving_fallback(mocker):
|
||||
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
||||
mocker.patch.object(Path, "open", MagicMock(return_value=MagicMock()))
|
||||
mocker.patch("freqtrade.configuration.configuration.json_load",
|
||||
MagicMock(return_value=['XRP/BTC', 'ETH/BTC']))
|
||||
arglist = [
|
||||
'download-data',
|
||||
'--exchange', 'binance'
|
||||
]
|
||||
|
||||
args = Arguments(arglist).get_parsed_arg()
|
||||
# Fix flaky tests if config.json exists
|
||||
args.config = None
|
||||
|
||||
configuration = Configuration(args)
|
||||
config = configuration.get_config()
|
||||
|
||||
assert config['pairs'] == ['ETH/BTC', 'XRP/BTC']
|
||||
assert config['exchange']['name'] == 'binance'
|
||||
assert config['datadir'] == str(Path.cwd() / "user_data/data/binance")
|
3462
tests/test_freqtradebot.py
Normal file
3462
tests/test_freqtradebot.py
Normal file
File diff suppressed because it is too large
Load Diff
15
tests/test_indicator_helpers.py
Normal file
15
tests/test_indicator_helpers.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade.indicator_helpers import went_down, went_up
|
||||
|
||||
|
||||
def test_went_up():
|
||||
series = pd.Series([1, 2, 3, 1])
|
||||
assert went_up(series).equals(pd.Series([False, True, True, False]))
|
||||
|
||||
|
||||
def test_went_down():
|
||||
series = pd.Series([1, 2, 3, 1])
|
||||
assert went_down(series).equals(pd.Series([False, False, False, True]))
|
158
tests/test_main.py
Normal file
158
tests/test_main.py
Normal file
@@ -0,0 +1,158 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
|
||||
from copy import deepcopy
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.configuration import Arguments
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
from freqtrade.main import main
|
||||
from freqtrade.state import State
|
||||
from freqtrade.tests.conftest import (log_has, patch_exchange,
|
||||
patched_configuration_load_config_file)
|
||||
from freqtrade.worker import Worker
|
||||
|
||||
|
||||
def test_parse_args_backtesting(mocker) -> None:
|
||||
"""
|
||||
Test that main() can start backtesting and also ensure we can pass some specific arguments
|
||||
further argument parsing is done in test_arguments.py
|
||||
"""
|
||||
backtesting_mock = mocker.patch('freqtrade.optimize.start_backtesting', MagicMock())
|
||||
backtesting_mock.__name__ = PropertyMock("start_backtesting")
|
||||
# it's sys.exit(0) at the end of backtesting
|
||||
with pytest.raises(SystemExit):
|
||||
main(['backtesting'])
|
||||
assert backtesting_mock.call_count == 1
|
||||
call_args = backtesting_mock.call_args[0][0]
|
||||
assert call_args.config == ['config.json']
|
||||
assert call_args.verbosity == 0
|
||||
assert call_args.subparser == 'backtesting'
|
||||
assert call_args.func is not None
|
||||
assert call_args.ticker_interval is None
|
||||
|
||||
|
||||
def test_main_start_hyperopt(mocker) -> None:
|
||||
hyperopt_mock = mocker.patch('freqtrade.optimize.start_hyperopt', MagicMock())
|
||||
hyperopt_mock.__name__ = PropertyMock("start_hyperopt")
|
||||
# it's sys.exit(0) at the end of hyperopt
|
||||
with pytest.raises(SystemExit):
|
||||
main(['hyperopt'])
|
||||
assert hyperopt_mock.call_count == 1
|
||||
call_args = hyperopt_mock.call_args[0][0]
|
||||
assert call_args.config == ['config.json']
|
||||
assert call_args.verbosity == 0
|
||||
assert call_args.subparser == 'hyperopt'
|
||||
assert call_args.func is not None
|
||||
|
||||
|
||||
def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
|
||||
mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=Exception))
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
|
||||
|
||||
args = ['-c', 'config.json.example']
|
||||
|
||||
# Test Main + the KeyboardInterrupt exception
|
||||
with pytest.raises(SystemExit):
|
||||
main(args)
|
||||
assert log_has('Using config: config.json.example ...', caplog)
|
||||
assert log_has('Fatal exception!', caplog)
|
||||
|
||||
|
||||
def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
|
||||
mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=KeyboardInterrupt))
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
|
||||
|
||||
args = ['-c', 'config.json.example']
|
||||
|
||||
# Test Main + the KeyboardInterrupt exception
|
||||
with pytest.raises(SystemExit):
|
||||
main(args)
|
||||
assert log_has('Using config: config.json.example ...', caplog)
|
||||
assert log_has('SIGINT received, aborting ...', caplog)
|
||||
|
||||
|
||||
def test_main_operational_exception(mocker, default_conf, caplog) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
|
||||
mocker.patch(
|
||||
'freqtrade.worker.Worker._worker',
|
||||
MagicMock(side_effect=OperationalException('Oh snap!'))
|
||||
)
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
|
||||
|
||||
args = ['-c', 'config.json.example']
|
||||
|
||||
# Test Main + the KeyboardInterrupt exception
|
||||
with pytest.raises(SystemExit):
|
||||
main(args)
|
||||
assert log_has('Using config: config.json.example ...', caplog)
|
||||
assert log_has('Oh snap!', caplog)
|
||||
|
||||
|
||||
def test_main_reload_conf(mocker, default_conf, caplog) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
|
||||
# Simulate Running, reload, running workflow
|
||||
worker_mock = MagicMock(side_effect=[State.RUNNING,
|
||||
State.RELOAD_CONF,
|
||||
State.RUNNING,
|
||||
OperationalException("Oh snap!")])
|
||||
mocker.patch('freqtrade.worker.Worker._worker', worker_mock)
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
reconfigure_mock = mocker.patch('freqtrade.main.Worker._reconfigure', MagicMock())
|
||||
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
|
||||
|
||||
args = Arguments(['-c', 'config.json.example']).get_parsed_arg()
|
||||
worker = Worker(args=args, config=default_conf)
|
||||
with pytest.raises(SystemExit):
|
||||
main(['-c', 'config.json.example'])
|
||||
|
||||
assert log_has('Using config: config.json.example ...', caplog)
|
||||
assert worker_mock.call_count == 4
|
||||
assert reconfigure_mock.call_count == 1
|
||||
assert isinstance(worker.freqtrade, FreqtradeBot)
|
||||
|
||||
|
||||
def test_reconfigure(mocker, default_conf) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
|
||||
mocker.patch(
|
||||
'freqtrade.worker.Worker._worker',
|
||||
MagicMock(side_effect=OperationalException('Oh snap!'))
|
||||
)
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
|
||||
|
||||
args = Arguments(['-c', 'config.json.example']).get_parsed_arg()
|
||||
worker = Worker(args=args, config=default_conf)
|
||||
freqtrade = worker.freqtrade
|
||||
|
||||
# Renew mock to return modified data
|
||||
conf = deepcopy(default_conf)
|
||||
conf['stake_amount'] += 1
|
||||
patched_configuration_load_config_file(mocker, conf)
|
||||
|
||||
worker._config = conf
|
||||
# reconfigure should return a new instance
|
||||
worker._reconfigure()
|
||||
freqtrade2 = worker.freqtrade
|
||||
|
||||
# Verify we have a new instance with the new config
|
||||
assert freqtrade is not freqtrade2
|
||||
assert freqtrade.config['stake_amount'] + 1 == freqtrade2.config['stake_amount']
|
71
tests/test_misc.py
Normal file
71
tests/test_misc.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# pragma pylint: disable=missing-docstring,C0103
|
||||
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freqtrade.data.converter import parse_ticker_dataframe
|
||||
from freqtrade.data.history import pair_data_filename
|
||||
from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json,
|
||||
file_load_json, format_ms_time, shorten_date)
|
||||
|
||||
|
||||
def test_shorten_date() -> None:
|
||||
str_data = '1 day, 2 hours, 3 minutes, 4 seconds ago'
|
||||
str_shorten_data = '1 d, 2 h, 3 min, 4 sec ago'
|
||||
assert shorten_date(str_data) == str_shorten_data
|
||||
|
||||
|
||||
def test_datesarray_to_datetimearray(ticker_history_list):
|
||||
dataframes = parse_ticker_dataframe(ticker_history_list, "5m", pair="UNITTEST/BTC",
|
||||
fill_missing=True)
|
||||
dates = datesarray_to_datetimearray(dataframes['date'])
|
||||
|
||||
assert isinstance(dates[0], datetime.datetime)
|
||||
assert dates[0].year == 2017
|
||||
assert dates[0].month == 11
|
||||
assert dates[0].day == 26
|
||||
assert dates[0].hour == 8
|
||||
assert dates[0].minute == 50
|
||||
|
||||
date_len = len(dates)
|
||||
assert date_len == 2
|
||||
|
||||
|
||||
def test_file_dump_json(mocker) -> None:
|
||||
file_open = mocker.patch('freqtrade.misc.open', MagicMock())
|
||||
json_dump = mocker.patch('rapidjson.dump', MagicMock())
|
||||
file_dump_json(Path('somefile'), [1, 2, 3])
|
||||
assert file_open.call_count == 1
|
||||
assert json_dump.call_count == 1
|
||||
file_open = mocker.patch('freqtrade.misc.gzip.open', MagicMock())
|
||||
json_dump = mocker.patch('rapidjson.dump', MagicMock())
|
||||
file_dump_json(Path('somefile'), [1, 2, 3], True)
|
||||
assert file_open.call_count == 1
|
||||
assert json_dump.call_count == 1
|
||||
|
||||
|
||||
def test_file_load_json(mocker, testdatadir) -> None:
|
||||
|
||||
# 7m .json does not exist
|
||||
ret = file_load_json(pair_data_filename(testdatadir, 'UNITTEST/BTC', '7m'))
|
||||
assert not ret
|
||||
# 1m json exists (but no .gz exists)
|
||||
ret = file_load_json(pair_data_filename(testdatadir, 'UNITTEST/BTC', '1m'))
|
||||
assert ret
|
||||
# 8 .json is empty and will fail if it's loaded. .json.gz is a copy of 1.json
|
||||
ret = file_load_json(pair_data_filename(testdatadir, 'UNITTEST/BTC', '8m'))
|
||||
assert ret
|
||||
|
||||
|
||||
def test_format_ms_time() -> None:
|
||||
# Date 2018-04-10 18:02:01
|
||||
date_in_epoch_ms = 1523383321000
|
||||
date = format_ms_time(date_in_epoch_ms)
|
||||
assert type(date) is str
|
||||
res = datetime.datetime(2018, 4, 10, 18, 2, 1, tzinfo=datetime.timezone.utc)
|
||||
assert date == res.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S')
|
||||
res = datetime.datetime(2017, 12, 13, 8, 2, 1, tzinfo=datetime.timezone.utc)
|
||||
# Date 2017-12-13 08:02:01
|
||||
date_in_epoch_ms = 1513152121000
|
||||
assert format_ms_time(date_in_epoch_ms) == res.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S')
|
838
tests/test_persistence.py
Normal file
838
tests/test_persistence.py
Normal file
@@ -0,0 +1,838 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
import logging
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import arrow
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
from freqtrade import OperationalException, constants
|
||||
from freqtrade.persistence import Trade, clean_dry_run_db, init
|
||||
from freqtrade.tests.conftest import log_has
|
||||
|
||||
|
||||
def create_mock_trades(fee):
|
||||
"""
|
||||
Create some fake trades ...
|
||||
"""
|
||||
# Simulate dry_run entries
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.123,
|
||||
exchange='bittrex',
|
||||
open_order_id='dry_run_buy_12345'
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
|
||||
trade = Trade(
|
||||
pair='ETC/BTC',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.123,
|
||||
exchange='bittrex',
|
||||
is_open=False,
|
||||
open_order_id='dry_run_sell_12345'
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
|
||||
# Simulate prod entry
|
||||
trade = Trade(
|
||||
pair='ETC/BTC',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.123,
|
||||
exchange='bittrex',
|
||||
open_order_id='prod_buy_12345'
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
|
||||
|
||||
def test_init_create_session(default_conf):
|
||||
# Check if init create a session
|
||||
init(default_conf['db_url'], default_conf['dry_run'])
|
||||
assert hasattr(Trade, 'session')
|
||||
assert 'Session' in type(Trade.session).__name__
|
||||
|
||||
|
||||
def test_init_custom_db_url(default_conf, mocker):
|
||||
# Update path to a value other than default, but still in-memory
|
||||
default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'})
|
||||
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||
|
||||
init(default_conf['db_url'], default_conf['dry_run'])
|
||||
assert create_engine_mock.call_count == 1
|
||||
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite'
|
||||
|
||||
|
||||
def test_init_invalid_db_url(default_conf):
|
||||
# Update path to a value other than default, but still in-memory
|
||||
default_conf.update({'db_url': 'unknown:///some.url'})
|
||||
with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
|
||||
init(default_conf['db_url'], default_conf['dry_run'])
|
||||
|
||||
|
||||
def test_init_prod_db(default_conf, mocker):
|
||||
default_conf.update({'dry_run': False})
|
||||
default_conf.update({'db_url': constants.DEFAULT_DB_PROD_URL})
|
||||
|
||||
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||
|
||||
init(default_conf['db_url'], default_conf['dry_run'])
|
||||
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, mocker):
|
||||
default_conf.update({'dry_run': True})
|
||||
default_conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL})
|
||||
|
||||
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||
|
||||
init(default_conf['db_url'], default_conf['dry_run'])
|
||||
assert create_engine_mock.call_count == 1
|
||||
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://'
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog):
|
||||
"""
|
||||
On this test we will buy and sell a crypto currency.
|
||||
|
||||
Buy
|
||||
- Buy: 90.99181073 Crypto at 0.00001099 BTC
|
||||
(90.99181073*0.00001099 = 0.0009999 BTC)
|
||||
- Buying fee: 0.25%
|
||||
- Total cost of buy trade: 0.001002500 BTC
|
||||
((90.99181073*0.00001099) + ((90.99181073*0.00001099)*0.0025))
|
||||
|
||||
Sell
|
||||
- Sell: 90.99181073 Crypto at 0.00001173 BTC
|
||||
(90.99181073*0.00001173 = 0,00106733394 BTC)
|
||||
- Selling fee: 0.25%
|
||||
- Total cost of sell trade: 0.001064666 BTC
|
||||
((90.99181073*0.00001173) - ((90.99181073*0.00001173)*0.0025))
|
||||
|
||||
Profit/Loss: +0.000062166 BTC
|
||||
(Sell:0.001064666 - Buy:0.001002500)
|
||||
Profit/Loss percentage: 0.0620
|
||||
((0.001064666/0.001002500)-1 = 6.20%)
|
||||
|
||||
:param limit_buy_order:
|
||||
:param limit_sell_order:
|
||||
:return:
|
||||
"""
|
||||
|
||||
trade = Trade(
|
||||
id=2,
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
exchange='bittrex',
|
||||
)
|
||||
assert trade.open_order_id is None
|
||||
assert trade.open_rate is None
|
||||
assert trade.close_profit is None
|
||||
assert trade.close_date is None
|
||||
|
||||
trade.open_order_id = 'something'
|
||||
trade.update(limit_buy_order)
|
||||
assert trade.open_order_id is None
|
||||
assert trade.open_rate == 0.00001099
|
||||
assert trade.close_profit is None
|
||||
assert trade.close_date is None
|
||||
assert log_has("LIMIT_BUY has been fulfilled for Trade(id=2, "
|
||||
"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=closed).",
|
||||
caplog)
|
||||
|
||||
caplog.clear()
|
||||
trade.open_order_id = 'something'
|
||||
trade.update(limit_sell_order)
|
||||
assert trade.open_order_id is None
|
||||
assert trade.close_rate == 0.00001173
|
||||
assert trade.close_profit == 0.06201058
|
||||
assert trade.close_date is not None
|
||||
assert log_has("LIMIT_SELL has been fulfilled for Trade(id=2, "
|
||||
"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=closed).",
|
||||
caplog)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_update_market_order(market_buy_order, market_sell_order, fee, caplog):
|
||||
trade = Trade(
|
||||
id=1,
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
exchange='bittrex',
|
||||
)
|
||||
|
||||
trade.open_order_id = 'something'
|
||||
trade.update(market_buy_order)
|
||||
assert trade.open_order_id is None
|
||||
assert trade.open_rate == 0.00004099
|
||||
assert trade.close_profit is None
|
||||
assert trade.close_date is None
|
||||
assert log_has("MARKET_BUY has been fulfilled for Trade(id=1, "
|
||||
"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=closed).",
|
||||
caplog)
|
||||
|
||||
caplog.clear()
|
||||
trade.open_order_id = 'something'
|
||||
trade.update(market_sell_order)
|
||||
assert trade.open_order_id is None
|
||||
assert trade.close_rate == 0.00004173
|
||||
assert trade.close_profit == 0.01297561
|
||||
assert trade.close_date is not None
|
||||
assert log_has("MARKET_SELL has been fulfilled for Trade(id=1, "
|
||||
"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=closed).",
|
||||
caplog)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee):
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
exchange='bittrex',
|
||||
)
|
||||
|
||||
trade.open_order_id = 'something'
|
||||
trade.update(limit_buy_order)
|
||||
assert trade.calc_open_trade_price() == 0.0010024999999225068
|
||||
|
||||
trade.update(limit_sell_order)
|
||||
assert trade.calc_close_trade_price() == 0.0010646656050132426
|
||||
|
||||
# Profit in BTC
|
||||
assert trade.calc_profit() == 0.00006217
|
||||
|
||||
# Profit in percent
|
||||
assert trade.calc_profit_percent() == 0.06201058
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_calc_close_trade_price_exception(limit_buy_order, fee):
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
exchange='bittrex',
|
||||
)
|
||||
|
||||
trade.open_order_id = 'something'
|
||||
trade.update(limit_buy_order)
|
||||
assert trade.calc_close_trade_price() == 0.0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_update_open_order(limit_buy_order):
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=1.00,
|
||||
fee_open=0.1,
|
||||
fee_close=0.1,
|
||||
exchange='bittrex',
|
||||
)
|
||||
|
||||
assert trade.open_order_id is None
|
||||
assert trade.open_rate is None
|
||||
assert trade.close_profit is None
|
||||
assert trade.close_date is None
|
||||
|
||||
limit_buy_order['status'] = 'open'
|
||||
trade.update(limit_buy_order)
|
||||
|
||||
assert trade.open_order_id is None
|
||||
assert trade.open_rate is None
|
||||
assert trade.close_profit is None
|
||||
assert trade.close_date is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_update_invalid_order(limit_buy_order):
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=1.00,
|
||||
fee_open=0.1,
|
||||
fee_close=0.1,
|
||||
exchange='bittrex',
|
||||
)
|
||||
limit_buy_order['type'] = 'invalid'
|
||||
with pytest.raises(ValueError, match=r'Unknown order type'):
|
||||
trade.update(limit_buy_order)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_calc_open_trade_price(limit_buy_order, fee):
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
exchange='bittrex',
|
||||
)
|
||||
trade.open_order_id = 'open_trade'
|
||||
trade.update(limit_buy_order) # Buy @ 0.00001099
|
||||
|
||||
# Get the open rate price with the standard fee rate
|
||||
assert trade.calc_open_trade_price() == 0.0010024999999225068
|
||||
|
||||
# Get the open rate price with a custom fee rate
|
||||
assert trade.calc_open_trade_price(fee=0.003) == 0.001002999999922468
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee):
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
exchange='bittrex',
|
||||
)
|
||||
trade.open_order_id = 'close_trade'
|
||||
trade.update(limit_buy_order) # Buy @ 0.00001099
|
||||
|
||||
# Get the close rate price with a custom close rate and a regular fee rate
|
||||
assert trade.calc_close_trade_price(rate=0.00001234) == 0.0011200318470471794
|
||||
|
||||
# Get the close rate price with a custom close rate and a custom fee rate
|
||||
assert trade.calc_close_trade_price(rate=0.00001234, fee=0.003) == 0.0011194704275749754
|
||||
|
||||
# Test when we apply a Sell order, and ask price with a custom fee rate
|
||||
trade.update(limit_sell_order)
|
||||
assert trade.calc_close_trade_price(fee=0.005) == 0.0010619972701635854
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_calc_profit(limit_buy_order, limit_sell_order, fee):
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
exchange='bittrex',
|
||||
)
|
||||
trade.open_order_id = 'profit_percent'
|
||||
trade.update(limit_buy_order) # Buy @ 0.00001099
|
||||
|
||||
# Custom closing rate and regular fee rate
|
||||
# Higher than open rate
|
||||
assert trade.calc_profit(rate=0.00001234) == 0.00011753
|
||||
# Lower than open rate
|
||||
assert trade.calc_profit(rate=0.00000123) == -0.00089086
|
||||
|
||||
# Custom closing rate and custom fee rate
|
||||
# Higher than open rate
|
||||
assert trade.calc_profit(rate=0.00001234, fee=0.003) == 0.00011697
|
||||
# Lower than open rate
|
||||
assert trade.calc_profit(rate=0.00000123, fee=0.003) == -0.00089092
|
||||
|
||||
# Test when we apply a Sell order. Sell higher than open rate @ 0.00001173
|
||||
trade.update(limit_sell_order)
|
||||
assert trade.calc_profit() == 0.00006217
|
||||
|
||||
# Test with a custom fee rate on the close trade
|
||||
assert trade.calc_profit(fee=0.003) == 0.00006163
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee):
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
exchange='bittrex',
|
||||
)
|
||||
trade.open_order_id = 'profit_percent'
|
||||
trade.update(limit_buy_order) # Buy @ 0.00001099
|
||||
|
||||
# Get percent of profit with a custom rate (Higher than open rate)
|
||||
assert trade.calc_profit_percent(rate=0.00001234) == 0.11723875
|
||||
|
||||
# Get percent of profit with a custom rate (Lower than open rate)
|
||||
assert trade.calc_profit_percent(rate=0.00000123) == -0.88863828
|
||||
|
||||
# Test when we apply a Sell order. Sell higher than open rate @ 0.00001173
|
||||
trade.update(limit_sell_order)
|
||||
assert trade.calc_profit_percent() == 0.06201058
|
||||
|
||||
# Test with a custom fee rate on the close trade
|
||||
assert trade.calc_profit_percent(fee=0.003) == 0.06147824
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_clean_dry_run_db(default_conf, fee):
|
||||
|
||||
# Simulate dry_run entries
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.123,
|
||||
exchange='bittrex',
|
||||
open_order_id='dry_run_buy_12345'
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
|
||||
trade = Trade(
|
||||
pair='ETC/BTC',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.123,
|
||||
exchange='bittrex',
|
||||
open_order_id='dry_run_sell_12345'
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
|
||||
# Simulate prod entry
|
||||
trade = Trade(
|
||||
pair='ETC/BTC',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.123,
|
||||
exchange='bittrex',
|
||||
open_order_id='prod_buy_12345'
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
|
||||
# We have 3 entries: 2 dry_run, 1 prod
|
||||
assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 3
|
||||
|
||||
clean_dry_run_db()
|
||||
|
||||
# We have now only the prod
|
||||
assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 1
|
||||
|
||||
|
||||
def test_migrate_old(mocker, default_conf, fee):
|
||||
"""
|
||||
Test Database migration(starting with old pairformat)
|
||||
"""
|
||||
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 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_rate, stake_amount, amount, open_date)
|
||||
VALUES ('BITTREX', 'BTC_ETC', 1, {fee},
|
||||
0.00258580, {stake}, {amount},
|
||||
'2017-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.create_engine', lambda *args, **kwargs: engine)
|
||||
|
||||
# Create table using the old format
|
||||
engine.execute(create_table_old)
|
||||
engine.execute(insert_table_old)
|
||||
# Run init to test migration
|
||||
init(default_conf['db_url'], default_conf['dry_run'])
|
||||
|
||||
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.stake_amount == default_conf.get("stake_amount")
|
||||
assert trade.pair == "ETC/BTC"
|
||||
assert trade.exchange == "bittrex"
|
||||
assert trade.max_rate == 0.0
|
||||
assert trade.stop_loss == 0.0
|
||||
assert trade.initial_stop_loss == 0.0
|
||||
|
||||
|
||||
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,
|
||||
PRIMARY KEY (id),
|
||||
CHECK (is_open IN (0, 1))
|
||||
);"""
|
||||
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)
|
||||
VALUES ('binance', 'ETC/BTC', 1, {fee},
|
||||
0.00258580, {stake}, {amount},
|
||||
'2019-11-28 12:44:24.000000',
|
||||
0.0, 0.0, 0.0)
|
||||
""".format(fee=fee.return_value,
|
||||
stake=default_conf.get("stake_amount"),
|
||||
amount=amount
|
||||
)
|
||||
engine = create_engine('sqlite://')
|
||||
mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine)
|
||||
|
||||
# Create table using the old format
|
||||
engine.execute(create_table_old)
|
||||
engine.execute("create index ix_trades_is_open on trades(is_open)")
|
||||
engine.execute("create index ix_trades_pair on trades(pair)")
|
||||
engine.execute(insert_table_old)
|
||||
|
||||
# fake previous backup
|
||||
engine.execute("create table trades_bak as select * from trades")
|
||||
|
||||
engine.execute("create table trades_bak1 as select * from trades")
|
||||
# Run init to test migration
|
||||
init(default_conf['db_url'], default_conf['dry_run'])
|
||||
|
||||
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.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.sell_reason is None
|
||||
assert trade.strategy is None
|
||||
assert trade.ticker_interval is None
|
||||
assert trade.stoploss_order_id is None
|
||||
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 - backup available as trades_bak2", caplog)
|
||||
|
||||
|
||||
def test_migrate_mid_state(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.create_engine', lambda *args, **kwargs: engine)
|
||||
|
||||
# Create table using the old format
|
||||
engine.execute(create_table_old)
|
||||
engine.execute(insert_table_old)
|
||||
|
||||
# Run init to test migration
|
||||
init(default_conf['db_url'], default_conf['dry_run'])
|
||||
|
||||
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.stake_amount == default_conf.get("stake_amount")
|
||||
assert trade.pair == "ETC/BTC"
|
||||
assert trade.exchange == "binance"
|
||||
assert trade.max_rate == 0.0
|
||||
assert trade.stop_loss == 0.0
|
||||
assert trade.initial_stop_loss == 0.0
|
||||
assert log_has("trying trades_bak0", caplog)
|
||||
assert log_has("Running database migration - backup available as trades_bak0", caplog)
|
||||
|
||||
|
||||
def test_adjust_stop_loss(fee):
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
exchange='bittrex',
|
||||
open_rate=1,
|
||||
max_rate=1,
|
||||
)
|
||||
|
||||
trade.adjust_stop_loss(trade.open_rate, 0.05, True)
|
||||
assert trade.stop_loss == 0.95
|
||||
assert trade.stop_loss_pct == -0.05
|
||||
assert trade.initial_stop_loss == 0.95
|
||||
assert trade.initial_stop_loss_pct == -0.05
|
||||
|
||||
# Get percent of profit with a lower rate
|
||||
trade.adjust_stop_loss(0.96, 0.05)
|
||||
assert trade.stop_loss == 0.95
|
||||
assert trade.stop_loss_pct == -0.05
|
||||
assert trade.initial_stop_loss == 0.95
|
||||
assert trade.initial_stop_loss_pct == -0.05
|
||||
|
||||
# Get percent of profit with a custom rate (Higher than open rate)
|
||||
trade.adjust_stop_loss(1.3, -0.1)
|
||||
assert round(trade.stop_loss, 8) == 1.17
|
||||
assert trade.stop_loss_pct == -0.1
|
||||
assert trade.initial_stop_loss == 0.95
|
||||
assert trade.initial_stop_loss_pct == -0.05
|
||||
|
||||
# current rate lower again ... should not change
|
||||
trade.adjust_stop_loss(1.2, 0.1)
|
||||
assert round(trade.stop_loss, 8) == 1.17
|
||||
assert trade.initial_stop_loss == 0.95
|
||||
assert trade.initial_stop_loss_pct == -0.05
|
||||
|
||||
# current rate higher... should raise stoploss
|
||||
trade.adjust_stop_loss(1.4, 0.1)
|
||||
assert round(trade.stop_loss, 8) == 1.26
|
||||
assert trade.initial_stop_loss == 0.95
|
||||
assert trade.initial_stop_loss_pct == -0.05
|
||||
|
||||
# Initial is true but stop_loss set - so doesn't do anything
|
||||
trade.adjust_stop_loss(1.7, 0.1, True)
|
||||
assert round(trade.stop_loss, 8) == 1.26
|
||||
assert trade.initial_stop_loss == 0.95
|
||||
assert trade.initial_stop_loss_pct == -0.05
|
||||
assert trade.stop_loss_pct == -0.1
|
||||
|
||||
|
||||
def test_adjust_min_max_rates(fee):
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
exchange='bittrex',
|
||||
open_rate=1,
|
||||
)
|
||||
|
||||
trade.adjust_min_max_rates(trade.open_rate)
|
||||
assert trade.max_rate == 1
|
||||
assert trade.min_rate == 1
|
||||
|
||||
# check min adjusted, max remained
|
||||
trade.adjust_min_max_rates(0.96)
|
||||
assert trade.max_rate == 1
|
||||
assert trade.min_rate == 0.96
|
||||
|
||||
# check max adjusted, min remains
|
||||
trade.adjust_min_max_rates(1.05)
|
||||
assert trade.max_rate == 1.05
|
||||
assert trade.min_rate == 0.96
|
||||
|
||||
# current rate "in the middle" - no adjustment
|
||||
trade.adjust_min_max_rates(1.03)
|
||||
assert trade.max_rate == 1.05
|
||||
assert trade.min_rate == 0.96
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_get_open(default_conf, fee):
|
||||
|
||||
create_mock_trades(fee)
|
||||
assert len(Trade.get_open_trades()) == 2
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
def test_to_json(default_conf, fee):
|
||||
|
||||
# Simulate dry_run entries
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_date=arrow.utcnow().shift(hours=-2).datetime,
|
||||
open_rate=0.123,
|
||||
exchange='bittrex',
|
||||
open_order_id='dry_run_buy_12345'
|
||||
)
|
||||
result = trade.to_json()
|
||||
assert isinstance(result, dict)
|
||||
print(result)
|
||||
|
||||
assert result == {'trade_id': None,
|
||||
'pair': 'ETH/BTC',
|
||||
'open_date_hum': '2 hours ago',
|
||||
'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'close_date_hum': None,
|
||||
'close_date': None,
|
||||
'open_rate': 0.123,
|
||||
'close_rate': None,
|
||||
'amount': 123.0,
|
||||
'stake_amount': 0.001,
|
||||
'stop_loss': None,
|
||||
'stop_loss_pct': None,
|
||||
'initial_stop_loss': None,
|
||||
'initial_stop_loss_pct': None}
|
||||
|
||||
# Simulate dry_run entries
|
||||
trade = Trade(
|
||||
pair='XRP/BTC',
|
||||
stake_amount=0.001,
|
||||
amount=100.0,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_date=arrow.utcnow().shift(hours=-2).datetime,
|
||||
close_date=arrow.utcnow().shift(hours=-1).datetime,
|
||||
open_rate=0.123,
|
||||
close_rate=0.125,
|
||||
exchange='bittrex',
|
||||
)
|
||||
result = trade.to_json()
|
||||
assert isinstance(result, dict)
|
||||
|
||||
assert result == {'trade_id': None,
|
||||
'pair': 'XRP/BTC',
|
||||
'open_date_hum': '2 hours ago',
|
||||
'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'close_date_hum': 'an hour ago',
|
||||
'close_date': trade.close_date.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'open_rate': 0.123,
|
||||
'close_rate': 0.125,
|
||||
'amount': 100.0,
|
||||
'stake_amount': 0.001,
|
||||
'stop_loss': None,
|
||||
'stop_loss_pct': None,
|
||||
'initial_stop_loss': None,
|
||||
'initial_stop_loss_pct': None}
|
||||
|
||||
|
||||
def test_stoploss_reinitialization(default_conf, fee):
|
||||
init(default_conf['db_url'])
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
fee_open=fee.return_value,
|
||||
open_date=arrow.utcnow().shift(hours=-2).datetime,
|
||||
amount=10,
|
||||
fee_close=fee.return_value,
|
||||
exchange='bittrex',
|
||||
open_rate=1,
|
||||
max_rate=1,
|
||||
)
|
||||
|
||||
trade.adjust_stop_loss(trade.open_rate, 0.05, True)
|
||||
assert trade.stop_loss == 0.95
|
||||
assert trade.stop_loss_pct == -0.05
|
||||
assert trade.initial_stop_loss == 0.95
|
||||
assert trade.initial_stop_loss_pct == -0.05
|
||||
Trade.session.add(trade)
|
||||
|
||||
# Lower stoploss
|
||||
Trade.stoploss_reinitialization(0.06)
|
||||
|
||||
trades = Trade.get_open_trades()
|
||||
assert len(trades) == 1
|
||||
trade_adj = trades[0]
|
||||
assert trade_adj.stop_loss == 0.94
|
||||
assert trade_adj.stop_loss_pct == -0.06
|
||||
assert trade_adj.initial_stop_loss == 0.94
|
||||
assert trade_adj.initial_stop_loss_pct == -0.06
|
||||
|
||||
# Raise stoploss
|
||||
Trade.stoploss_reinitialization(0.04)
|
||||
|
||||
trades = Trade.get_open_trades()
|
||||
assert len(trades) == 1
|
||||
trade_adj = trades[0]
|
||||
assert trade_adj.stop_loss == 0.96
|
||||
assert trade_adj.stop_loss_pct == -0.04
|
||||
assert trade_adj.initial_stop_loss == 0.96
|
||||
assert trade_adj.initial_stop_loss_pct == -0.04
|
||||
|
||||
# Trailing stoploss (move stoplos up a bit)
|
||||
trade.adjust_stop_loss(1.02, 0.04)
|
||||
assert trade_adj.stop_loss == 0.9792
|
||||
assert trade_adj.initial_stop_loss == 0.96
|
||||
|
||||
Trade.stoploss_reinitialization(0.04)
|
||||
|
||||
trades = Trade.get_open_trades()
|
||||
assert len(trades) == 1
|
||||
trade_adj = trades[0]
|
||||
# Stoploss should not change in this case.
|
||||
assert trade_adj.stop_loss == 0.9792
|
||||
assert trade_adj.stop_loss_pct == -0.04
|
||||
assert trade_adj.initial_stop_loss == 0.96
|
||||
assert trade_adj.initial_stop_loss_pct == -0.04
|
367
tests/test_plotting.py
Normal file
367
tests/test_plotting.py
Normal file
@@ -0,0 +1,367 @@
|
||||
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import plotly.graph_objects as go
|
||||
import pytest
|
||||
from plotly.subplots import make_subplots
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.data import history
|
||||
from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data
|
||||
from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit
|
||||
from freqtrade.plot.plotting import (add_indicators, add_profit,
|
||||
analyse_and_plot_pairs,
|
||||
generate_candlestick_graph,
|
||||
generate_plot_filename,
|
||||
generate_profit_graph, init_plotscript,
|
||||
plot_profit, plot_trades, store_plot_file)
|
||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||
from freqtrade.tests.conftest import get_args, log_has, log_has_re
|
||||
|
||||
|
||||
def fig_generating_mock(fig, *args, **kwargs):
|
||||
""" Return Fig - used to mock add_indicators and plot_trades"""
|
||||
return fig
|
||||
|
||||
|
||||
def find_trace_in_fig_data(data, search_string: str):
|
||||
matches = (d for d in data if d.name == search_string)
|
||||
return next(matches)
|
||||
|
||||
|
||||
def generage_empty_figure():
|
||||
return make_subplots(
|
||||
rows=3,
|
||||
cols=1,
|
||||
shared_xaxes=True,
|
||||
row_width=[1, 1, 4],
|
||||
vertical_spacing=0.0001,
|
||||
)
|
||||
|
||||
|
||||
def test_init_plotscript(default_conf, mocker, testdatadir):
|
||||
default_conf['timerange'] = "20180110-20180112"
|
||||
default_conf['trade_source'] = "file"
|
||||
default_conf['ticker_interval'] = "5m"
|
||||
default_conf["datadir"] = testdatadir
|
||||
default_conf['exportfilename'] = str(testdatadir / "backtest-result_test.json")
|
||||
ret = init_plotscript(default_conf)
|
||||
assert "tickers" in ret
|
||||
assert "trades" in ret
|
||||
assert "pairs" in ret
|
||||
|
||||
default_conf['pairs'] = ["POWR/BTC", "XLM/BTC"]
|
||||
ret = init_plotscript(default_conf)
|
||||
assert "tickers" in ret
|
||||
assert "POWR/BTC" in ret["tickers"]
|
||||
assert "XLM/BTC" in ret["tickers"]
|
||||
|
||||
|
||||
def test_add_indicators(default_conf, testdatadir, caplog):
|
||||
pair = "UNITTEST/BTC"
|
||||
timerange = TimeRange(None, 'line', 0, -1000)
|
||||
|
||||
data = history.load_pair_history(pair=pair, ticker_interval='1m',
|
||||
datadir=testdatadir, timerange=timerange)
|
||||
indicators1 = ["ema10"]
|
||||
indicators2 = ["macd"]
|
||||
|
||||
# Generate buy/sell signals and indicators
|
||||
strat = DefaultStrategy(default_conf)
|
||||
data = strat.analyze_ticker(data, {'pair': pair})
|
||||
fig = generage_empty_figure()
|
||||
|
||||
# Row 1
|
||||
fig1 = add_indicators(fig=deepcopy(fig), row=1, indicators=indicators1, data=data)
|
||||
figure = fig1.layout.figure
|
||||
ema10 = find_trace_in_fig_data(figure.data, "ema10")
|
||||
assert isinstance(ema10, go.Scatter)
|
||||
assert ema10.yaxis == "y"
|
||||
|
||||
fig2 = add_indicators(fig=deepcopy(fig), row=3, indicators=indicators2, data=data)
|
||||
figure = fig2.layout.figure
|
||||
macd = find_trace_in_fig_data(figure.data, "macd")
|
||||
assert isinstance(macd, go.Scatter)
|
||||
assert macd.yaxis == "y3"
|
||||
|
||||
# No indicator found
|
||||
fig3 = add_indicators(fig=deepcopy(fig), row=3, indicators=['no_indicator'], data=data)
|
||||
assert fig == fig3
|
||||
assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog)
|
||||
|
||||
|
||||
def test_plot_trades(testdatadir, caplog):
|
||||
fig1 = generage_empty_figure()
|
||||
# nothing happens when no trades are available
|
||||
fig = plot_trades(fig1, None)
|
||||
assert fig == fig1
|
||||
assert log_has("No trades found.", caplog)
|
||||
pair = "ADA/BTC"
|
||||
filename = testdatadir / "backtest-result_test.json"
|
||||
trades = load_backtest_data(filename)
|
||||
trades = trades.loc[trades['pair'] == pair]
|
||||
|
||||
fig = plot_trades(fig, trades)
|
||||
figure = fig1.layout.figure
|
||||
|
||||
# Check buys - color, should be in first graph, ...
|
||||
trade_buy = find_trace_in_fig_data(figure.data, "trade_buy")
|
||||
assert isinstance(trade_buy, go.Scatter)
|
||||
assert trade_buy.yaxis == 'y'
|
||||
assert len(trades) == len(trade_buy.x)
|
||||
assert trade_buy.marker.color == 'green'
|
||||
|
||||
trade_sell = find_trace_in_fig_data(figure.data, "trade_sell")
|
||||
assert isinstance(trade_sell, go.Scatter)
|
||||
assert trade_sell.yaxis == 'y'
|
||||
assert len(trades) == len(trade_sell.x)
|
||||
assert trade_sell.marker.color == 'red'
|
||||
|
||||
|
||||
def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, testdatadir, caplog):
|
||||
row_mock = mocker.patch('freqtrade.plot.plotting.add_indicators',
|
||||
MagicMock(side_effect=fig_generating_mock))
|
||||
trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades',
|
||||
MagicMock(side_effect=fig_generating_mock))
|
||||
|
||||
pair = "UNITTEST/BTC"
|
||||
timerange = TimeRange(None, 'line', 0, -1000)
|
||||
data = history.load_pair_history(pair=pair, ticker_interval='1m',
|
||||
datadir=testdatadir, timerange=timerange)
|
||||
data['buy'] = 0
|
||||
data['sell'] = 0
|
||||
|
||||
indicators1 = []
|
||||
indicators2 = []
|
||||
fig = generate_candlestick_graph(pair=pair, data=data, trades=None,
|
||||
indicators1=indicators1, indicators2=indicators2)
|
||||
assert isinstance(fig, go.Figure)
|
||||
assert fig.layout.title.text == pair
|
||||
figure = fig.layout.figure
|
||||
|
||||
assert len(figure.data) == 2
|
||||
# Candlesticks are plotted first
|
||||
candles = find_trace_in_fig_data(figure.data, "Price")
|
||||
assert isinstance(candles, go.Candlestick)
|
||||
|
||||
volume = find_trace_in_fig_data(figure.data, "Volume")
|
||||
assert isinstance(volume, go.Bar)
|
||||
|
||||
assert row_mock.call_count == 2
|
||||
assert trades_mock.call_count == 1
|
||||
|
||||
assert log_has("No buy-signals found.", caplog)
|
||||
assert log_has("No sell-signals found.", caplog)
|
||||
|
||||
|
||||
def test_generate_candlestick_graph_no_trades(default_conf, mocker, testdatadir):
|
||||
row_mock = mocker.patch('freqtrade.plot.plotting.add_indicators',
|
||||
MagicMock(side_effect=fig_generating_mock))
|
||||
trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades',
|
||||
MagicMock(side_effect=fig_generating_mock))
|
||||
pair = 'UNITTEST/BTC'
|
||||
timerange = TimeRange(None, 'line', 0, -1000)
|
||||
data = history.load_pair_history(pair=pair, ticker_interval='1m',
|
||||
datadir=testdatadir, timerange=timerange)
|
||||
|
||||
# Generate buy/sell signals and indicators
|
||||
strat = DefaultStrategy(default_conf)
|
||||
data = strat.analyze_ticker(data, {'pair': pair})
|
||||
|
||||
indicators1 = []
|
||||
indicators2 = []
|
||||
fig = generate_candlestick_graph(pair=pair, data=data, trades=None,
|
||||
indicators1=indicators1, indicators2=indicators2)
|
||||
assert isinstance(fig, go.Figure)
|
||||
assert fig.layout.title.text == pair
|
||||
figure = fig.layout.figure
|
||||
|
||||
assert len(figure.data) == 6
|
||||
# Candlesticks are plotted first
|
||||
candles = find_trace_in_fig_data(figure.data, "Price")
|
||||
assert isinstance(candles, go.Candlestick)
|
||||
|
||||
volume = find_trace_in_fig_data(figure.data, "Volume")
|
||||
assert isinstance(volume, go.Bar)
|
||||
|
||||
buy = find_trace_in_fig_data(figure.data, "buy")
|
||||
assert isinstance(buy, go.Scatter)
|
||||
# All buy-signals should be plotted
|
||||
assert int(data.buy.sum()) == len(buy.x)
|
||||
|
||||
sell = find_trace_in_fig_data(figure.data, "sell")
|
||||
assert isinstance(sell, go.Scatter)
|
||||
# All buy-signals should be plotted
|
||||
assert int(data.sell.sum()) == len(sell.x)
|
||||
|
||||
assert find_trace_in_fig_data(figure.data, "BB lower")
|
||||
assert find_trace_in_fig_data(figure.data, "BB upper")
|
||||
|
||||
assert row_mock.call_count == 2
|
||||
assert trades_mock.call_count == 1
|
||||
|
||||
|
||||
def test_generate_Plot_filename():
|
||||
fn = generate_plot_filename("UNITTEST/BTC", "5m")
|
||||
assert fn == "freqtrade-plot-UNITTEST_BTC-5m.html"
|
||||
|
||||
|
||||
def test_generate_plot_file(mocker, caplog):
|
||||
fig = generage_empty_figure()
|
||||
plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock())
|
||||
store_plot_file(fig, filename="freqtrade-plot-UNITTEST_BTC-5m.html",
|
||||
directory=Path("user_data/plots"))
|
||||
|
||||
assert plot_mock.call_count == 1
|
||||
assert plot_mock.call_args[0][0] == fig
|
||||
assert (plot_mock.call_args_list[0][1]['filename']
|
||||
== "user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html")
|
||||
assert log_has("Stored plot as user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html",
|
||||
caplog)
|
||||
|
||||
|
||||
def test_add_profit(testdatadir):
|
||||
filename = testdatadir / "backtest-result_test.json"
|
||||
bt_data = load_backtest_data(filename)
|
||||
timerange = TimeRange.parse_timerange("20180110-20180112")
|
||||
|
||||
df = history.load_pair_history(pair="POWR/BTC", ticker_interval='5m',
|
||||
datadir=testdatadir, timerange=timerange)
|
||||
fig = generage_empty_figure()
|
||||
|
||||
cum_profits = create_cum_profit(df.set_index('date'),
|
||||
bt_data[bt_data["pair"] == 'POWR/BTC'],
|
||||
"cum_profits")
|
||||
|
||||
fig1 = add_profit(fig, row=2, data=cum_profits, column='cum_profits', name='Profits')
|
||||
figure = fig1.layout.figure
|
||||
profits = find_trace_in_fig_data(figure.data, "Profits")
|
||||
assert isinstance(profits, go.Scattergl)
|
||||
assert profits.yaxis == "y2"
|
||||
|
||||
|
||||
def test_generate_profit_graph(testdatadir):
|
||||
filename = testdatadir / "backtest-result_test.json"
|
||||
trades = load_backtest_data(filename)
|
||||
timerange = TimeRange.parse_timerange("20180110-20180112")
|
||||
pairs = ["POWR/BTC", "XLM/BTC"]
|
||||
|
||||
tickers = history.load_data(datadir=testdatadir,
|
||||
pairs=pairs,
|
||||
ticker_interval='5m',
|
||||
timerange=timerange
|
||||
)
|
||||
trades = trades[trades['pair'].isin(pairs)]
|
||||
|
||||
fig = generate_profit_graph(pairs, tickers, trades)
|
||||
assert isinstance(fig, go.Figure)
|
||||
|
||||
assert fig.layout.title.text == "Freqtrade Profit plot"
|
||||
assert fig.layout.yaxis.title.text == "Price"
|
||||
assert fig.layout.yaxis2.title.text == "Profit"
|
||||
assert fig.layout.yaxis3.title.text == "Profit"
|
||||
|
||||
figure = fig.layout.figure
|
||||
assert len(figure.data) == 4
|
||||
|
||||
avgclose = find_trace_in_fig_data(figure.data, "Avg close price")
|
||||
assert isinstance(avgclose, go.Scattergl)
|
||||
|
||||
profit = find_trace_in_fig_data(figure.data, "Profit")
|
||||
assert isinstance(profit, go.Scattergl)
|
||||
|
||||
for pair in pairs:
|
||||
profit_pair = find_trace_in_fig_data(figure.data, f"Profit {pair}")
|
||||
assert isinstance(profit_pair, go.Scattergl)
|
||||
|
||||
|
||||
def test_start_plot_dataframe(mocker):
|
||||
aup = mocker.patch("freqtrade.plot.plotting.analyse_and_plot_pairs", MagicMock())
|
||||
args = [
|
||||
"--config", "config.json.example",
|
||||
"plot-dataframe",
|
||||
"--pairs", "ETH/BTC"
|
||||
]
|
||||
start_plot_dataframe(get_args(args))
|
||||
|
||||
assert aup.call_count == 1
|
||||
called_config = aup.call_args_list[0][0][0]
|
||||
assert "pairs" in called_config
|
||||
assert called_config['pairs'] == ["ETH/BTC"]
|
||||
|
||||
|
||||
def test_analyse_and_plot_pairs(default_conf, mocker, caplog, testdatadir):
|
||||
default_conf['trade_source'] = 'file'
|
||||
default_conf["datadir"] = testdatadir
|
||||
default_conf['exportfilename'] = str(testdatadir / "backtest-result_test.json")
|
||||
default_conf['indicators1'] = ["sma5", "ema10"]
|
||||
default_conf['indicators2'] = ["macd"]
|
||||
default_conf['pairs'] = ["ETH/BTC", "LTC/BTC"]
|
||||
|
||||
candle_mock = MagicMock()
|
||||
store_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
"freqtrade.plot.plotting",
|
||||
generate_candlestick_graph=candle_mock,
|
||||
store_plot_file=store_mock
|
||||
)
|
||||
analyse_and_plot_pairs(default_conf)
|
||||
|
||||
# Both mocks should be called once per pair
|
||||
assert candle_mock.call_count == 2
|
||||
assert store_mock.call_count == 2
|
||||
|
||||
assert candle_mock.call_args_list[0][1]['indicators1'] == ['sma5', 'ema10']
|
||||
assert candle_mock.call_args_list[0][1]['indicators2'] == ['macd']
|
||||
|
||||
assert log_has("End of plotting process. 2 plots generated", caplog)
|
||||
|
||||
|
||||
def test_start_plot_profit(mocker):
|
||||
aup = mocker.patch("freqtrade.plot.plotting.plot_profit", MagicMock())
|
||||
args = [
|
||||
"--config", "config.json.example",
|
||||
"plot-profit",
|
||||
"--pairs", "ETH/BTC"
|
||||
]
|
||||
start_plot_profit(get_args(args))
|
||||
|
||||
assert aup.call_count == 1
|
||||
called_config = aup.call_args_list[0][0][0]
|
||||
assert "pairs" in called_config
|
||||
assert called_config['pairs'] == ["ETH/BTC"]
|
||||
|
||||
|
||||
def test_start_plot_profit_error(mocker):
|
||||
args = [
|
||||
"plot-profit",
|
||||
"--pairs", "ETH/BTC"
|
||||
]
|
||||
with pytest.raises(OperationalException):
|
||||
start_plot_profit(get_args(args))
|
||||
|
||||
|
||||
def test_plot_profit(default_conf, mocker, testdatadir, caplog):
|
||||
default_conf['trade_source'] = 'file'
|
||||
default_conf["datadir"] = testdatadir
|
||||
default_conf['exportfilename'] = str(testdatadir / "backtest-result_test.json")
|
||||
default_conf['pairs'] = ["ETH/BTC", "LTC/BTC"]
|
||||
|
||||
profit_mock = MagicMock()
|
||||
store_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
"freqtrade.plot.plotting",
|
||||
generate_profit_graph=profit_mock,
|
||||
store_plot_file=store_mock
|
||||
)
|
||||
plot_profit(default_conf)
|
||||
|
||||
# Plot-profit generates one combined plot
|
||||
assert profit_mock.call_count == 1
|
||||
assert store_mock.call_count == 1
|
||||
|
||||
assert profit_mock.call_args_list[0][0][0] == default_conf['pairs']
|
||||
assert store_mock.call_args_list[0][1]['auto_open'] is True
|
16
tests/test_talib.py
Normal file
16
tests/test_talib.py
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
|
||||
import talib.abstract as ta
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def test_talib_bollingerbands_near_zero_values():
|
||||
inputs = pd.DataFrame([
|
||||
{'close': 0.00000010},
|
||||
{'close': 0.00000011},
|
||||
{'close': 0.00000012},
|
||||
{'close': 0.00000013},
|
||||
{'close': 0.00000014}
|
||||
])
|
||||
bollinger = ta.BBANDS(inputs, matype=0, timeperiod=2)
|
||||
assert bollinger['upperband'][3] != bollinger['middleband'][3]
|
28
tests/test_timerange.py
Normal file
28
tests/test_timerange.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# pragma pylint: disable=missing-docstring, C0103
|
||||
import pytest
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
|
||||
|
||||
def test_parse_timerange_incorrect() -> None:
|
||||
assert TimeRange(None, 'line', 0, -200) == TimeRange.parse_timerange('-200')
|
||||
assert TimeRange('line', None, 200, 0) == TimeRange.parse_timerange('200-')
|
||||
assert TimeRange('index', 'index', 200, 500) == TimeRange.parse_timerange('200-500')
|
||||
|
||||
assert TimeRange('date', None, 1274486400, 0) == TimeRange.parse_timerange('20100522-')
|
||||
assert TimeRange(None, 'date', 0, 1274486400) == TimeRange.parse_timerange('-20100522')
|
||||
timerange = TimeRange.parse_timerange('20100522-20150730')
|
||||
assert timerange == TimeRange('date', 'date', 1274486400, 1438214400)
|
||||
|
||||
# Added test for unix timestamp - BTC genesis date
|
||||
assert TimeRange('date', None, 1231006505, 0) == TimeRange.parse_timerange('1231006505-')
|
||||
assert TimeRange(None, 'date', 0, 1233360000) == TimeRange.parse_timerange('-1233360000')
|
||||
timerange = TimeRange.parse_timerange('1231006505-1233360000')
|
||||
assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange
|
||||
|
||||
# TODO: Find solution for the following case (passing timestamp in ms)
|
||||
timerange = TimeRange.parse_timerange('1231006505000-1233360000000')
|
||||
assert TimeRange('date', 'date', 1231006505, 1233360000) != timerange
|
||||
|
||||
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
|
||||
TimeRange.parse_timerange('-')
|
105
tests/test_utils.py
Normal file
105
tests/test_utils.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import re
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
import pytest
|
||||
|
||||
from freqtrade.state import RunMode
|
||||
from freqtrade.tests.conftest import get_args, log_has, patch_exchange
|
||||
from freqtrade.utils import (setup_utils_configuration, start_create_userdir,
|
||||
start_download_data, start_list_exchanges)
|
||||
|
||||
|
||||
def test_setup_utils_configuration():
|
||||
args = [
|
||||
'--config', 'config.json.example',
|
||||
]
|
||||
|
||||
config = setup_utils_configuration(get_args(args), RunMode.OTHER)
|
||||
assert "exchange" in config
|
||||
assert config['exchange']['dry_run'] is True
|
||||
assert config['exchange']['key'] == ''
|
||||
assert config['exchange']['secret'] == ''
|
||||
|
||||
|
||||
def test_list_exchanges(capsys):
|
||||
|
||||
args = [
|
||||
"list-exchanges",
|
||||
]
|
||||
|
||||
start_list_exchanges(get_args(args))
|
||||
captured = capsys.readouterr()
|
||||
assert re.match(r"Exchanges supported by ccxt and available.*", captured.out)
|
||||
assert re.match(r".*binance,.*", captured.out)
|
||||
assert re.match(r".*bittrex,.*", captured.out)
|
||||
|
||||
# Test with --one-column
|
||||
args = [
|
||||
"list-exchanges",
|
||||
"--one-column",
|
||||
]
|
||||
|
||||
start_list_exchanges(get_args(args))
|
||||
captured = capsys.readouterr()
|
||||
assert not re.match(r"Exchanges supported by ccxt and available.*", captured.out)
|
||||
assert re.search(r"^binance$", captured.out, re.MULTILINE)
|
||||
assert re.search(r"^bittrex$", captured.out, re.MULTILINE)
|
||||
|
||||
|
||||
def test_create_datadir_failed(caplog):
|
||||
|
||||
args = [
|
||||
"create-userdir",
|
||||
]
|
||||
with pytest.raises(SystemExit):
|
||||
start_create_userdir(get_args(args))
|
||||
assert log_has("`create-userdir` requires --userdir to be set.", caplog)
|
||||
|
||||
|
||||
def test_create_datadir(caplog, mocker):
|
||||
cud = mocker.patch("freqtrade.utils.create_userdata_dir", MagicMock())
|
||||
args = [
|
||||
"create-userdir",
|
||||
"--userdir",
|
||||
"/temp/freqtrade/test"
|
||||
]
|
||||
start_create_userdir(get_args(args))
|
||||
|
||||
assert cud.call_count == 1
|
||||
assert len(caplog.record_tuples) == 0
|
||||
|
||||
|
||||
def test_download_data_keyboardInterrupt(mocker, caplog, markets):
|
||||
dl_mock = mocker.patch('freqtrade.utils.refresh_backtest_ohlcv_data',
|
||||
MagicMock(side_effect=KeyboardInterrupt))
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
|
||||
)
|
||||
args = [
|
||||
"download-data",
|
||||
"--exchange", "binance",
|
||||
"--pairs", "ETH/BTC", "XRP/BTC",
|
||||
]
|
||||
with pytest.raises(SystemExit):
|
||||
start_download_data(get_args(args))
|
||||
|
||||
assert dl_mock.call_count == 1
|
||||
|
||||
|
||||
def test_download_data_no_markets(mocker, caplog):
|
||||
dl_mock = mocker.patch('freqtrade.utils.refresh_backtest_ohlcv_data',
|
||||
MagicMock(return_value=["ETH/BTC", "XRP/BTC"]))
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
|
||||
)
|
||||
args = [
|
||||
"download-data",
|
||||
"--exchange", "binance",
|
||||
"--pairs", "ETH/BTC", "XRP/BTC",
|
||||
"--days", "20"
|
||||
]
|
||||
start_download_data(get_args(args))
|
||||
assert dl_mock.call_args[1]['timerange'].starttype == "date"
|
||||
assert log_has("Pairs [ETH/BTC,XRP/BTC] not available on exchange binance.", caplog)
|
91
tests/test_wallets.py
Normal file
91
tests/test_wallets.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
from freqtrade.tests.conftest import get_patched_freqtradebot
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
def test_sync_wallet_at_boot(mocker, default_conf):
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_balances=MagicMock(return_value={
|
||||
"BNT": {
|
||||
"free": 1.0,
|
||||
"used": 2.0,
|
||||
"total": 3.0
|
||||
},
|
||||
"GAS": {
|
||||
"free": 0.260739,
|
||||
"used": 0.0,
|
||||
"total": 0.260739
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
|
||||
assert len(freqtrade.wallets._wallets) == 2
|
||||
assert freqtrade.wallets._wallets['BNT'].free == 1.0
|
||||
assert freqtrade.wallets._wallets['BNT'].used == 2.0
|
||||
assert freqtrade.wallets._wallets['BNT'].total == 3.0
|
||||
assert freqtrade.wallets._wallets['GAS'].free == 0.260739
|
||||
assert freqtrade.wallets._wallets['GAS'].used == 0.0
|
||||
assert freqtrade.wallets._wallets['GAS'].total == 0.260739
|
||||
assert freqtrade.wallets.get_free('BNT') == 1.0
|
||||
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_balances=MagicMock(return_value={
|
||||
"BNT": {
|
||||
"free": 1.2,
|
||||
"used": 1.9,
|
||||
"total": 3.5
|
||||
},
|
||||
"GAS": {
|
||||
"free": 0.270739,
|
||||
"used": 0.1,
|
||||
"total": 0.260439
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
freqtrade.wallets.update()
|
||||
|
||||
assert len(freqtrade.wallets._wallets) == 2
|
||||
assert freqtrade.wallets._wallets['BNT'].free == 1.2
|
||||
assert freqtrade.wallets._wallets['BNT'].used == 1.9
|
||||
assert freqtrade.wallets._wallets['BNT'].total == 3.5
|
||||
assert freqtrade.wallets._wallets['GAS'].free == 0.270739
|
||||
assert freqtrade.wallets._wallets['GAS'].used == 0.1
|
||||
assert freqtrade.wallets._wallets['GAS'].total == 0.260439
|
||||
assert freqtrade.wallets.get_free('GAS') == 0.270739
|
||||
assert freqtrade.wallets.get_used('GAS') == 0.1
|
||||
assert freqtrade.wallets.get_total('GAS') == 0.260439
|
||||
|
||||
|
||||
def test_sync_wallet_missing_data(mocker, default_conf):
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_balances=MagicMock(return_value={
|
||||
"BNT": {
|
||||
"free": 1.0,
|
||||
"used": 2.0,
|
||||
"total": 3.0
|
||||
},
|
||||
"GAS": {
|
||||
"free": 0.260739,
|
||||
"total": 0.260739
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
|
||||
assert len(freqtrade.wallets._wallets) == 2
|
||||
assert freqtrade.wallets._wallets['BNT'].free == 1.0
|
||||
assert freqtrade.wallets._wallets['BNT'].used == 2.0
|
||||
assert freqtrade.wallets._wallets['BNT'].total == 3.0
|
||||
assert freqtrade.wallets._wallets['GAS'].free == 0.260739
|
||||
assert freqtrade.wallets._wallets['GAS'].used is None
|
||||
assert freqtrade.wallets._wallets['GAS'].total == 0.260739
|
||||
assert freqtrade.wallets.get_free('GAS') == 0.260739
|
1
tests/testdata/ADA_BTC-1m.json
vendored
Normal file
1
tests/testdata/ADA_BTC-1m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/ADA_BTC-5m.json
vendored
Normal file
1
tests/testdata/ADA_BTC-5m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/DASH_BTC-1m.json
vendored
Normal file
1
tests/testdata/DASH_BTC-1m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/DASH_BTC-5m.json
vendored
Normal file
1
tests/testdata/DASH_BTC-5m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/ETC_BTC-1m.json
vendored
Normal file
1
tests/testdata/ETC_BTC-1m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/ETC_BTC-5m.json
vendored
Normal file
1
tests/testdata/ETC_BTC-5m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/ETH_BTC-1m.json
vendored
Normal file
1
tests/testdata/ETH_BTC-1m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/ETH_BTC-5m.json
vendored
Normal file
1
tests/testdata/ETH_BTC-5m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/LTC_BTC-1m.json
vendored
Normal file
1
tests/testdata/LTC_BTC-1m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/LTC_BTC-5m.json
vendored
Normal file
1
tests/testdata/LTC_BTC-5m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/NXT_BTC-1m.json
vendored
Normal file
1
tests/testdata/NXT_BTC-1m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/NXT_BTC-5m.json
vendored
Normal file
1
tests/testdata/NXT_BTC-5m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/POWR_BTC-1m.json
vendored
Normal file
1
tests/testdata/POWR_BTC-1m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/POWR_BTC-5m.json
vendored
Normal file
1
tests/testdata/POWR_BTC-5m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/UNITTEST_BTC-1m.json
vendored
Normal file
1
tests/testdata/UNITTEST_BTC-1m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/UNITTEST_BTC-30m.json
vendored
Normal file
1
tests/testdata/UNITTEST_BTC-30m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/UNITTEST_BTC-5m.json
vendored
Normal file
1
tests/testdata/UNITTEST_BTC-5m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
0
tests/testdata/UNITTEST_BTC-8m.json
vendored
Normal file
0
tests/testdata/UNITTEST_BTC-8m.json
vendored
Normal file
BIN
tests/testdata/UNITTEST_BTC-8m.json.gz
vendored
Normal file
BIN
tests/testdata/UNITTEST_BTC-8m.json.gz
vendored
Normal file
Binary file not shown.
1
tests/testdata/XLM_BTC-1m.json
vendored
Normal file
1
tests/testdata/XLM_BTC-1m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/XLM_BTC-5m.json
vendored
Normal file
1
tests/testdata/XLM_BTC-5m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/XMR_BTC-1m.json
vendored
Normal file
1
tests/testdata/XMR_BTC-1m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/XMR_BTC-5m.json
vendored
Normal file
1
tests/testdata/XMR_BTC-5m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/ZEC_BTC-1m.json
vendored
Normal file
1
tests/testdata/ZEC_BTC-1m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/ZEC_BTC-5m.json
vendored
Normal file
1
tests/testdata/ZEC_BTC-5m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
tests/testdata/backtest-result_test.json
vendored
Normal file
1
tests/testdata/backtest-result_test.json
vendored
Normal file
File diff suppressed because one or more lines are too long
26
tests/testdata/pairs.json
vendored
Normal file
26
tests/testdata/pairs.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
[
|
||||
"ADA/BTC",
|
||||
"BAT/BTC",
|
||||
"DASH/BTC",
|
||||
"ETC/BTC",
|
||||
"ETH/BTC",
|
||||
"GBYTE/BTC",
|
||||
"LSK/BTC",
|
||||
"LTC/BTC",
|
||||
"NEO/BTC",
|
||||
"NXT/BTC",
|
||||
"POWR/BTC",
|
||||
"STORJ/BTC",
|
||||
"QTUM/BTC",
|
||||
"WAVES/BTC",
|
||||
"VTC/BTC",
|
||||
"XLM/BTC",
|
||||
"XMR/BTC",
|
||||
"XVG/BTC",
|
||||
"XRP/BTC",
|
||||
"ZEC/BTC",
|
||||
"BTC/USDT",
|
||||
"LTC/USDT",
|
||||
"ETH/USDT"
|
||||
]
|
||||
|
Reference in New Issue
Block a user