Merge branch 'develop' into fix/validate_dataframe

This commit is contained in:
Matthias
2019-06-24 06:21:08 +02:00
53 changed files with 1433 additions and 735 deletions

View File

@@ -5,6 +5,8 @@ import re
from copy import deepcopy
from datetime import datetime
from functools import reduce
from pathlib import Path
from typing import List
from unittest.mock import MagicMock, PropertyMock
import arrow
@@ -12,6 +14,7 @@ import pytest
from telegram import Chat, Message, Update
from freqtrade import constants, persistence
from freqtrade.arguments import Arguments
from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.edge import Edge, PairInfo
from freqtrade.exchange import Exchange
@@ -36,6 +39,10 @@ def log_has_re(line, logs):
False)
def get_args(args) -> List[str]:
return Arguments(args, '').get_parsed_arg()
def patch_exchange(mocker, api_mock=None, id='bittrex') -> None:
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
@@ -54,7 +61,7 @@ def get_patched_exchange(mocker, config, api_mock=None, id='bittrex') -> Exchang
patch_exchange(mocker, api_mock, id)
config["exchange"]["name"] = id
try:
exchange = ExchangeResolver(id.title(), config).exchange
exchange = ExchangeResolver(id, config).exchange
except ImportError:
exchange = Exchange(config)
return exchange
@@ -104,11 +111,23 @@ def patch_freqtradebot(mocker, config) -> None:
def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
"""
This function patches _init_modules() to not call dependencies
:param mocker: a Mocker object to apply patches
:param config: Config to pass to the bot
:return: FreqtradeBot
"""
patch_freqtradebot(mocker, config)
return FreqtradeBot(config)
def get_patched_worker(mocker, config) -> Worker:
"""
This function patches _init_modules() to not call dependencies
:param mocker: a Mocker object to apply patches
:param config: Config to pass to the bot
:return: Worker
"""
patch_freqtradebot(mocker, config)
return Worker(args=None, config=config)
@@ -145,6 +164,11 @@ def patch_coinmarketcap(mocker) -> None:
)
@pytest.fixture(scope='function')
def init_persistence(default_conf):
persistence.init(default_conf['db_url'], default_conf['dry_run'])
@pytest.fixture(scope="function")
def default_conf():
""" Returns validated configuration suitable for most tests """
@@ -854,9 +878,9 @@ def tickers():
@pytest.fixture
def result():
with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file:
return parse_ticker_dataframe(json.load(data_file), '1m',
pair="UNITTEST/BTC", fill_missing=True)
with Path('freqtrade/tests/testdata/UNITTEST_BTC-1m.json').open('r') as data_file:
return parse_ticker_dataframe(json.load(data_file), '1m', pair="UNITTEST/BTC",
fill_missing=True)
# FIX:
# Create an fixture/function

View File

@@ -1,8 +1,15 @@
import pytest
from pandas import DataFrame
from unittest.mock import MagicMock
from freqtrade.data.btanalysis import BT_DATA_COLUMNS, load_backtest_data
from freqtrade.data.history import make_testdata_path
from arrow import Arrow
import pytest
from pandas import DataFrame, to_datetime
from freqtrade.arguments import TimeRange
from freqtrade.data.btanalysis import (BT_DATA_COLUMNS,
extract_trades_of_period,
load_backtest_data, load_trades)
from freqtrade.data.history import load_pair_history, make_testdata_path
from freqtrade.tests.test_persistence import create_mock_trades
def test_load_backtest_data():
@@ -19,3 +26,59 @@ def test_load_backtest_data():
with pytest.raises(ValueError, match=r"File .* does not exist\."):
load_backtest_data(str("filename") + "nofile")
def test_load_trades_file(default_conf, fee, mocker):
# Real testing of load_backtest_data is done in test_load_backtest_data
lbt = mocker.patch("freqtrade.data.btanalysis.load_backtest_data", MagicMock())
filename = make_testdata_path(None) / "backtest-result_test.json"
load_trades(db_url=None, exportfilename=filename)
assert lbt.call_count == 1
@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(db_url=default_conf['db_url'], exportfilename=None)
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
def test_extract_trades_of_period():
pair = "UNITTEST/BTC"
timerange = TimeRange(None, 'line', 0, -1000)
data = load_pair_history(pair=pair, ticker_interval='1m',
datadir=None, 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

View File

@@ -124,14 +124,14 @@ def test_exchange_resolver(default_conf, mocker, caplog):
caplog.record_tuples)
caplog.clear()
exchange = ExchangeResolver('Kraken', default_conf).exchange
exchange = ExchangeResolver('kraken', default_conf).exchange
assert isinstance(exchange, Exchange)
assert isinstance(exchange, Kraken)
assert not isinstance(exchange, Binance)
assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.",
caplog.record_tuples)
exchange = ExchangeResolver('Binance', default_conf).exchange
exchange = ExchangeResolver('binance', default_conf).exchange
assert isinstance(exchange, Exchange)
assert isinstance(exchange, Binance)
assert not isinstance(exchange, Kraken)

View File

@@ -29,6 +29,10 @@ class BTContainer(NamedTuple):
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):

View File

@@ -14,6 +14,21 @@ from freqtrade.tests.optimize import (BTContainer, BTrade,
_get_frame_time_from_offset,
tests_ticker_interval)
# Test 0 Sell signal sell
# Test with Stop-loss at 1%
# TC0: Sell signal in candle 3
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 Minus 8% Close
# Test with Stop-loss at 1%
# TC1: Stop-Loss Triggered 1% loss
@@ -146,7 +161,7 @@ tc8 = BTContainer(data=[
# Test 9 - trailing_stop should raise - high and low in same candle.
# Candle Data for test 9
# Set stop-loss at 10%, ROI at 10% (should not apply)
# TC9: Trailing stoploss - stoploss should be adjusted candle 2
# TC9: Trailing stoploss - stoploss should be adjusted candle 3
tc9 = BTContainer(data=[
# D O H L C V B S
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
@@ -158,7 +173,59 @@ tc9 = BTContainer(data=[
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%.
# Set stop-loss at 10%, ROI at 10% (should not apply)
# TC10: Trailing stoploss - stoploss should be 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.
# Set stop-loss at 10%, ROI at 10% (should not apply)
# TC11: Trailing stoploss - stoploss should be 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.
# Set stop-loss at 10%, ROI at 10% (should not apply)
# TC12: Trailing stoploss - stoploss should be 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)]
)
TESTS = [
tc0,
tc1,
tc2,
tc3,
@@ -168,6 +235,9 @@ TESTS = [
tc7,
tc8,
tc9,
tc10,
tc11,
tc12,
]
@@ -180,6 +250,13 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
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)

View File

@@ -3,7 +3,6 @@
import json
import math
import random
from typing import List
from unittest.mock import MagicMock
import numpy as np
@@ -12,7 +11,7 @@ import pytest
from arrow import Arrow
from freqtrade import DependencyException, constants
from freqtrade.arguments import Arguments, TimeRange
from freqtrade.arguments import TimeRange
from freqtrade.data import history
from freqtrade.data.btanalysis import evaluate_result_multi
from freqtrade.data.converter import parse_ticker_dataframe
@@ -23,11 +22,7 @@ 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 log_has, log_has_re, patch_exchange
def get_args(args) -> List[str]:
return Arguments(args, '').get_parsed_arg()
from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange
def trim_dictlist(dict_list, num):

View File

@@ -2,19 +2,13 @@
# pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments
import json
from typing import List
from unittest.mock import MagicMock
from freqtrade.arguments import Arguments
from freqtrade.edge import PairInfo
from freqtrade.optimize import start_edge, setup_configuration
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 log_has, log_has_re, patch_exchange
def get_args(args) -> List[str]:
return Arguments(args, '').get_parsed_arg()
from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
@@ -117,8 +111,10 @@ def test_start(mocker, fee, edge_conf, caplog) -> None:
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)

View File

@@ -16,8 +16,7 @@ from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE
from freqtrade.optimize import setup_configuration, start_hyperopt
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver
from freqtrade.state import RunMode
from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange
from freqtrade.tests.optimize.test_backtesting import get_args
from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange
@pytest.fixture(scope='function')
@@ -168,6 +167,7 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
"Using populate_sell_trend from DefaultStrategy.", caplog.record_tuples)
assert log_has("Custom Hyperopt does not provide populate_buy_trend. "
"Using populate_buy_trend from DefaultStrategy.", caplog.record_tuples)
assert hasattr(x, "ticker_interval")
def test_start(mocker, default_conf, caplog) -> None:

View File

@@ -756,6 +756,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
'gain': 'profit',
'limit': 1.172e-05,
'amount': 90.99181073703367,
'order_type': 'limit',
'open_rate': 1.099e-05,
'current_rate': 1.172e-05,
'profit_amount': 6.126e-05,
@@ -810,6 +811,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
'gain': 'loss',
'limit': 1.044e-05,
'amount': 90.99181073703367,
'order_type': 'limit',
'open_rate': 1.099e-05,
'current_rate': 1.044e-05,
'profit_amount': -5.492e-05,
@@ -855,6 +857,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker
'gain': 'loss',
'limit': 1.098e-05,
'amount': 90.99181073703367,
'order_type': 'limit',
'open_rate': 1.099e-05,
'current_rate': 1.098e-05,
'profit_amount': -5.91e-06,
@@ -1188,6 +1191,7 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None:
'exchange': 'Bittrex',
'pair': 'ETH/BTC',
'limit': 1.099e-05,
'order_type': 'limit',
'stake_amount': 0.001,
'stake_amount_fiat': 0.0,
'stake_currency': 'BTC',
@@ -1195,7 +1199,7 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None:
})
assert msg_mock.call_args[0][0] \
== '*Bittrex:* Buying ETH/BTC\n' \
'with limit `0.00001099\n' \
'at rate `0.00001099\n' \
'(0.001000 BTC,0.000 USD)`'
@@ -1217,6 +1221,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'gain': 'loss',
'limit': 3.201e-05,
'amount': 1333.3333333333335,
'order_type': 'market',
'open_rate': 7.5e-05,
'current_rate': 3.201e-05,
'profit_amount': -0.05746268,
@@ -1227,7 +1232,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
})
assert msg_mock.call_args[0][0] \
== ('*Binance:* Selling KEY/ETH\n'
'*Limit:* `0.00003201`\n'
'*Rate:* `0.00003201`\n'
'*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n'
@@ -1242,6 +1247,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'gain': 'loss',
'limit': 3.201e-05,
'amount': 1333.3333333333335,
'order_type': 'market',
'open_rate': 7.5e-05,
'current_rate': 3.201e-05,
'profit_amount': -0.05746268,
@@ -1251,7 +1257,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
})
assert msg_mock.call_args[0][0] \
== ('*Binance:* Selling KEY/ETH\n'
'*Limit:* `0.00003201`\n'
'*Rate:* `0.00003201`\n'
'*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n'
@@ -1339,6 +1345,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
'exchange': 'Bittrex',
'pair': 'ETH/BTC',
'limit': 1.099e-05,
'order_type': 'limit',
'stake_amount': 0.001,
'stake_amount_fiat': 0.0,
'stake_currency': 'BTC',
@@ -1346,7 +1353,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
})
assert msg_mock.call_args[0][0] \
== '*Bittrex:* Buying ETH/BTC\n' \
'with limit `0.00001099\n' \
'at rate `0.00001099\n' \
'(0.001000 BTC)`'
@@ -1367,6 +1374,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
'gain': 'loss',
'limit': 3.201e-05,
'amount': 1333.3333333333335,
'order_type': 'limit',
'open_rate': 7.5e-05,
'current_rate': 3.201e-05,
'profit_amount': -0.05746268,
@@ -1377,7 +1385,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
})
assert msg_mock.call_args[0][0] \
== '*Binance:* Selling KEY/ETH\n' \
'*Limit:* `0.00003201`\n' \
'*Rate:* `0.00003201`\n' \
'*Amount:* `1333.33333333`\n' \
'*Open Rate:* `0.00007500`\n' \
'*Current Rate:* `0.00003201`\n' \

View File

@@ -74,6 +74,7 @@ def test_send_msg(default_conf, mocker):
'gain': "profit",
'limit': 0.005,
'amount': 0.8,
'order_type': 'limit',
'open_rate': 0.004,
'current_rate': 0.005,
'profit_amount': 0.001,
@@ -126,6 +127,7 @@ def test_exception_send_msg(default_conf, mocker, caplog):
'exchange': 'Bittrex',
'pair': 'ETH/BTC',
'limit': 0.005,
'order_type': 'limit',
'stake_amount': 0.8,
'stake_amount_fiat': 500,
'stake_currency': 'BTC',

View File

@@ -63,27 +63,22 @@ def test_search_strategy():
def test_load_strategy(result):
resolver = StrategyResolver({'strategy': 'TestStrategy'})
metadata = {'pair': 'ETH/BTC'}
assert 'adx' in resolver.strategy.advise_indicators(result, metadata=metadata)
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
def test_load_strategy_byte64(result):
with open("freqtrade/tests/strategy/test_strategy.py", "r") as file:
encoded_string = urlsafe_b64encode(file.read().encode("utf-8")).decode("utf-8")
resolver = StrategyResolver({'strategy': 'TestStrategy:{}'.format(encoded_string)})
assert 'adx' in resolver.strategy.advise_indicators(result, 'ETH/BTC')
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
def test_load_strategy_invalid_directory(result, caplog):
resolver = StrategyResolver()
extra_dir = path.join('some', 'path')
extra_dir = Path.cwd() / 'some/path'
resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir)
assert (
'freqtrade.resolvers.strategy_resolver',
logging.WARNING,
'Path "{}" does not exist'.format(extra_dir),
) in caplog.record_tuples
assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog.record_tuples)
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
@@ -371,7 +366,7 @@ def test_deprecate_populate_indicators(result):
with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered.
warnings.simplefilter("always")
indicators = resolver.strategy.advise_indicators(result, 'ETH/BTC')
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!" \
@@ -380,7 +375,7 @@ def test_deprecate_populate_indicators(result):
with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered.
warnings.simplefilter("always")
resolver.strategy.advise_buy(indicators, 'ETH/BTC')
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!" \
@@ -389,7 +384,7 @@ def test_deprecate_populate_indicators(result):
with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered.
warnings.simplefilter("always")
resolver.strategy.advise_sell(indicators, 'ETH_BTC')
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!" \

View File

@@ -47,9 +47,9 @@ def test_parse_args_verbose() -> None:
assert args.loglevel == 1
def test_scripts_options() -> None:
def test_common_scripts_options() -> None:
arguments = Arguments(['-p', 'ETH/BTC'], '')
arguments.scripts_options()
arguments.common_scripts_options()
args = arguments.get_parsed_arg()
assert args.pairs == 'ETH/BTC'
@@ -170,22 +170,40 @@ def test_parse_args_hyperopt_custom() -> None:
assert call_args.func is not None
def test_testdata_dl_options() -> None:
def test_download_data_options() -> None:
args = [
'--pairs-file', 'file_with_pairs',
'--export', 'export/folder',
'--datadir', 'datadir/folder',
'--days', '30',
'--exchange', 'binance'
]
arguments = Arguments(args, '')
arguments.testdata_dl_options()
arguments.common_options()
arguments.download_data_options()
args = arguments.parse_args()
assert args.pairs_file == 'file_with_pairs'
assert args.export == 'export/folder'
assert args.datadir == 'datadir/folder'
assert args.days == 30
assert args.exchange == 'binance'
def test_plot_dataframe_options() -> None:
args = [
'--indicators1', 'sma10,sma100',
'--indicators2', 'macd,fastd,fastk',
'--plot-limit', '30',
'-p', 'UNITTEST/BTC',
]
arguments = Arguments(args, '')
arguments.common_scripts_options()
arguments.plot_dataframe_options()
pargs = arguments.parse_args(True)
assert pargs.indicators1 == "sma10,sma100"
assert pargs.indicators2 == "macd,fastd,fastk"
assert pargs.plot_limit == 30
assert pargs.pairs == "UNITTEST/BTC"
def test_check_int_positive() -> None:
assert Arguments.check_int_positive("3") == 3

View File

@@ -15,7 +15,7 @@ from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration, set_loggers
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
from freqtrade.state import RunMode
from freqtrade.tests.conftest import log_has
from freqtrade.tests.conftest import log_has, log_has_re
@pytest.fixture(scope="function")
@@ -470,21 +470,52 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
def test_check_exchange(default_conf, caplog) -> None:
configuration = Configuration(Namespace())
# Test a valid exchange
# Test an officially supported by Freqtrade team exchange
default_conf.get('exchange').update({'name': 'BITTREX'})
assert configuration.check_exchange(default_conf)
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
caplog.record_tuples)
caplog.clear()
# Test a valid exchange
# Test an officially supported by Freqtrade team exchange
default_conf.get('exchange').update({'name': 'binance'})
assert configuration.check_exchange(default_conf)
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
caplog.record_tuples)
caplog.clear()
# Test a invalid exchange
# Test an available exchange, supported by ccxt
default_conf.get('exchange').update({'name': 'kraken'})
assert configuration.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.record_tuples)
caplog.clear()
# Test a 'bad' exchange, which known to have serious problems
default_conf.get('exchange').update({'name': 'bitmex'})
assert not configuration.check_exchange(default_conf)
assert log_has_re(r"Exchange .* is known to not work with the bot yet\. "
r"Use it only for development and testing purposes\.",
caplog.record_tuples)
caplog.clear()
# Test a 'bad' exchange with check_for_bad=False
default_conf.get('exchange').update({'name': 'bitmex'})
assert configuration.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.record_tuples)
caplog.clear()
# Test an invalid exchange
default_conf.get('exchange').update({'name': 'unknown_exchange'})
configuration.config = default_conf
with pytest.raises(
OperationalException,
match=r'.*Exchange "unknown_exchange" not supported.*'
match=r'.*Exchange "unknown_exchange" is not supported by ccxt '
r'and therefore not available for the bot.*'
):
configuration.check_exchange(default_conf)

View File

@@ -19,47 +19,13 @@ from freqtrade.persistence import Trade
from freqtrade.rpc import RPCMessageType
from freqtrade.state import State
from freqtrade.strategy.interface import SellCheckTuple, SellType
from freqtrade.tests.conftest import (log_has, log_has_re, patch_edge,
patch_exchange, patch_get_signal,
patch_wallet)
from freqtrade.tests.conftest import (get_patched_freqtradebot,
get_patched_worker, log_has, log_has_re,
patch_edge, patch_exchange,
patch_get_signal, patch_wallet)
from freqtrade.worker import Worker
# Functions for recurrent object patching
def patch_freqtradebot(mocker, config) -> None:
"""
This function patches _init_modules() to not call dependencies
:param mocker: a Mocker object to apply patches
:param config: Config to pass to the bot
:return: None
"""
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
patch_exchange(mocker)
def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
"""
This function patches _init_modules() to not call dependencies
:param mocker: a Mocker object to apply patches
:param config: Config to pass to the bot
:return: FreqtradeBot
"""
patch_freqtradebot(mocker, config)
return FreqtradeBot(config)
def get_patched_worker(mocker, config) -> Worker:
"""
This function patches _init_modules() to not call dependencies
:param mocker: a Mocker object to apply patches
:param config: Config to pass to the bot
:return: Worker
"""
patch_freqtradebot(mocker, config)
return Worker(args=None, config=config)
def patch_RPCManager(mocker) -> MagicMock:
"""
This function mock RPC manager to avoid repeating this code in almost every tests
@@ -1176,6 +1142,77 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
stop_price=0.00002344 * 0.95)
def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog,
markets, limit_buy_order,
limit_sell_order) -> None:
# When trailing stoploss is set
stoploss_limit = MagicMock(return_value={'id': 13434334})
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_ticker=MagicMock(return_value={
'bid': 0.00001172,
'ask': 0.00001173,
'last': 0.00001172
}),
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
get_fee=fee,
markets=PropertyMock(return_value=markets),
stoploss_limit=stoploss_limit
)
# enabling TSL
default_conf['trailing_stop'] = True
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# enabling stoploss on exchange
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
# setting stoploss
freqtrade.strategy.stoploss = -0.05
# setting stoploss_on_exchange_interval to 60 seconds
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60
patch_get_signal(freqtrade)
freqtrade.create_trade()
trade = Trade.query.first()
trade.is_open = True
trade.open_order_id = None
trade.stoploss_order_id = "abcd"
trade.stop_loss = 0.2
trade.stoploss_last_update = arrow.utcnow().shift(minutes=-601).datetime.replace(tzinfo=None)
stoploss_order_hanging = {
'id': "abcd",
'status': 'open',
'type': 'stop_loss_limit',
'price': 3,
'average': 2,
'info': {
'stopPrice': '0.1'
}
}
mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException())
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging)
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*",
caplog.record_tuples)
# Still try to create order
assert stoploss_limit.call_count == 1
# Fail creating stoploss order
caplog.clear()
cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_order", MagicMock())
mocker.patch("freqtrade.exchange.Exchange.stoploss_limit", side_effect=DependencyException())
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
assert cancel_mock.call_count == 1
assert log_has_re(r"Could create trailing stoploss order for pair ETH/BTC\..*",
caplog.record_tuples)
def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
markets, limit_buy_order, limit_sell_order) -> None:
@@ -1994,6 +2031,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc
'gain': 'profit',
'limit': 1.172e-05,
'amount': 90.99181073703367,
'order_type': 'limit',
'open_rate': 1.099e-05,
'current_rate': 1.172e-05,
'profit_amount': 6.126e-05,
@@ -2040,6 +2078,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets,
'gain': 'loss',
'limit': 1.044e-05,
'amount': 90.99181073703367,
'order_type': 'limit',
'open_rate': 1.099e-05,
'current_rate': 1.044e-05,
'profit_amount': -5.492e-05,
@@ -2094,6 +2133,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
'gain': 'loss',
'limit': 1.08801e-05,
'amount': 90.99181073703367,
'order_type': 'limit',
'open_rate': 1.099e-05,
'current_rate': 1.044e-05,
'profit_amount': -1.498e-05,
@@ -2105,6 +2145,36 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
} == last_msg
def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee,
markets, caplog) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException())
sellmock = MagicMock()
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
_load_markets=MagicMock(return_value={}),
get_ticker=ticker,
get_fee=fee,
markets=PropertyMock(return_value=markets),
sell=sellmock
)
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
patch_get_signal(freqtrade)
freqtrade.create_trade()
trade = Trade.query.first()
Trade.session = MagicMock()
freqtrade.config['dry_run'] = False
trade.stoploss_order_id = "abcd"
freqtrade.execute_sell(trade=trade, limit=1234,
sell_reason=SellType.STOP_LOSS)
assert sellmock.call_count == 1
assert log_has('Could not cancel stoploss order abcd', caplog.record_tuples)
def test_execute_sell_with_stoploss_on_exchange(default_conf,
ticker, fee, ticker_sell_up,
markets, mocker) -> None:
@@ -2265,6 +2335,7 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee,
'gain': 'profit',
'limit': 1.172e-05,
'amount': 90.99181073703367,
'order_type': 'limit',
'open_rate': 1.099e-05,
'current_rate': 1.172e-05,
'profit_amount': 6.126e-05,
@@ -2312,6 +2383,7 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee,
'gain': 'loss',
'limit': 1.044e-05,
'amount': 90.99181073703367,
'order_type': 'limit',
'open_rate': 1.099e-05,
'current_rate': 1.044e-05,
'profit_amount': -5.492e-05,

View File

@@ -11,9 +11,48 @@ from freqtrade.persistence import Trade, clean_dry_run_db, init
from freqtrade.tests.conftest import log_has
@pytest.fixture(scope='function')
def init_persistence(default_conf):
init(default_conf['db_url'], default_conf['dry_run'])
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):
@@ -671,45 +710,7 @@ def test_adjust_min_max_rates(fee):
@pytest.mark.usefixtures("init_persistence")
def test_get_open(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',
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)
create_mock_trades(fee)
assert len(Trade.get_open_trades()) == 2

View File

@@ -0,0 +1,188 @@
from unittest.mock import MagicMock
from plotly import tools
import plotly.graph_objs as go
from copy import deepcopy
from freqtrade.arguments import TimeRange
from freqtrade.data import history
from freqtrade.data.btanalysis import load_backtest_data
from freqtrade.plot.plotting import (generate_graph, generate_plot_file,
generate_row, plot_trades)
from freqtrade.strategy.default_strategy import DefaultStrategy
from freqtrade.tests.conftest import log_has, log_has_re
def fig_generating_mock(fig, *args, **kwargs):
""" Return Fig - used to mock generate_row and plot_trades"""
return fig
def find_trace_in_fig_data(data, search_string: str):
matches = filter(lambda x: x.name == search_string, data)
return next(matches)
def generage_empty_figure():
return tools.make_subplots(
rows=3,
cols=1,
shared_xaxes=True,
row_width=[1, 1, 4],
vertical_spacing=0.0001,
)
def test_generate_row(default_conf, caplog):
pair = "UNITTEST/BTC"
timerange = TimeRange(None, 'line', 0, -1000)
data = history.load_pair_history(pair=pair, ticker_interval='1m',
datadir=None, 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 = generate_row(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 = generate_row(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 = generate_row(fig=deepcopy(fig), row=3, indicators=['no_indicator'], data=data)
assert fig == fig3
assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog.record_tuples)
def test_plot_trades():
fig1 = generage_empty_figure()
# nothing happens when no trades are available
fig = plot_trades(fig1, None)
assert fig == fig1
pair = "ADA/BTC"
filename = history.make_testdata_path(None) / "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_graph_no_signals_no_trades(default_conf, mocker, caplog):
row_mock = mocker.patch('freqtrade.plot.plotting.generate_row',
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=None, timerange=timerange)
data['buy'] = 0
data['sell'] = 0
indicators1 = []
indicators2 = []
fig = generate_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.record_tuples)
assert log_has("No sell-signals found.", caplog.record_tuples)
def test_generate_graph_no_trades(default_conf, mocker):
row_mock = mocker.patch('freqtrade.plot.plotting.generate_row',
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=None, timerange=timerange)
# Generate buy/sell signals and indicators
strat = DefaultStrategy(default_conf)
data = strat.analyze_ticker(data, {'pair': pair})
indicators1 = []
indicators2 = []
fig = generate_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_file(mocker, caplog):
fig = generage_empty_figure()
plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock())
generate_plot_file(fig, "UNITTEST/BTC", "5m")
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")

View File

@@ -0,0 +1,42 @@
from freqtrade.utils import setup_utils_configuration, start_list_exchanges
from freqtrade.tests.conftest import get_args
from freqtrade.state import RunMode
import re
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)