Merge branch 'develop' into data_handler

This commit is contained in:
Matthias
2020-02-16 15:12:14 +01:00
70 changed files with 1831 additions and 398 deletions

View File

@@ -0,0 +1,116 @@
from pathlib import Path
from unittest.mock import MagicMock
import pytest
import rapidjson
from freqtrade.commands.build_config_commands import (ask_user_config,
ask_user_overwrite,
start_new_config,
validate_is_float,
validate_is_int)
from freqtrade.exceptions import OperationalException
from tests.conftest import get_args, log_has_re
def test_validate_is_float():
assert validate_is_float('2.0')
assert validate_is_float('2.1')
assert validate_is_float('0.1')
assert validate_is_float('-0.5')
assert not validate_is_float('-0.5e')
def test_validate_is_int():
assert validate_is_int('2')
assert validate_is_int('6')
assert validate_is_int('-1')
assert validate_is_int('500')
assert not validate_is_int('2.0')
assert not validate_is_int('2.1')
assert not validate_is_int('-2.1')
assert not validate_is_int('-ee')
@pytest.mark.parametrize('exchange', ['bittrex', 'binance', 'kraken', 'ftx'])
def test_start_new_config(mocker, caplog, exchange):
wt_mock = mocker.patch.object(Path, "write_text", MagicMock())
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
unlink_mock = mocker.patch.object(Path, "unlink", MagicMock())
mocker.patch('freqtrade.commands.build_config_commands.ask_user_overwrite', return_value=True)
sample_selections = {
'max_open_trades': 3,
'stake_currency': 'USDT',
'stake_amount': 100,
'fiat_display_currency': 'EUR',
'ticker_interval': '15m',
'dry_run': True,
'exchange_name': exchange,
'exchange_key': 'sampleKey',
'exchange_secret': 'Samplesecret',
'telegram': False,
'telegram_token': 'asdf1244',
'telegram_chat_id': '1144444',
}
mocker.patch('freqtrade.commands.build_config_commands.ask_user_config',
return_value=sample_selections)
args = [
"new-config",
"--config",
"coolconfig.json"
]
start_new_config(get_args(args))
assert log_has_re("Writing config to .*", caplog)
assert wt_mock.call_count == 1
assert unlink_mock.call_count == 1
result = rapidjson.loads(wt_mock.call_args_list[0][0][0],
parse_mode=rapidjson.PM_COMMENTS | rapidjson.PM_TRAILING_COMMAS)
assert result['exchange']['name'] == exchange
assert result['ticker_interval'] == '15m'
def test_start_new_config_exists(mocker, caplog):
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
mocker.patch('freqtrade.commands.build_config_commands.ask_user_overwrite', return_value=False)
args = [
"new-config",
"--config",
"coolconfig.json"
]
with pytest.raises(OperationalException, match=r"Configuration .* already exists\."):
start_new_config(get_args(args))
def test_ask_user_overwrite(mocker):
"""
Once https://github.com/tmbo/questionary/issues/35 is implemented, improve this test.
"""
prompt_mock = mocker.patch('freqtrade.commands.build_config_commands.prompt',
return_value={'overwrite': False})
assert not ask_user_overwrite(Path('test.json'))
assert prompt_mock.call_count == 1
prompt_mock.reset_mock()
prompt_mock = mocker.patch('freqtrade.commands.build_config_commands.prompt',
return_value={'overwrite': True})
assert ask_user_overwrite(Path('test.json'))
assert prompt_mock.call_count == 1
def test_ask_user_config(mocker):
"""
Once https://github.com/tmbo/questionary/issues/35 is implemented, improve this test.
"""
prompt_mock = mocker.patch('freqtrade.commands.build_config_commands.prompt',
return_value={'overwrite': False})
answers = ask_user_config()
assert isinstance(answers, dict)
assert prompt_mock.call_count == 1
prompt_mock = mocker.patch('freqtrade.commands.build_config_commands.prompt',
return_value={})
with pytest.raises(OperationalException, match=r"User interrupted interactive questions\."):
ask_user_config()

View File

@@ -778,6 +778,121 @@ def test_hyperopt_list(mocker, capsys, hyperopt_results):
assert all(x not in captured.out
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
" 11/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--no-color",
"--min-trades", "20"
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 3/12", " 6/12", " 7/12", " 9/12", " 11/12"])
assert all(x not in captured.out
for x in [" 1/12", " 2/12", " 4/12", " 5/12", " 8/12", " 10/12", " 12/12"])
args = [
"hyperopt-list",
"--profitable",
"--no-details",
"--max-trades", "20"
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 2/12", " 10/12"])
assert all(x not in captured.out
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
" 11/12", " 12/12"])
args = [
"hyperopt-list",
"--profitable",
"--no-details",
"--min-avg-profit", "0.11"
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 2/12"])
assert all(x not in captured.out
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
" 10/12", " 11/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--max-avg-profit", "0.10"
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 1/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12",
" 11/12"])
assert all(x not in captured.out
for x in [" 2/12", " 4/12", " 10/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--min-total-profit", "0.4"
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 10/12"])
assert all(x not in captured.out
for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12",
" 9/12", " 11/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--max-total-profit", "0.4"
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12",
" 9/12", " 11/12"])
assert all(x not in captured.out
for x in [" 4/12", " 10/12", " 12/12"])
args = [
"hyperopt-list",
"--profitable",
"--no-details",
"--min-avg-time", "2000"
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 10/12"])
assert all(x not in captured.out
for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12",
" 8/12", " 9/12", " 11/12", " 12/12"])
args = [
"hyperopt-list",
"--no-details",
"--max-avg-time", "1500"
]
pargs = get_args(args)
pargs['config'] = None
start_hyperopt_list(pargs)
captured = capsys.readouterr()
assert all(x in captured.out
for x in [" 2/12", " 6/12"])
assert all(x not in captured.out
for x in [" 1/12", " 3/12", " 4/12", " 5/12", " 7/12", " 8/12"
" 9/12", " 10/12", " 11/12", " 12/12"])
def test_hyperopt_show(mocker, capsys, hyperopt_results):

View File

@@ -24,7 +24,7 @@ from freqtrade.data.history.jsondatahandler import (JsonDataHandler,
JsonGzDataHandler)
from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import file_dump_json
from freqtrade.strategy.default_strategy import DefaultStrategy
from freqtrade.resolvers import StrategyResolver
from tests.conftest import (get_patched_exchange, log_has, log_has_re,
patch_exchange)
@@ -352,7 +352,7 @@ def test_load_partial_missing(testdatadir, caplog) -> None:
def test_init(default_conf, mocker) -> None:
assert {} == load_data(
datadir='',
datadir=Path(''),
pairs=[],
timeframe=default_conf['ticker_interval']
)
@@ -361,13 +361,13 @@ def test_init(default_conf, mocker) -> None:
def test_init_with_refresh(default_conf, mocker) -> None:
exchange = get_patched_exchange(mocker, default_conf)
refresh_data(
datadir='',
datadir=Path(''),
pairs=[],
timeframe=default_conf['ticker_interval'],
exchange=exchange
)
assert {} == load_data(
datadir='',
datadir=Path(''),
pairs=[],
timeframe=default_conf['ticker_interval']
)
@@ -399,7 +399,9 @@ def test_file_dump_json_tofile(testdatadir) -> None:
def test_get_timerange(default_conf, mocker, testdatadir) -> None:
patch_exchange(mocker)
strategy = DefaultStrategy(default_conf)
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf)
data = strategy.tickerdata_to_dataframe(
load_data(
@@ -415,7 +417,9 @@ def test_get_timerange(default_conf, mocker, testdatadir) -> None:
def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) -> None:
patch_exchange(mocker)
strategy = DefaultStrategy(default_conf)
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf)
data = strategy.tickerdata_to_dataframe(
load_data(
@@ -437,7 +441,9 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir)
def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> None:
patch_exchange(mocker)
strategy = DefaultStrategy(default_conf)
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf)
timerange = TimeRange('index', 'index', 200, 250)
data = strategy.tickerdata_to_dataframe(

View File

@@ -1,4 +1,4 @@
from typing import Dict, List, NamedTuple
from typing import Dict, List, NamedTuple, Optional
import arrow
from pandas import DataFrame
@@ -23,14 +23,14 @@ class BTContainer(NamedTuple):
"""
Minimal BacktestContainer defining Backtest inputs and results.
"""
data: List[float]
data: List[List[float]]
stop_loss: float
roi: Dict[str, 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: Optional[float] = None
trailing_stop_positive_offset: float = 0.0
use_sell_signal: bool = False

View File

@@ -364,7 +364,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
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:
if data.trailing_stop_positive is not None:
default_conf["trailing_stop_positive"] = data.trailing_stop_positive
default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset
default_conf["ask_strategy"] = {"use_sell_signal": data.use_sell_signal}

View File

@@ -19,8 +19,8 @@ from freqtrade.data.dataprovider import DataProvider
from freqtrade.data.history import get_timerange
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.optimize.backtesting import Backtesting
from freqtrade.resolvers import StrategyResolver
from freqtrade.state import RunMode
from freqtrade.strategy.default_strategy import DefaultStrategy
from freqtrade.strategy.interface import SellType
from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
patched_configuration_load_config_file)
@@ -257,8 +257,8 @@ def test_start(mocker, fee, default_conf, caplog) -> None:
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
]
args = get_args(args)
start_backtesting(args)
pargs = get_args(args)
start_backtesting(pargs)
assert log_has('Starting freqtrade in Backtesting mode', caplog)
assert start_mock.call_count == 1
@@ -317,7 +317,9 @@ def test_tickerdata_to_dataframe_bt(default_conf, mocker, testdatadir) -> None:
assert len(data['UNITTEST/BTC']) == 102
# Load strategy to compare the result between Backtesting function and strategy are the same
strategy = DefaultStrategy(default_conf)
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf)
data2 = strategy.tickerdata_to_dataframe(tickerlist)
assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC'])

View File

@@ -82,8 +82,8 @@ def test_start(mocker, fee, edge_conf, caplog) -> None:
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
]
args = get_args(args)
start_edge(args)
pargs = get_args(args)
start_edge(pargs)
assert log_has('Starting freqtrade in Edge mode', caplog)
assert start_mock.call_count == 1

View File

@@ -2,6 +2,7 @@
import locale
from datetime import datetime
from pathlib import Path
from typing import Dict, List
from unittest.mock import MagicMock, PropertyMock
import pandas as pd
@@ -54,7 +55,7 @@ def hyperopt_results():
# Functions for recurrent object patching
def create_trials(mocker, hyperopt, testdatadir) -> None:
def create_trials(mocker, hyperopt, testdatadir) -> List[Dict]:
"""
When creating trials, mock the hyperopt Trials so that *by default*
- we don't create any pickle'd files in the filesystem
@@ -228,10 +229,10 @@ def test_start_not_installed(mocker, default_conf, caplog, import_fails) -> None
'--hyperopt', 'DefaultHyperOpt',
'--epochs', '5'
]
args = get_args(args)
pargs = get_args(args)
with pytest.raises(OperationalException, match=r"Please ensure that the hyperopt dependencies"):
start_hyperopt(args)
start_hyperopt(pargs)
def test_start(mocker, default_conf, caplog) -> None:
@@ -246,8 +247,8 @@ def test_start(mocker, default_conf, caplog) -> None:
'--hyperopt', 'DefaultHyperOpt',
'--epochs', '5'
]
args = get_args(args)
start_hyperopt(args)
pargs = get_args(args)
start_hyperopt(pargs)
assert log_has('Starting freqtrade in Hyperopt mode', caplog)
assert start_mock.call_count == 1
@@ -269,9 +270,9 @@ def test_start_no_data(mocker, default_conf, caplog) -> None:
'--hyperopt', 'DefaultHyperOpt',
'--epochs', '5'
]
args = get_args(args)
pargs = get_args(args)
with pytest.raises(OperationalException, match='No data found. Terminating.'):
start_hyperopt(args)
start_hyperopt(pargs)
def test_start_filelock(mocker, default_conf, caplog) -> None:
@@ -286,16 +287,19 @@ def test_start_filelock(mocker, default_conf, caplog) -> None:
'--hyperopt', 'DefaultHyperOpt',
'--epochs', '5'
]
args = get_args(args)
start_hyperopt(args)
pargs = get_args(args)
start_hyperopt(pargs)
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.load_hyperoptloss(default_conf)
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)
correct = hl.hyperopt_loss_function(hyperopt_results, 600,
datetime(2019, 1, 1), datetime(2019, 5, 1))
over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100,
datetime(2019, 1, 1), datetime(2019, 5, 1))
under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100,
datetime(2019, 1, 1), datetime(2019, 5, 1))
assert over > correct
assert under > correct
@@ -305,8 +309,10 @@ def test_loss_calculation_prefer_shorter_trades(default_conf, hyperopt_results)
resultsb.loc[1, 'trade_duration'] = 20
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
longer = hl.hyperopt_loss_function(hyperopt_results, 100)
shorter = hl.hyperopt_loss_function(resultsb, 100)
longer = hl.hyperopt_loss_function(hyperopt_results, 100,
datetime(2019, 1, 1), datetime(2019, 5, 1))
shorter = hl.hyperopt_loss_function(resultsb, 100,
datetime(2019, 1, 1), datetime(2019, 5, 1))
assert shorter < longer
@@ -317,9 +323,12 @@ def test_loss_calculation_has_limited_profit(default_conf, hyperopt_results) ->
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
correct = hl.hyperopt_loss_function(hyperopt_results, 600)
over = hl.hyperopt_loss_function(results_over, 600)
under = hl.hyperopt_loss_function(results_under, 600)
correct = hl.hyperopt_loss_function(hyperopt_results, 600,
datetime(2019, 1, 1), datetime(2019, 5, 1))
over = hl.hyperopt_loss_function(results_over, 600,
datetime(2019, 1, 1), datetime(2019, 5, 1))
under = hl.hyperopt_loss_function(results_under, 600,
datetime(2019, 1, 1), datetime(2019, 5, 1))
assert over < correct
assert under > correct

View File

@@ -122,7 +122,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
assert "Since" in headers
assert "Pair" in headers
assert 'instantly' == result[0][2]
assert 'ETH/BTC' == result[0][1]
assert 'ETH/BTC' in result[0][1]
assert '-0.59%' == result[0][3]
# Test with fiatconvert
@@ -131,7 +131,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
assert "Since" in headers
assert "Pair" in headers
assert 'instantly' == result[0][2]
assert 'ETH/BTC' == result[0][1]
assert 'ETH/BTC' in result[0][1]
assert '-0.59% (-0.09)' == result[0][3]
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
@@ -140,7 +140,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
rpc._freqtrade.exchange._cached_ticker = {}
result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
assert 'instantly' == result[0][2]
assert 'ETH/BTC' == result[0][1]
assert 'ETH/BTC' in result[0][1]
assert 'nan%' == result[0][3]

View File

@@ -284,7 +284,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
fields = re.sub('[ ]+', ' ', line[2].strip()).split(' ')
assert int(fields[0]) == 1
assert fields[1] == 'ETH/BTC'
assert 'ETH/BTC' in fields[1]
assert msg_mock.call_count == 1
@@ -1200,12 +1200,35 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None:
'stake_amount': 0.001,
'stake_amount_fiat': 0.0,
'stake_currency': 'BTC',
'fiat_currency': 'USD'
'fiat_currency': 'USD',
'current_rate': 1.099e-05,
'amount': 1333.3333333333335,
'open_date': arrow.utcnow().shift(hours=-1)
})
assert msg_mock.call_args[0][0] \
== '*Bittrex:* Buying ETH/BTC\n' \
'at rate `0.00001099\n' \
'(0.001000 BTC,0.000 USD)`'
'*Amount:* `1333.33333333`\n' \
'*Open Rate:* `0.00001099`\n' \
'*Current Rate:* `0.00001099`\n' \
'*Total:* `(0.001000 BTC, 0.000 USD)`'
def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None:
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
telegram.send_msg({
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
'exchange': 'Bittrex',
'pair': 'ETH/BTC',
})
assert msg_mock.call_args[0][0] \
== ('*Bittrex:* Cancelling Open Buy Order for ETH/BTC')
def test_send_msg_sell_notification(default_conf, mocker) -> None:
@@ -1239,13 +1262,13 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
})
assert msg_mock.call_args[0][0] \
== ('*Binance:* Selling KEY/ETH\n'
'*Rate:* `0.00003201`\n'
'*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n'
'*Close Rate:* `0.00003201`\n'
'*Sell Reason:* `stop_loss`\n'
'*Duration:* `1:00:00 (60.0 min)`\n'
'*Profit:* `-57.41%`` (loss: -0.05746268 ETH`` / -24.812 USD)`')
'*Profit:* `-57.41%` `(loss: -0.05746268 ETH / -24.812 USD)`')
msg_mock.reset_mock()
telegram.send_msg({
@@ -1267,10 +1290,10 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
})
assert msg_mock.call_args[0][0] \
== ('*Binance:* Selling KEY/ETH\n'
'*Rate:* `0.00003201`\n'
'*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n'
'*Close Rate:* `0.00003201`\n'
'*Sell Reason:* `stop_loss`\n'
'*Duration:* `1 day, 2:30:00 (1590.0 min)`\n'
'*Profit:* `-57.41%`')
@@ -1278,6 +1301,37 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
telegram._fiat_converter.convert_amount = old_convamount
def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
old_convamount = telegram._fiat_converter.convert_amount
telegram._fiat_converter.convert_amount = lambda a, b, c: -24.812
telegram.send_msg({
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
'exchange': 'Binance',
'pair': 'KEY/ETH',
})
assert msg_mock.call_args[0][0] \
== ('*Binance:* Cancelling Open Sell Order for KEY/ETH')
msg_mock.reset_mock()
telegram.send_msg({
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
'exchange': 'Binance',
'pair': 'KEY/ETH',
})
assert msg_mock.call_args[0][0] \
== ('*Binance:* Cancelling Open Sell Order for KEY/ETH')
# Reset singleton function to avoid random breaks
telegram._fiat_converter.convert_amount = old_convamount
def test_send_msg_status_notification(default_conf, mocker) -> None:
msg_mock = MagicMock()
mocker.patch.multiple(
@@ -1360,12 +1414,17 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
'stake_amount': 0.001,
'stake_amount_fiat': 0.0,
'stake_currency': 'BTC',
'fiat_currency': None
'fiat_currency': None,
'current_rate': 1.099e-05,
'amount': 1333.3333333333335,
'open_date': arrow.utcnow().shift(hours=-1)
})
assert msg_mock.call_args[0][0] \
== '*Bittrex:* Buying ETH/BTC\n' \
'at rate `0.00001099\n' \
'(0.001000 BTC)`'
'*Amount:* `1333.33333333`\n' \
'*Open Rate:* `0.00001099`\n' \
'*Current Rate:* `0.00001099`\n' \
'*Total:* `(0.001000 BTC)`'
def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
@@ -1398,10 +1457,10 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
})
assert msg_mock.call_args[0][0] \
== '*Binance:* Selling KEY/ETH\n' \
'*Rate:* `0.00003201`\n' \
'*Amount:* `1333.33333333`\n' \
'*Open Rate:* `0.00007500`\n' \
'*Current Rate:* `0.00003201`\n' \
'*Close Rate:* `0.00003201`\n' \
'*Sell Reason:* `stop_loss`\n' \
'*Duration:* `2:35:03 (155.1 min)`\n' \
'*Profit:* `-57.41%`'

View File

@@ -13,24 +13,34 @@ from 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": ""
}
}
"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}"
},
"webhookbuycancel": {
"value1": "Cancelling Open Buy Order for {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}"
},
"webhooksellcancel": {
"value1": "Cancelling Open Sell Order for {pair}",
"value2": "limit {limit:8f}",
"value3": "profit: {profit_amount:8f} {stake_currency}"
},
"webhookstatus": {
"value1": "Status: {status}",
"value2": "",
"value3": ""
}
}
def test__init__(mocker, default_conf):
@@ -44,6 +54,9 @@ def test_send_msg(default_conf, mocker):
msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
# Test buy
msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
msg = {
'type': RPCMessageType.BUY_NOTIFICATION,
'exchange': 'Bittrex',
@@ -54,8 +67,6 @@ def test_send_msg(default_conf, mocker):
'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"] ==
@@ -64,6 +75,27 @@ def test_send_msg(default_conf, mocker):
default_conf["webhook"]["webhookbuy"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookbuy"]["value3"].format(**msg))
# Test buy cancel
msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
msg = {
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
'exchange': 'Bittrex',
'pair': 'ETH/BTC',
'limit': 0.005,
'stake_amount': 0.8,
'stake_amount_fiat': 500,
'stake_currency': 'BTC',
'fiat_currency': 'EUR'
}
webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhookbuycancel"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhookbuycancel"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookbuycancel"]["value3"].format(**msg))
# Test sell
msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
@@ -90,7 +122,32 @@ def test_send_msg(default_conf, mocker):
default_conf["webhook"]["webhooksell"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhooksell"]["value3"].format(**msg))
# Test sell cancel
msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
msg = {
'type': RPCMessageType.SELL_CANCEL_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"]["webhooksellcancel"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhooksellcancel"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhooksellcancel"]["value3"].format(**msg))
for msgtype in [RPCMessageType.STATUS_NOTIFICATION,
RPCMessageType.WARNING_NOTIFICATION,
RPCMessageType.CUSTOM_NOTIFICATION]:

View File

@@ -0,0 +1,9 @@
# The strategy which fails to load due to non-existent dependency
import nonexiting_module # noqa
from freqtrade.strategy.interface import IStrategy
class TestStrategyLegacy(IStrategy):
pass

View File

@@ -9,8 +9,9 @@ from pandas import DataFrame
from freqtrade.configuration import TimeRange
from freqtrade.data.history import load_data
from freqtrade.persistence import Trade
from tests.conftest import get_patched_exchange, log_has
from freqtrade.resolvers import StrategyResolver
from freqtrade.strategy.default_strategy import DefaultStrategy
from tests.conftest import get_patched_exchange, log_has
# Avoid to reinit the same object again and again
_STRATEGY = DefaultStrategy(config={})
@@ -103,7 +104,8 @@ def test_get_signal_handles_exceptions(mocker, default_conf):
def test_tickerdata_to_dataframe(default_conf, testdatadir) -> None:
strategy = DefaultStrategy(default_conf)
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf)
timerange = TimeRange.parse_timerange('1510694220-1510700340')
tickerlist = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange,
@@ -118,7 +120,8 @@ def test_min_roi_reached(default_conf, fee) -> None:
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)
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf)
strategy.minimal_roi = roi
trade = Trade(
pair='ETH/BTC',
@@ -156,7 +159,8 @@ def test_min_roi_reached2(default_conf, fee) -> None:
},
]
for roi in min_roi_list:
strategy = DefaultStrategy(default_conf)
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf)
strategy.minimal_roi = roi
trade = Trade(
pair='ETH/BTC',
@@ -190,7 +194,8 @@ def test_min_roi_reached3(default_conf, fee) -> None:
30: 0.05,
55: 0.30,
}
strategy = DefaultStrategy(default_conf)
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf)
strategy.minimal_roi = min_roi
trade = Trade(
pair='ETH/BTC',
@@ -290,7 +295,8 @@ def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) -
def test_is_pair_locked(default_conf):
strategy = DefaultStrategy(default_conf)
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf)
# dict should be empty
assert not strategy._pair_locked_until

View File

@@ -30,14 +30,25 @@ def test_search_strategy():
assert s is None
def test_search_all_strategies():
def test_search_all_strategies_no_failed():
directory = Path(__file__).parent
strategies = StrategyResolver.search_all_objects(directory)
strategies = StrategyResolver.search_all_objects(directory, enum_failed=False)
assert isinstance(strategies, list)
assert len(strategies) == 3
assert isinstance(strategies[0], dict)
def test_search_all_strategies_with_failed():
directory = Path(__file__).parent
strategies = StrategyResolver.search_all_objects(directory, enum_failed=True)
assert isinstance(strategies, list)
assert len(strategies) == 4
# with enum_failed=True search_all_objects() shall find 3 good strategies
# and 1 which fails to load
assert len([x for x in strategies if x['class'] is not None]) == 3
assert len([x for x in strategies if x['class'] is None]) == 1
def test_load_strategy(default_conf, result):
default_conf.update({'strategy': 'SampleStrategy',
'strategy_path': str(Path(__file__).parents[2] / 'freqtrade/templates')

View File

@@ -18,7 +18,8 @@ def test_parse_args_none() -> None:
assert isinstance(arguments.parser, argparse.ArgumentParser)
def test_parse_args_defaults() -> None:
def test_parse_args_defaults(mocker) -> None:
mocker.patch.object(Path, "is_file", MagicMock(side_effect=[False, True]))
args = Arguments(['trade']).get_parsed_arg()
assert args["config"] == ['config.json']
assert args["strategy_path"] is None
@@ -26,6 +27,26 @@ def test_parse_args_defaults() -> None:
assert args["verbosity"] == 0
def test_parse_args_default_userdatadir(mocker) -> None:
mocker.patch.object(Path, "is_file", MagicMock(return_value=True))
args = Arguments(['trade']).get_parsed_arg()
# configuration defaults to user_data if that is available.
assert args["config"] == [str(Path('user_data/config.json'))]
assert args["strategy_path"] is None
assert args["datadir"] is None
assert args["verbosity"] == 0
def test_parse_args_userdatadir(mocker) -> None:
mocker.patch.object(Path, "is_file", MagicMock(return_value=True))
args = Arguments(['trade', '--user-data-dir', 'user_data']).get_parsed_arg()
# configuration defaults to user_data if that is available.
assert args["config"] == [str(Path('user_data/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(['trade', '-c', '/dev/null']).get_parsed_arg()
assert args["config"] == ['/dev/null']
@@ -208,7 +229,7 @@ def test_config_notrequired(mocker) -> None:
assert pargs["config"] is None
# When file exists:
mocker.patch.object(Path, "is_file", MagicMock(return_value=True))
mocker.patch.object(Path, "is_file", MagicMock(side_effect=[False, True]))
args = [
'download-data',
]

View File

@@ -300,7 +300,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf
# stoploss shoud be hit
assert freqtrade.handle_trade(trade) is True
assert log_has('executed sell, reason: SellType.STOP_LOSS', caplog)
assert log_has('Executing Sell for NEO/BTC. Reason: SellType.STOP_LOSS', caplog)
assert trade.sell_reason == SellType.STOP_LOSS.value
@@ -921,7 +921,7 @@ def test_get_buy_rate(mocker, default_conf, ask, last, last_ab, expected) -> Non
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
MagicMock(return_value={'ask': ask, 'last': last}))
assert freqtrade.get_buy_rate('ETH/BTC') == expected
assert freqtrade.get_buy_rate('ETH/BTC', True) == expected
def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None:
@@ -1964,7 +1964,7 @@ def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, o
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
nb_trades = len(trades)
assert nb_trades == 0
assert log_has_re("Buy order canceled on Exchange for Trade.*", caplog)
assert log_has_re("Buy order cancelled on exchange for Trade.*", caplog)
def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_order_old, open_trade,
@@ -2045,7 +2045,7 @@ def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old,
assert cancel_order_mock.call_count == 0
assert rpc_mock.call_count == 1
assert open_trade.is_open is True
assert log_has_re("Sell order canceled on exchange for Trade.*", caplog)
assert log_has_re("Sell order cancelled on exchange for Trade.*", caplog)
def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial,
@@ -2067,7 +2067,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old
# note this is for a partially-complete buy order
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1
assert rpc_mock.call_count == 2
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
assert len(trades) == 1
assert trades[0].amount == 23.0
@@ -2101,7 +2101,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap
assert log_has_re(r"Applying fee on amount for Trade.* Order", caplog)
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1
assert rpc_mock.call_count == 2
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
assert len(trades) == 1
# Verify that tradehas been updated
@@ -2140,7 +2140,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade,
assert log_has_re(r"Could not update trade amount: .*", caplog)
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1
assert rpc_mock.call_count == 2
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
assert len(trades) == 1
# Verify that tradehas been updated
@@ -3524,7 +3524,7 @@ def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2) -> None:
default_conf['telegram']['enabled'] = False
freqtrade = FreqtradeBot(default_conf)
assert freqtrade.get_buy_rate('ETH/BTC') == 0.043935
assert freqtrade.get_buy_rate('ETH/BTC', True) == 0.043935
assert ticker_mock.call_count == 0
@@ -3549,7 +3549,7 @@ def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2) -> None:
freqtrade = FreqtradeBot(default_conf)
# orderbook shall be used even if tickers would be lower.
assert freqtrade.get_buy_rate('ETH/BTC') != 0.042
assert freqtrade.get_buy_rate('ETH/BTC', True) != 0.042
assert ticker_mock.call_count == 0

View File

@@ -5,8 +5,9 @@ from unittest.mock import MagicMock, PropertyMock
import pytest
from pathlib import Path
from freqtrade.commands import Arguments
from freqtrade.exceptions import OperationalException, FreqtradeException
from freqtrade.exceptions import FreqtradeException, OperationalException
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.main import main
from freqtrade.state import State
@@ -26,6 +27,7 @@ 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
"""
mocker.patch.object(Path, "is_file", MagicMock(side_effect=[False, True]))
backtesting_mock = mocker.patch('freqtrade.commands.start_backtesting')
backtesting_mock.__name__ = PropertyMock("start_backtesting")
# it's sys.exit(0) at the end of backtesting
@@ -42,6 +44,7 @@ def test_parse_args_backtesting(mocker) -> None:
def test_main_start_hyperopt(mocker) -> None:
mocker.patch.object(Path, "is_file", MagicMock(side_effect=[False, True]))
hyperopt_mock = mocker.patch('freqtrade.commands.start_hyperopt', MagicMock())
hyperopt_mock.__name__ = PropertyMock("start_hyperopt")
# it's sys.exit(0) at the end of hyperopt

View File

@@ -19,7 +19,7 @@ from freqtrade.plot.plotting import (add_indicators, add_profit,
generate_profit_graph, init_plotscript,
load_and_plot_trades, plot_profit,
plot_trades, store_plot_file)
from freqtrade.strategy.default_strategy import DefaultStrategy
from freqtrade.resolvers import StrategyResolver
from tests.conftest import get_args, log_has, log_has_re
@@ -70,9 +70,11 @@ def test_add_indicators(default_conf, testdatadir, caplog):
indicators1 = {"ema10": {}}
indicators2 = {"macd": {"color": "red"}}
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf)
# Generate buy/sell signals and indicators
strat = DefaultStrategy(default_conf)
data = strat.analyze_ticker(data, {'pair': pair})
data = strategy.analyze_ticker(data, {'pair': pair})
fig = generate_empty_figure()
# Row 1
@@ -181,9 +183,11 @@ def test_generate_candlestick_graph_no_trades(default_conf, mocker, testdatadir)
data = history.load_pair_history(pair=pair, timeframe='1m',
datadir=testdatadir, timerange=timerange)
default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf)
# Generate buy/sell signals and indicators
strat = DefaultStrategy(default_conf)
data = strat.analyze_ticker(data, {'pair': pair})
data = strategy.analyze_ticker(data, {'pair': pair})
indicators1 = []
indicators2 = []