diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 94d998310..ae5375f43 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -5,6 +5,7 @@ If it hasn't been reported, please create a new issue. ## Step 2: Describe your environment + * Operating system: ____ * Python Version: _____ (`python -V`) * CCXT version: _____ (`pip freeze | grep ccxt`) * Branch: Master | Develop diff --git a/docs/data-analysis.md b/docs/data-analysis.md index cda7a9a57..155dfa8e0 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -6,6 +6,128 @@ A good way for this is using Jupyter (notebook or lab) - which provides an inter The following helpers will help you loading the data into Pandas DataFrames, and may also give you some starting points in analyzing the results. +## Strategy development problem analysis + +Debugging a strategy (are there no buy signals, ...) can be very time-consuming. +FreqTrade tries to help you by exposing a few helper-functions, which can be very handy. + +It's recommended using Juptyer Notebooks for analysis, since it offers a dynamic way to rerun certain parts of the code. + +The following is a full code-snippet, which will be explained by both comments, and step by step below. + +```python +# Some necessary imports +from pathlib import Path + +from freqtrade.data.history import load_pair_history +from freqtrade.resolvers import StrategyResolver +# Define some constants +ticker_interval = "5m" + +# Name of the strategy class +strategyname = 'Awesomestrategy' +# Location of the strategy +strategy_location = '../xmatt/strategies' +# Location of the data +data_location = '../freqtrade/user_data/data/binance/' +# Only use one pair here +pair = "XRP_ETH" + +### End constants + +# Load data +bt_data = load_pair_history(datadir=Path(data_location), + ticker_interval = ticker_interval, + pair=pair) +print(len(bt_data)) + +### Start strategy reload +# Load strategy - best done in a new cell +# Rerun each time the strategy-file is changed. +strategy = StrategyResolver({'strategy': strategyname, + 'user_data_dir': Path.cwd(), + 'strategy_path': location}).strategy + +# Run strategy (just like in backtesting) +df = strategy.analyze_ticker(bt_data, {'pair': pair}) +print(f"Generated {df['buy'].sum()} buy signals") + +# Reindex data to be "nicer" and show data +data = df.set_index('date', drop=True) +data.tail() + +``` + +### Explanation + +#### Imports and constant definition + +``` python +# Some necessary imports +from pathlib import Path + +from freqtrade.data.history import load_pair_history +from freqtrade.resolvers import StrategyResolver +# Define some constants +ticker_interval = "5m" + +# Name of the strategy class +strategyname = 'Awesomestrategy' +# Location of the strategy +strategy_location = 'user_data/strategies' +# Location of the data +data_location = 'user_data/data/binance' +# Only use one pair here +pair = "XRP_ETH" +``` + +This first section imports necessary modules, and defines some constants you'll probably need to adjust for your case. + +#### Load candles + +``` python +# Load data +bt_data = load_pair_history(datadir=Path(data_location), + ticker_interval = ticker_interval, + pair=pair) +print(len(bt_data)) +``` + +This second section loads the historic data and prints the amount of candles in the DataFrame. +You can also inspect this dataframe by using `bt_data.head()` or `bt_data.tail()`. + +#### Run strategy and analyze results + +Now, it's time to load and run your strategy. +For this, I recommend using a new cell in your notebook, since you'll want to repeat this until you're satisfied with your strategy. + +``` python +# Load strategy - best done in a new cell +# Needs to be ran each time the strategy-file is changed. +strategy = StrategyResolver({'strategy': strategyname, + 'user_data_dir': Path.cwd(), + 'strategy_path': location}).strategy + +# Run strategy (just like in backtesting) +df = strategy.analyze_ticker(bt_data, {'pair': pair}) +print(f"Generated {df['buy'].sum()} buy signals") + +# Reindex data to be "nicer" and show data +data = df.set_index('date', drop=True) +data.tail() +``` + +The code snippet loads and analyzes the strategy, calculates and prints the number of buy signals. + +The last 2 lines serve to analyze the dataframe in detail. +This can be important if your strategy did not generate any buy signals. +Note that using `data.head()` would also work, however this is misleading since most indicators have some "startup" time at the start of a backtested dataframe. + +There can be many things wrong, some signs to look for are: + +* Columns with NaN values at the end of the dataframe +* Columns used in `crossed*()` functions with completely different units + ## Backtesting To analyze your backtest results, you can [export the trades](#exporting-trades-to-file). diff --git a/docs/faq.md b/docs/faq.md index 83576af4d..a441ffacd 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -45,6 +45,16 @@ the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-c You can use the `/forcesell all` command from Telegram. +### I get the message "RESTRICTED_MARKET" + +Currently known to happen for US Bittrex users. +Bittrex split its exchange into US and International versions. +The International version has more pairs available, however the API always returns all pairs, so there is currently no automated way to detect if you're affected by the restriction. + +If you have restricted pairs in your whitelist, you'll get a warning message in the log on FreqTrade startup for each restricted pair. +If you're an "International" Customer on the Bittrex exchange, then this warning will probably not impact you. +If you're a US customer, the bot will fail to create orders for these pairs, and you should remove them from your Whitelist. + ## Hyperopt module ### How many epoch do I need to get a good Hyperopt result? diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 2755cae2d..0b5d1a50e 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -303,8 +303,10 @@ Given the following result from hyperopt: ``` Best result: - 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. -with values: + + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + +Buy hyperspace params: { 'adx-value': 44, 'rsi-value': 29, 'adx-enabled': False, @@ -347,21 +349,15 @@ If you are optimizing ROI, you're result will look as follows and include a ROI ``` Best result: - 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. -with values: + + 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367 + +Buy hyperspace params: { 'adx-value': 44, 'rsi-value': 29, - 'adx-enabled': false, + 'adx-enabled': False, 'rsi-enabled': True, - 'trigger': 'bb_lower', - 'roi_t1': 40, - 'roi_t2': 57, - 'roi_t3': 21, - 'roi_p1': 0.03634636907306948, - 'roi_p2': 0.055237357937802885, - 'roi_p3': 0.015163796015548354, - 'stoploss': -0.37996664668703606 -} + 'trigger': 'bb_lower'} ROI table: { 0: 0.10674752302642071, 21: 0.09158372701087236, @@ -372,9 +368,9 @@ ROI table: This would translate to the following ROI table: ``` python - minimal_roi = { +minimal_roi = { "118": 0, - "78": 0.0363463, + "78": 0.0363, "21": 0.0915, "0": 0.106 } diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 5865d56a7..36d8aedbb 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -81,19 +81,30 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: """ trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS) persistence.init(db_url, clean_open_orders=False) - columns = ["pair", "profit", "open_time", "close_time", - "open_rate", "close_rate", "duration", "sell_reason", - "max_rate", "min_rate"] - trades = pd.DataFrame([(t.pair, t.calc_profit(), + columns = ["pair", "open_time", "close_time", "profit", "profitperc", + "open_rate", "close_rate", "amount", "duration", "sell_reason", + "fee_open", "fee_close", "open_rate_requested", "close_rate_requested", + "stake_amount", "max_rate", "min_rate", "id", "exchange", + "stop_loss", "initial_stop_loss", "strategy", "ticker_interval"] + + trades = pd.DataFrame([(t.pair, t.open_date.replace(tzinfo=pytz.UTC), t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None, - t.open_rate, t.close_rate, - t.close_date.timestamp() - t.open_date.timestamp() - if t.close_date else None, + t.calc_profit(), t.calc_profit_percent(), + t.open_rate, t.close_rate, t.amount, + (t.close_date.timestamp() - t.open_date.timestamp() + if t.close_date else None), t.sell_reason, + t.fee_open, t.fee_close, + t.open_rate_requested, + t.close_rate_requested, + t.stake_amount, t.max_rate, t.min_rate, + t.id, t.exchange, + t.stop_loss, t.initial_stop_loss, + t.strategy, t.ticker_interval ) for t in Trade.query.all()], columns=columns) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a7c76e635..37bbb778e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -260,7 +260,7 @@ class Exchange(object): if not self.markets: logger.warning('Unable to validate pairs (assuming they are correct).') - # return + return for pair in pairs: # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs @@ -269,6 +269,12 @@ class Exchange(object): raise OperationalException( f'Pair {pair} is not available on {self.name}. ' f'Please remove {pair} from your whitelist.') + elif self.markets[pair].get('info', {}).get('IsRestricted', False): + # Warn users about restricted pairs in whitelist. + # We cannot determine reliably if Users are affected. + logger.warning(f"Pair {pair} is restricted for some users on this exchange." + f"Please check if you are impacted by this restriction " + f"on the exchange and eventually remove {pair} from your whitelist.") def get_valid_pair_combination(self, curr_1, curr_2) -> str: """ diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 93dfa5102..76891cbb5 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -10,7 +10,7 @@ import sys from operator import itemgetter from pathlib import Path from pprint import pprint -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects, cpu_count from pandas import DataFrame @@ -70,7 +70,7 @@ class Hyperopt(Backtesting): if hasattr(self.custom_hyperopt, 'populate_sell_trend'): self.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore - # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set + # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set if self.config.get('use_max_market_positions', True): self.max_open_trades = self.config['max_open_trades'] else: @@ -138,11 +138,20 @@ class Hyperopt(Backtesting): params = best_result['params'] log_str = self.format_results_logstring(best_result) - print(f"\nBest result:\n{log_str}\nwith values:") - pprint(params, indent=4) + print(f"\nBest result:\n\n{log_str}\n") + if self.has_space('buy'): + print('Buy hyperspace params:') + pprint({p.name: params.get(p.name) for p in self.hyperopt_space('buy')}, + indent=4) + if self.has_space('sell'): + print('Sell hyperspace params:') + pprint({p.name: params.get(p.name) for p in self.hyperopt_space('sell')}, + indent=4) if self.has_space('roi'): print("ROI table:") pprint(self.custom_hyperopt.generate_roi_table(params), indent=4) + if self.has_space('stoploss'): + print(f"Stoploss: {params.get('stoploss')}") def log_results(self, results) -> None: """ @@ -176,21 +185,24 @@ class Hyperopt(Backtesting): """ return any(s in self.config['spaces'] for s in [space, 'all']) - def hyperopt_space(self) -> List[Dimension]: + def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]: """ - Return the space to use during Hyperopt + Return the dimensions in the hyperoptimization space. + :param space: Defines hyperspace to return dimensions for. + If None, then the self.has_space() will be used to return dimensions + for all hyperspaces used. """ spaces: List[Dimension] = [] - if self.has_space('buy'): + if space == 'buy' or (space is None and self.has_space('buy')): logger.debug("Hyperopt has 'buy' space") spaces += self.custom_hyperopt.indicator_space() - if self.has_space('sell'): + if space == 'sell' or (space is None and self.has_space('sell')): logger.debug("Hyperopt has 'sell' space") spaces += self.custom_hyperopt.sell_indicator_space() - if self.has_space('roi'): + if space == 'roi' or (space is None and self.has_space('roi')): logger.debug("Hyperopt has 'roi' space") spaces += self.custom_hyperopt.roi_space() - if self.has_space('stoploss'): + if space == 'stoploss' or (space is None and self.has_space('stoploss')): logger.debug("Hyperopt has 'stoploss' space") spaces += self.custom_hyperopt.stoploss_space() return spaces diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 5ecdc6615..e02a22a05 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -316,8 +316,9 @@ def store_plot_file(fig, filename: str, directory: Path, auto_open: bool = False :param ticker_interval: Used as part of the filename :return: None """ - directory.mkdir(parents=True, exist_ok=True) - plot(fig, filename=str(directory.joinpath(filename)), + _filename = directory.joinpath(filename) + plot(fig, filename=str(_filename), auto_open=auto_open) + logger.info(f"Stored plot as {_filename}") diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 2a28bcd22..37aa97bb1 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -158,6 +158,23 @@ class IStrategy(ABC): """ Parses the given ticker history and returns a populated DataFrame add several TA indicators and buy signal to it + :param dataframe: Dataframe containing ticker data + :param metadata: Metadata dictionary with additional data (e.g. 'pair') + :return: DataFrame with ticker data and indicator data + """ + logger.debug("TA Analysis Launched") + dataframe = self.advise_indicators(dataframe, metadata) + dataframe = self.advise_buy(dataframe, metadata) + dataframe = self.advise_sell(dataframe, metadata) + return dataframe + + def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Parses the given ticker history and returns a populated DataFrame + add several TA indicators and buy signal to it + WARNING: Used internally only, may skip analysis if `process_only_new_candles` is set. + :param dataframe: Dataframe containing ticker data + :param metadata: Metadata dictionary with additional data (e.g. 'pair') :return: DataFrame with ticker data and indicator data """ @@ -168,10 +185,7 @@ class IStrategy(ABC): if (not self.process_only_new_candles or self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']): # Defs that only make change on new candle data. - logger.debug("TA Analysis Launched") - dataframe = self.advise_indicators(dataframe, metadata) - dataframe = self.advise_buy(dataframe, metadata) - dataframe = self.advise_sell(dataframe, metadata) + dataframe = self.analyze_ticker(dataframe, metadata) self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date'] else: logger.debug("Skipping TA Analysis for already analyzed candle") @@ -198,7 +212,7 @@ class IStrategy(ABC): return False, False try: - dataframe = self.analyze_ticker(dataframe, {'pair': pair}) + dataframe = self._analyze_ticker_internal(dataframe, {'pair': pair}) except ValueError as error: logger.warning( 'Unable to analyze ticker for pair %s: %s', diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 0aaf28d96..bdac54c78 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -305,7 +305,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'TKN/BTC': { 'id': 'tknbtc', @@ -330,7 +330,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'BLK/BTC': { 'id': 'blkbtc', @@ -355,7 +355,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'LTC/BTC': { 'id': 'ltcbtc', @@ -380,7 +380,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'XRP/BTC': { 'id': 'xrpbtc', @@ -405,7 +405,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'NEO/BTC': { 'id': 'neobtc', @@ -430,7 +430,7 @@ def markets(): 'max': 500000, }, }, - 'info': '', + 'info': {}, }, 'BTT/BTC': { 'id': 'BTTBTC', @@ -458,7 +458,7 @@ def markets(): 'max': None } }, - 'info': "", + 'info': {}, }, 'ETH/USDT': { 'id': 'USDT-ETH', @@ -480,7 +480,7 @@ def markets(): } }, 'active': True, - 'info': "" + 'info': {}, }, 'LTC/USDT': { 'id': 'USDT-LTC', @@ -502,7 +502,7 @@ def markets(): 'max': None } }, - 'info': "" + 'info': {}, } } diff --git a/freqtrade/tests/data/test_btanalysis.py b/freqtrade/tests/data/test_btanalysis.py index e80840009..bad8db66f 100644 --- a/freqtrade/tests/data/test_btanalysis.py +++ b/freqtrade/tests/data/test_btanalysis.py @@ -45,6 +45,11 @@ def test_load_trades_db(default_conf, fee, mocker): 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(): diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index a5cdf0a82..2f9e525dd 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -318,7 +318,7 @@ def test__reload_markets_exception(default_conf, mocker, caplog): def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly api_mock = MagicMock() type(api_mock).markets = PropertyMock(return_value={ - 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' + 'ETH/BTC': {}, 'LTC/BTC': {}, 'XRP/BTC': {}, 'NEO/BTC': {} }) id_mock = PropertyMock(return_value='test_exchange') type(api_mock).id = id_mock @@ -332,7 +332,7 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d def test_validate_pairs_not_available(default_conf, mocker): api_mock = MagicMock() type(api_mock).markets = PropertyMock(return_value={ - 'XRP/BTC': 'inactive' + 'XRP/BTC': {'inactive': True} }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) @@ -361,6 +361,23 @@ def test_validate_pairs_exception(default_conf, mocker, caplog): caplog.record_tuples) +def test_validate_pairs_restricted(default_conf, mocker, caplog): + api_mock = MagicMock() + type(api_mock).markets = PropertyMock(return_value={ + 'ETH/BTC': {}, 'LTC/BTC': {}, 'NEO/BTC': {}, + 'XRP/BTC': {'info': {'IsRestricted': True}} + }) + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) + + Exchange(default_conf) + assert log_has(f"Pair XRP/BTC is restricted for some users on this exchange." + f"Please check if you are impacted by this restriction " + f"on the exchange and eventually remove XRP/BTC from your whitelist.", + caplog.record_tuples) + + def test_validate_timeframes(default_conf, mocker): default_conf["ticker_interval"] = "5m" api_mock = MagicMock() diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 9304871a8..37757743e 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -202,6 +202,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> 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( @@ -812,6 +813,7 @@ def test_backtest_record(default_conf, fee, mocker): assert dur > 0 +@pytest.mark.filterwarnings("ignore:DEPRECATED") def test_backtest_start_live(default_conf, mocker, caplog): default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] @@ -858,6 +860,7 @@ def test_backtest_start_live(default_conf, mocker, caplog): assert log_has(line, caplog.record_tuples) +@pytest.mark.filterwarnings("ignore:DEPRECATED") def test_backtest_start_multi_strat(default_conf, mocker, caplog): default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 8bc0f6695..d964e58a6 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -466,7 +466,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: parallel.assert_called_once() out, err = capsys.readouterr() - assert 'Best result:\n* 1/1: foo result Objective: 1.00000\nwith values:\n' in out + 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 diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py index ee8c8ddd4..9f5ab70e3 100644 --- a/freqtrade/tests/strategy/test_interface.py +++ b/freqtrade/tests/strategy/test_interface.py @@ -19,13 +19,13 @@ _STRATEGY = DefaultStrategy(config={}) def test_returns_latest_buy_signal(mocker, default_conf, ticker_history): mocker.patch.object( - _STRATEGY, 'analyze_ticker', + _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', + _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) @@ -33,14 +33,14 @@ def test_returns_latest_buy_signal(mocker, default_conf, ticker_history): def test_returns_latest_sell_signal(mocker, default_conf, ticker_history): mocker.patch.object( - _STRATEGY, 'analyze_ticker', + _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', + _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) @@ -60,7 +60,7 @@ def test_get_signal_empty(default_conf, mocker, caplog): def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_history): caplog.set_level(logging.INFO) mocker.patch.object( - _STRATEGY, 'analyze_ticker', + _STRATEGY, '_analyze_ticker_internal', side_effect=ValueError('xyz') ) assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], @@ -71,7 +71,7 @@ def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_hi def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ticker_history): caplog.set_level(logging.INFO) mocker.patch.object( - _STRATEGY, 'analyze_ticker', + _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame([]) ) assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], @@ -86,7 +86,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history): oldtime = arrow.utcnow().shift(minutes=-16) ticks = DataFrame([{'buy': 1, 'date': oldtime}]) mocker.patch.object( - _STRATEGY, 'analyze_ticker', + _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame(ticks) ) assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], @@ -252,7 +252,7 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None: caplog.record_tuples) -def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None: +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) @@ -267,7 +267,7 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None: strategy = DefaultStrategy({}) strategy.process_only_new_candles = True - ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'}) + 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 @@ -280,7 +280,7 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None: caplog.record_tuples) caplog.clear() - ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'}) + 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 diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 838578e25..5dfb9fd57 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -327,6 +327,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> 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( diff --git a/freqtrade/tests/test_plotting.py b/freqtrade/tests/test_plotting.py index 333cf9152..fa4e7e9da 100644 --- a/freqtrade/tests/test_plotting.py +++ b/freqtrade/tests/test_plotting.py @@ -217,6 +217,8 @@ def test_generate_plot_file(mocker, caplog): 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.record_tuples) def test_add_profit(): diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index e1548754a..db4f99d61 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -16,8 +16,6 @@ import logging import sys from typing import Any, Dict, List -import pandas as pd - from freqtrade.configuration import Arguments from freqtrade.configuration.arguments import ARGS_PLOT_DATAFRAME from freqtrade.data.btanalysis import extract_trades_of_period @@ -30,20 +28,6 @@ from freqtrade.state import RunMode logger = logging.getLogger(__name__) -def generate_dataframe(strategy, tickers, pair) -> pd.DataFrame: - """ - Get tickers then Populate strategy indicators and signals, then return the full dataframe - :return: the DataFrame of a pair - """ - - dataframes = strategy.tickerdata_to_dataframe(tickers) - dataframe = dataframes[pair] - dataframe = strategy.advise_buy(dataframe, {'pair': pair}) - dataframe = strategy.advise_sell(dataframe, {'pair': pair}) - - return dataframe - - def analyse_and_plot_pairs(config: Dict[str, Any]): """ From arguments provided in cli: @@ -57,6 +41,7 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): """ plot_elements = init_plotscript(config) trades = plot_elements['trades'] + strategy = plot_elements["strategy"] pair_counter = 0 for pair, data in plot_elements["tickers"].items(): @@ -64,7 +49,8 @@ def analyse_and_plot_pairs(config: Dict[str, Any]): logger.info("analyse pair %s", pair) tickers = {} tickers[pair] = data - dataframe = generate_dataframe(plot_elements["strategy"], tickers, pair) + + dataframe = strategy.analyze_ticker(tickers[pair], {'pair': pair}) trades_pair = trades.loc[trades['pair'] == pair] trades_pair = extract_trades_of_period(dataframe, trades_pair)