From 0614e599661a459d27a96bdbec00b4221709301d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jul 2020 20:06:59 +0200 Subject: [PATCH 001/224] Add tables dependency --- freqtrade/commands/arguments.py | 2 +- freqtrade/constants.py | 2 +- requirements-common.txt | 1 + setup.py | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index e6f6f8167..0899321db 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -15,7 +15,7 @@ ARGS_STRATEGY = ["strategy", "strategy_path"] ARGS_TRADE = ["db_url", "sd_notify", "dry_run"] -ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", +ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv", "max_open_trades", "stake_amount", "fee"] ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 1dadc6e16..a5a5a3339 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -24,7 +24,7 @@ ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PrecisionFilter', 'PriceFilter', 'ShuffleFilter', 'SpreadFilter'] -AVAILABLE_DATAHANDLERS = ['json', 'jsongz'] +AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume'] diff --git a/requirements-common.txt b/requirements-common.txt index d5c5fd832..604a769d4 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -13,6 +13,7 @@ TA-Lib==0.4.18 tabulate==0.8.7 pycoingecko==1.3.0 jinja2==2.11.2 +tables==3.6.1 # find first, C search in arrays py_find_1st==1.1.4 diff --git a/setup.py b/setup.py index 6d832e3f5..b1b500cc8 100644 --- a/setup.py +++ b/setup.py @@ -85,6 +85,7 @@ setup(name='freqtrade', # from requirements.txt 'numpy', 'pandas', + 'tables', ], extras_require={ 'api': api, From 55591e287c567d1671811c441c15c3df0b0d0d85 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jul 2020 20:17:21 +0200 Subject: [PATCH 002/224] First version of hdf5handler - no proper support for trades yet --- freqtrade/data/history/hdf5datahandler.py | 184 ++++++++++++++++++++++ freqtrade/data/history/idatahandler.py | 7 +- 2 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 freqtrade/data/history/hdf5datahandler.py diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py new file mode 100644 index 000000000..21f521cc3 --- /dev/null +++ b/freqtrade/data/history/hdf5datahandler.py @@ -0,0 +1,184 @@ +import logging +import re +from pathlib import Path +from typing import List, Optional + +import pandas as pd + +from freqtrade import misc +from freqtrade.configuration import TimeRange +from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS + +from .idatahandler import IDataHandler, TradeList + +logger = logging.getLogger(__name__) + + +class HDF5Handler(IDataHandler): + + _columns = DEFAULT_DATAFRAME_COLUMNS + + @classmethod + def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]: + """ + Returns a list of all pairs with ohlcv data available in this datadir + for the specified timeframe + :param datadir: Directory to search for ohlcv files + :param timeframe: Timeframe to search pairs for + :return: List of Pairs + """ + + _tmp = [re.search(r'^(\S+)(?=\-' + timeframe + '.h5)', p.name) + for p in datadir.glob(f"*{timeframe}.{cls._get_file_extension()}")] + # Check if regex found something and only return these results + return [match[0].replace('_', '/') for match in _tmp if match] + + def ohlcv_store(self, pair: str, timeframe: str, data: pd.DataFrame) -> None: + """ + Store data in hdf5 file. + :param pair: Pair - used to generate filename + :timeframe: Timeframe - used to generate filename + :data: Dataframe containing OHLCV data + :return: None + """ + key = self._pair_ohlcv_key(pair, timeframe) + _data = data.copy() + # Convert date to int + # _data['date'] = _data['date'].astype(np.int64) // 1000 // 1000 + + filename = self._pair_data_filename(self._datadir, pair, timeframe) + ds = pd.HDFStore(filename, mode='a', complevel=9) + ds.put(key, _data.loc[:, self._columns], format='table', data_columns=['date']) + + ds.close() + + def _ohlcv_load(self, pair: str, timeframe: str, + timerange: Optional[TimeRange] = None) -> pd.DataFrame: + """ + Internal method used to load data for one pair from disk. + Implements the loading and conversion to a Pandas dataframe. + Timerange trimming and dataframe validation happens outside of this method. + :param pair: Pair to load data + :param timeframe: Timeframe (e.g. "5m") + :param timerange: Limit data to be loaded to this timerange. + Optionally implemented by subclasses to avoid loading + all data where possible. + :return: DataFrame with ohlcv data, or empty DataFrame + """ + key = self._pair_ohlcv_key(pair, timeframe) + filename = self._pair_data_filename(self._datadir, pair, timeframe) + + if not filename.exists(): + return pd.DataFrame(columns=self._columns) + where = [] + if timerange: + if timerange.starttype == 'date': + where.append(f"date >= Timestamp({timerange.startts * 1e9})") + if timerange.stoptype == 'date': + where.append(f"date < Timestamp({timerange.stopts * 1e9})") + + pairdata = pd.read_hdf(filename, key=key, mode="r", where=where) + + if list(pairdata.columns) != self._columns: + raise ValueError("Wrong dataframe format") + pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float', + 'low': 'float', 'close': 'float', 'volume': 'float'}) + return pairdata + + def ohlcv_purge(self, pair: str, timeframe: str) -> bool: + """ + Remove data for this pair + :param pair: Delete data for this pair. + :param timeframe: Timeframe (e.g. "5m") + :return: True when deleted, false if file did not exist. + """ + filename = self._pair_data_filename(self._datadir, pair, timeframe) + if filename.exists(): + filename.unlink() + return True + return False + + def ohlcv_append(self, pair: str, timeframe: str, data: pd.DataFrame) -> None: + """ + Append data to existing data structures + :param pair: Pair + :param timeframe: Timeframe this ohlcv data is for + :param data: Data to append. + """ + raise NotImplementedError() + + @classmethod + def trades_get_pairs(cls, datadir: Path) -> List[str]: + """ + Returns a list of all pairs for which trade data is available in this + :param datadir: Directory to search for ohlcv files + :return: List of Pairs + """ + _tmp = [re.search(r'^(\S+)(?=\-trades.h5)', p.name) + for p in datadir.glob(f"*trades.{cls._get_file_extension()}")] + # Check if regex found something and only return these results to avoid exceptions. + return [match[0].replace('_', '/') for match in _tmp if match] + + def trades_store(self, pair: str, data: TradeList) -> None: + """ + Store trades data (list of Dicts) to file + :param pair: Pair - used for filename + :param data: List of Lists containing trade data, + column sequence as in DEFAULT_TRADES_COLUMNS + """ + key = self._pair_trades_key(pair) + ds = pd.HDFStore(self.filename_trades, mode='a', complevel=9) + ds.put(key, pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS), + format='table', data_columns=['timestamp']) + ds.close() + + def trades_append(self, pair: str, data: TradeList): + """ + Append data to existing files + :param pair: Pair - used for filename + :param data: List of Lists containing trade data, + column sequence as in DEFAULT_TRADES_COLUMNS + """ + raise NotImplementedError() + + def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList: + """ + Load a pair from file, either .json.gz or .json + # TODO: respect timerange ... + :param pair: Load trades for this pair + :param timerange: Timerange to load trades for - currently not implemented + :return: List of trades + """ + raise NotImplementedError() + + def trades_purge(self, pair: str) -> bool: + """ + Remove data for this pair + :param pair: Delete data for this pair. + :return: True when deleted, false if file did not exist. + """ + filename = self._pair_trades_filename(self._datadir, pair) + if filename.exists(): + filename.unlink() + return True + return False + + @classmethod + def _pair_ohlcv_key(cls, pair: str, timeframe: str) -> Path: + return f"{pair}/ohlcv/tf_{timeframe}" + + @classmethod + def _pair_trades_key(cls, pair: str) -> Path: + return f"{pair}/trades" + + @classmethod + def _pair_data_filename(cls, datadir: Path, pair: str, timeframe: str) -> Path: + pair_s = misc.pair_to_filename(pair) + filename = datadir.joinpath(f'{pair_s}-{timeframe}.h5') + return filename + + @classmethod + def _pair_trades_filename(cls, datadir: Path, pair: str) -> Path: + pair_s = misc.pair_to_filename(pair) + filename = datadir.joinpath(f'{pair_s}-trades.h5') + return filename diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index 96d288e01..be3e34e04 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -50,9 +50,7 @@ class IDataHandler(ABC): @abstractmethod def ohlcv_store(self, pair: str, timeframe: str, data: DataFrame) -> None: """ - Store data in json format "values". - format looks as follows: - [[,,,,]] + Store ohlcv data. :param pair: Pair - used to generate filename :timeframe: Timeframe - used to generate filename :data: Dataframe containing OHLCV data @@ -239,6 +237,9 @@ def get_datahandlerclass(datatype: str) -> Type[IDataHandler]: elif datatype == 'jsongz': from .jsondatahandler import JsonGzDataHandler return JsonGzDataHandler + elif datatype == 'hdf5': + from .hdf5datahandler import HDF5Handler + return HDF5Handler else: raise ValueError(f"No datahandler for datatype {datatype} available.") From d4540c846ae9fdf427364b8dd1664b3d0b604347 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jul 2020 20:41:25 +0200 Subject: [PATCH 003/224] Add trades_load method --- freqtrade/data/history/hdf5datahandler.py | 26 ++++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index 21f521cc3..c99436b3c 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -7,7 +7,7 @@ import pandas as pd from freqtrade import misc from freqtrade.configuration import TimeRange -from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS +from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS from .idatahandler import IDataHandler, TradeList @@ -29,7 +29,7 @@ class HDF5Handler(IDataHandler): """ _tmp = [re.search(r'^(\S+)(?=\-' + timeframe + '.h5)', p.name) - for p in datadir.glob(f"*{timeframe}.{cls._get_file_extension()}")] + for p in datadir.glob(f"*{timeframe}.h5")] # Check if regex found something and only return these results return [match[0].replace('_', '/') for match in _tmp if match] @@ -43,8 +43,6 @@ class HDF5Handler(IDataHandler): """ key = self._pair_ohlcv_key(pair, timeframe) _data = data.copy() - # Convert date to int - # _data['date'] = _data['date'].astype(np.int64) // 1000 // 1000 filename = self._pair_data_filename(self._datadir, pair, timeframe) ds = pd.HDFStore(filename, mode='a', complevel=9) @@ -115,7 +113,7 @@ class HDF5Handler(IDataHandler): :return: List of Pairs """ _tmp = [re.search(r'^(\S+)(?=\-trades.h5)', p.name) - for p in datadir.glob(f"*trades.{cls._get_file_extension()}")] + for p in datadir.glob("*trades.h5")] # Check if regex found something and only return these results to avoid exceptions. return [match[0].replace('_', '/') for match in _tmp if match] @@ -143,13 +141,25 @@ class HDF5Handler(IDataHandler): def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList: """ - Load a pair from file, either .json.gz or .json - # TODO: respect timerange ... + Load a pair from h5 file. :param pair: Load trades for this pair :param timerange: Timerange to load trades for - currently not implemented :return: List of trades """ - raise NotImplementedError() + key = self._pair_trades_key(pair) + filename = self._pair_trades_filename(self._datadir, pair) + + if not filename.exists(): + return [] + where = [] + if timerange: + if timerange.starttype == 'date': + where.append(f"timestamp >= {timerange.startts * 1e3}") + if timerange.stoptype == 'date': + where.append(f"timestamp < {timerange.stopts * 1e3}") + + trades = pd.read_hdf(filename, key=key, mode="r", where=where) + return trades.values.tolist() def trades_purge(self, pair: str) -> bool: """ From 31df42e7376b024c6a5cf7e6b8c01aff40be1734 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Jul 2020 17:30:16 +0200 Subject: [PATCH 004/224] Implement get_available_data --- freqtrade/data/history/hdf5datahandler.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index c99436b3c..82298f38b 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -7,7 +7,9 @@ import pandas as pd from freqtrade import misc from freqtrade.configuration import TimeRange -from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS +from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, + DEFAULT_TRADES_COLUMNS, + ListPairsWithTimeframes) from .idatahandler import IDataHandler, TradeList @@ -18,6 +20,18 @@ class HDF5Handler(IDataHandler): _columns = DEFAULT_DATAFRAME_COLUMNS + @classmethod + def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes: + """ + Returns a list of all pairs with ohlcv data available in this datadir + :param datadir: Directory to search for ohlcv files + :return: List of Tuples of (pair, timeframe) + """ + _tmp = [re.search(r'^([a-zA-Z_]+)\-(\d+\S+)(?=.h5)', p.name) + for p in datadir.glob("*.h5")] + return [(match[1].replace('_', '/'), match[2]) for match in _tmp + if match and len(match.groups()) > 1] + @classmethod def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]: """ @@ -45,7 +59,7 @@ class HDF5Handler(IDataHandler): _data = data.copy() filename = self._pair_data_filename(self._datadir, pair, timeframe) - ds = pd.HDFStore(filename, mode='a', complevel=9) + ds = pd.HDFStore(filename, mode='a', complevel=9, complib='blosc') ds.put(key, _data.loc[:, self._columns], format='table', data_columns=['date']) ds.close() From 0f08addfbea44fd3bd3ce83bbffc2e81c5290333 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Jul 2020 17:37:07 +0200 Subject: [PATCH 005/224] Don't store empty arrays --- freqtrade/data/converter.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 46b653eb0..100a578a2 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -255,7 +255,8 @@ def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to: drop_incomplete=False, startup_candles=0) logger.info(f"Converting {len(data)} candles for {pair}") - trg.ohlcv_store(pair=pair, timeframe=timeframe, data=data) - if erase and convert_from != convert_to: - logger.info(f"Deleting source data for {pair} / {timeframe}") - src.ohlcv_purge(pair=pair, timeframe=timeframe) + if len(data) > 0: + trg.ohlcv_store(pair=pair, timeframe=timeframe, data=data) + if erase and convert_from != convert_to: + logger.info(f"Deleting source data for {pair} / {timeframe}") + src.ohlcv_purge(pair=pair, timeframe=timeframe) From 3171ad33b756183e7240c38314abca4832583340 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Jul 2020 17:44:29 +0200 Subject: [PATCH 006/224] Add blosc compression --- freqtrade/data/history/hdf5datahandler.py | 2 +- requirements-common.txt | 1 + setup.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index 82298f38b..d27b28c2d 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -139,7 +139,7 @@ class HDF5Handler(IDataHandler): column sequence as in DEFAULT_TRADES_COLUMNS """ key = self._pair_trades_key(pair) - ds = pd.HDFStore(self.filename_trades, mode='a', complevel=9) + ds = pd.HDFStore(self.filename_trades, mode='a', complevel=9, complib='blosc') ds.put(key, pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS), format='table', data_columns=['timestamp']) ds.close() diff --git a/requirements-common.txt b/requirements-common.txt index 604a769d4..3e58f15b4 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -14,6 +14,7 @@ tabulate==0.8.7 pycoingecko==1.3.0 jinja2==2.11.2 tables==3.6.1 +blosc==1.9.1 # find first, C search in arrays py_find_1st==1.1.4 diff --git a/setup.py b/setup.py index b1b500cc8..7213d3092 100644 --- a/setup.py +++ b/setup.py @@ -86,6 +86,7 @@ setup(name='freqtrade', 'numpy', 'pandas', 'tables', + 'blosc', ], extras_require={ 'api': api, From 861e7099ccf14427313b79c5ceecd300963b3459 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Jul 2020 19:23:37 +0200 Subject: [PATCH 007/224] Rename hdf5handler to hdf5DataHandler --- freqtrade/data/history/hdf5datahandler.py | 2 +- freqtrade/data/history/idatahandler.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index d27b28c2d..debbcce8b 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -16,7 +16,7 @@ from .idatahandler import IDataHandler, TradeList logger = logging.getLogger(__name__) -class HDF5Handler(IDataHandler): +class HDF5DataHandler(IDataHandler): _columns = DEFAULT_DATAFRAME_COLUMNS diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index be3e34e04..01b14f501 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -238,8 +238,8 @@ def get_datahandlerclass(datatype: str) -> Type[IDataHandler]: from .jsondatahandler import JsonGzDataHandler return JsonGzDataHandler elif datatype == 'hdf5': - from .hdf5datahandler import HDF5Handler - return HDF5Handler + from .hdf5datahandler import HDF5DataHandler + return HDF5DataHandler else: raise ValueError(f"No datahandler for datatype {datatype} available.") From 6a0c84b64924db4889363e8b062d769c46f7deb4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Jul 2020 19:23:56 +0200 Subject: [PATCH 008/224] Add tests for hdf5 --- tests/data/test_history.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index d84c212b1..fd4f5a449 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -1,5 +1,6 @@ # pragma pylint: disable=missing-docstring, protected-access, C0103 +from freqtrade.data.history.hdf5datahandler import HDF5DataHandler import json import uuid from pathlib import Path @@ -12,6 +13,7 @@ from pandas import DataFrame from pandas.testing import assert_frame_equal from freqtrade.configuration import TimeRange +from freqtrade.constants import AVAILABLE_DATAHANDLERS from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.data.history.history_utils import ( _download_pair_history, _download_trades_history, @@ -682,14 +684,16 @@ def test_jsondatahandler_trades_purge(mocker, testdatadir): assert dh.trades_purge('UNITTEST/NONEXIST') -def test_jsondatahandler_ohlcv_append(testdatadir): - dh = JsonGzDataHandler(testdatadir) +@pytest.mark.parametrize('datahandler', AVAILABLE_DATAHANDLERS) +def test_datahandler_ohlcv_append(datahandler, testdatadir, ): + dh = get_datahandler(testdatadir, datahandler) with pytest.raises(NotImplementedError): dh.ohlcv_append('UNITTEST/ETH', '5m', DataFrame()) -def test_jsondatahandler_trades_append(testdatadir): - dh = JsonGzDataHandler(testdatadir) +@pytest.mark.parametrize('datahandler', AVAILABLE_DATAHANDLERS) +def test_datahandler_trades_append(datahandler, testdatadir): + dh = get_datahandler(testdatadir, datahandler) with pytest.raises(NotImplementedError): dh.trades_append('UNITTEST/ETH', []) @@ -702,6 +706,9 @@ def test_gethandlerclass(): assert cl == JsonGzDataHandler assert issubclass(cl, IDataHandler) assert issubclass(cl, JsonDataHandler) + cl = get_datahandlerclass('hdf5') + assert cl == HDF5DataHandler + assert issubclass(cl, IDataHandler) with pytest.raises(ValueError, match=r"No datahandler for .*"): get_datahandlerclass('DeadBeef') @@ -713,3 +720,6 @@ def test_get_datahandler(testdatadir): assert type(dh) == JsonGzDataHandler dh1 = get_datahandler(testdatadir, 'jsongz', dh) assert id(dh1) == id(dh) + + dh = get_datahandler(testdatadir, 'hdf5') + assert type(dh) == HDF5DataHandler From e26e658f99bc82f9cd194a0dfb2239397cfc60e2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Jul 2020 19:33:27 +0200 Subject: [PATCH 009/224] Improve a few tests --- tests/data/test_history.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index fd4f5a449..2c0665b69 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -622,7 +622,7 @@ def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog): _clean_test_file(file5) -def test_jsondatahandler_ohlcv_get_pairs(testdatadir): +def test_datahandler_ohlcv_get_pairs(testdatadir): pairs = JsonDataHandler.ohlcv_get_pairs(testdatadir, '5m') # Convert to set to avoid failures due to sorting assert set(pairs) == {'UNITTEST/BTC', 'XLM/BTC', 'ETH/BTC', 'TRX/BTC', 'LTC/BTC', @@ -632,8 +632,11 @@ def test_jsondatahandler_ohlcv_get_pairs(testdatadir): pairs = JsonGzDataHandler.ohlcv_get_pairs(testdatadir, '8m') assert set(pairs) == {'UNITTEST/BTC'} + pairs = HDF5DataHandler.ohlcv_get_pairs(testdatadir, '5m') + assert set(pairs) == {'UNITTEST/BTC'} -def test_jsondatahandler_ohlcv_get_available_data(testdatadir): + +def test_datahandler_ohlcv_get_available_data(testdatadir): paircombs = JsonDataHandler.ohlcv_get_available_data(testdatadir) # Convert to set to avoid failures due to sorting assert set(paircombs) == {('UNITTEST/BTC', '5m'), ('ETH/BTC', '5m'), ('XLM/BTC', '5m'), @@ -645,6 +648,8 @@ def test_jsondatahandler_ohlcv_get_available_data(testdatadir): paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir) assert set(paircombs) == {('UNITTEST/BTC', '8m')} + paircombs = HDF5DataHandler.ohlcv_get_available_data(testdatadir) + assert set(paircombs) == {('UNITTEST/BTC', '5m')} def test_jsondatahandler_trades_get_pairs(testdatadir): @@ -655,12 +660,14 @@ def test_jsondatahandler_trades_get_pairs(testdatadir): def test_jsondatahandler_ohlcv_purge(mocker, testdatadir): mocker.patch.object(Path, "exists", MagicMock(return_value=False)) - mocker.patch.object(Path, "unlink", MagicMock()) + unlinkmock = mocker.patch.object(Path, "unlink", MagicMock()) dh = JsonGzDataHandler(testdatadir) assert not dh.ohlcv_purge('UNITTEST/NONEXIST', '5m') + assert unlinkmock.call_count == 0 mocker.patch.object(Path, "exists", MagicMock(return_value=True)) assert dh.ohlcv_purge('UNITTEST/NONEXIST', '5m') + assert unlinkmock.call_count == 1 def test_jsondatahandler_trades_load(mocker, testdatadir, caplog): @@ -676,12 +683,14 @@ def test_jsondatahandler_trades_load(mocker, testdatadir, caplog): def test_jsondatahandler_trades_purge(mocker, testdatadir): mocker.patch.object(Path, "exists", MagicMock(return_value=False)) - mocker.patch.object(Path, "unlink", MagicMock()) + unlinkmock = mocker.patch.object(Path, "unlink", MagicMock()) dh = JsonGzDataHandler(testdatadir) assert not dh.trades_purge('UNITTEST/NONEXIST') + assert unlinkmock.call_count == 0 mocker.patch.object(Path, "exists", MagicMock(return_value=True)) assert dh.trades_purge('UNITTEST/NONEXIST') + assert unlinkmock.call_count == 1 @pytest.mark.parametrize('datahandler', AVAILABLE_DATAHANDLERS) From 0a28818b46bf1ded08579c41e951db45ff31fdc9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Jul 2020 19:37:37 +0200 Subject: [PATCH 010/224] Add some tests for hdf5 --- freqtrade/data/history/hdf5datahandler.py | 3 ++- tests/data/test_history.py | 6 ++++++ tests/testdata/UNITTEST_BTC-5m.h5 | Bin 0 -> 261142 bytes tests/testdata/XRP_ETH-trades.h5 | Bin 0 -> 310513 bytes 4 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 tests/testdata/UNITTEST_BTC-5m.h5 create mode 100644 tests/testdata/XRP_ETH-trades.h5 diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index debbcce8b..6a4f45fa9 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -139,7 +139,8 @@ class HDF5DataHandler(IDataHandler): column sequence as in DEFAULT_TRADES_COLUMNS """ key = self._pair_trades_key(pair) - ds = pd.HDFStore(self.filename_trades, mode='a', complevel=9, complib='blosc') + ds = pd.HDFStore(self._pair_trades_filename(self._datadir, pair), + mode='a', complevel=9, complib='blosc') ds.put(key, pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS), format='table', data_columns=['timestamp']) ds.close() diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 2c0665b69..003e90fbb 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -707,6 +707,12 @@ def test_datahandler_trades_append(datahandler, testdatadir): dh.trades_append('UNITTEST/ETH', []) +def test_hdf5datahandler_trades_get_pairs(testdatadir): + pairs = HDF5DataHandler.trades_get_pairs(testdatadir) + # Convert to set to avoid failures due to sorting + assert set(pairs) == {'XRP/ETH'} + + def test_gethandlerclass(): cl = get_datahandlerclass('json') assert cl == JsonDataHandler diff --git a/tests/testdata/UNITTEST_BTC-5m.h5 b/tests/testdata/UNITTEST_BTC-5m.h5 new file mode 100644 index 0000000000000000000000000000000000000000..52232af9e6a3fb48a11b1e28434d9ade31b363d2 GIT binary patch literal 261142 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%0DH8)0H> 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 M0Koj;+GPBH05iAmr~m)} literal 0 HcmV?d00001 diff --git a/tests/testdata/XRP_ETH-trades.h5 b/tests/testdata/XRP_ETH-trades.h5 new file mode 100644 index 0000000000000000000000000000000000000000..c13789e2acc9f44a81dbcc5907a7050fc3b933c7 GIT binary patch literal 310513 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

T;^Q^qt>Gk#(5KGYRvJ&9zjaUv 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%0AdKwn?Z%@4G@`=UNuYIClBntX=}?+m{x-Wg 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

    p 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 42Oawj1bj`}UciI)yUx96!Ft_&el5e!G%N>J$JI z2lqd9rbB%Bw(was7=n!i$h-zWfaZL@g3FFyvd1j znwzsgc~auiva`!tU$|bl#4jw4J^;KPiei}5QM9#9LD%~5i2ccTn_-Dnk}<)>CvPU9 zci8D0m!Vl~Y|)nDin{0ZEigII+0`=;cn^~3wY>e&cRuG?SOZaoRHSsjbRHpg83ODAq;(mKb!e&yVDLgd7gLICkE|NOb8Vg$fF6WHJ> z_y`*&$B;u1dm1c0!dV!^#ZKqFT-_~;J59t<{wb%W1$2KG*%Bbn<+FY{U0fZ?1Uob+cP2+#TR!08JN z47_~h8ijH_1Or!q&~RvIz=<~s&Tni)z!ej-LFa>oEyzqFv_8mfGQyRVlmxpNY&0;b zY4_l6=`dJBGH3{rNkVX058&V-9pTN+$$gxc4;d{gDu$vVP+a&nboy$ zTqk>lP!A1!0|P-gnp;|7>g}ItsLb^vXW?Ewy(9$bqaZ>s_xSQ9O!&NhGxm04N^|Nx zv{Gv-5b0Lp`PX?b~Nb#3i8eBU4NcwfhmSSb!14)!G_ z5CG7}N=ER&=p-Yo92{KS+yEejTR`pM1cfk!Sy)(jlZZGOVc)!2V#^jxN?LmBM)qYe zq<+2S3L*!x+sFtL1aB$O5N1V1B_$(W_lH=y;qxlzr)6T8O*E-tYYglVIaNr*HRNrHTB)Gq}=sE}t|>Kz6r zf~3L64N%}_1ZiZf4{dxEJo{D=@W?T;Tdo1P8-zP;COV6xs+GVJ6WKIvK$bRtklL zkdPq+bcz7HGkI-H24jcG8ai?Wj*o&VV?1~Y z86nGnc2XFWN=yJ*!HiiEB-a)p=jLK6%w&X4(xzj3@Dv0isX+oL$UZoS5HM{L24BoX zsingk3x+KGrXcc|D<8SCl2loVP}&ZF8PRE#2o1qA5QLP(96_(#NK6SE9X=wph$w7- zfZMnlQWS`Q_AyZqN-n0&L|Gss6ks7@2m-TK+;rsALxiqPqR=6O0E@%9L&ouR23*NG zhET{0NZ$gZ?*LpPB~KzC5g6o#K%qd(1Ny#zAT%iuQ&`2w4$axB#sR*H{VUkRc zDu&cPrg)@JTZ)T`l$)vzvv`OwhL9-~nmy9bh>fMuV<`-Gl0salln;~A#_ob1g1$5g z03Rv76olS(k=|B0UP3|0?)W}(Af|_JqmwaaJcWi}1R;bZPob#s(P%$>NeHbkG3Pyl z0o8_*>BnD^+lr7jC^wR48`9QBL+B$2{m~PI;)?hJZ<~U&<&zkQe4cNhuO|Z-jPgkE z2xV_Bf*2r38B~{aDH*|&fwad46?u;?N~0~%F=43KKr-YQV@2rFML0&E^aKqdFc2sV z5*Jf0h4Bz$!&Ca8&bH!l2p`4-<;99skP&eT8G&5UQpt2}&@o>qtU!6&Bm@+%h$O)t z4{jC+e~_3@KOcBAJ_Q)FZiH5EinU4^KoCL=@krN&;j zusEWpMwYV;lt!u6^2Qo$@*Xl^kwj~0g(Iiqtra9BLdg=I(lRQCJy736JLHtq&Xc9d zCK3|1ByUXVunH!TYGLGTOd*?_Q&3wuYjf+H;it*ea5bW}7Z%|W8+bO7OjdHWgT-A7 z4bD*mM5^^e5|QY9hPWYudYMOCn{NQ}G>tc2 zHhxvq%&n}0c9E%S)+vVQg$qhVYc)t8Y#Vs_x~0mE3q%@K-95r3<${?3h@firMsa(U z)T~`yc1Z8kQ=`g|Z$_vDMFa-jC9)~0t6`|pSq%*tK1(&KiVUBOjOhV;>W#fJkVF-= z^J*#=eN|AkbDjwrD)1d@Y8pgiB3T{0V~oyL(4N^i983-@)?hnBwsSSzAt9}T!QJ*# zfaOQD(8M&!NB3m`B99Cp@&sK!8DA}xK&3!*ha47YVoj7j;LY92|*sWfYMmwmC<)HZ4yb3rNUWU{ZX1j)!l zfqH^+6N#50D@YS10u=%SFbP8m4E1nVfh-+Np->iJ12`&0ZNXShUd4Y8+3pNA+q%jF zefW@i?4en3xc!r|Bx5Jz+cLxyjq}!7uIBbx*3MaR4Gc#e-6zHR z;PvS28+%hyZrXZeo^Y(rl%Wn9XGuQ_v`(qK4b;Jy+^n>yT7N%>sHi;+u?F|4m`aw0 z=A9#vE>M;ls082(O)W8usN{%I{coS5o;z)>KqOlkOPHNhlaM)zlI1Xa;=UxZ{}Brd zGZk$$88sa@b2(q~hMrK(ys0QumdQ2i9eY)X_U6)RzUJmcG7-3!hBA9))c4AmkiW4-w8(^_vuL!*DgKj!sHQ(oS`v3?=3) z)y0);3NR?y&?r9%d_zuW4!`F$26Ik-rTb;UsO2gQfu`)#Xiu} zTOX^*uaoF|cq+W+7FsT>v3jRq+%z`i??>F@;^Ob52inJ+zp5$SX3Cl@(_TRDo-2Ph z`nGj4#bd{zo3_p&RyA7ei^R)PZM-L14y#-@L%A#M6!H%ZkC%Qr#gQJ&@6he)+^;k| ze9`&X&r82*-f*EghmR$)U$9CINa!9&*-zqCusvS@o#0^A$P#^Qb*nY%NhHP|B8{^b z=?S+T!QF+S*glDyGW6pv1vGo>q+?mADYNlVtbUcvia_XIS)RO7g;CfvuGea9R6KWT za;x?#x{cRmeBRyYao!;=vH1nvf|2Lx-(axcuGeTax%=}^7(jmsnVOoSfaA+M)14N6 zi}AVI!nnRYV$NMc)nWJPW@t8e1uyRZbWp=zeY8f-5O=8Z<@t_6%M_1F<$D9Zlkd>a zUaL!twy5Uv58VuoX-4eyAHs?G=DT*OxpaC*%*7Q{Odr2eh^o#UdG|~$I6SLtO3kXD zc5mCzWcT7)&L|fb%B-&+%ZzY5D%;88J*Qn%_bptwVet}aT-Z)YOEP)CCt5zsVb{~1 zb7Oj1Wtu&bL;gZhi+0@33vVZaL|xjQcQ>F;Ke&8Os*Mz2j~|BWk4^nHXk1qy)hB)9 zDm=IIK(g{LH2G6^*GbFFUuxbG=gqENdw={-Fa5l>(LSX`NA+rtz{BXHXV2u1^uCeI z9e8p%*L7yCVp9~awBitNH5avDd0^X4^vA28Zc=Y#v3q3^kEO)U<{Q!UEiUIgW!>rB zx2V8&H4Z)KCE(CW%+L%7*mXA`E5XM8%F!@uN9(My7grQ?iBVrr7XK)sQ}I{4w*Eb@uoLlz7P?Y>^D1F>l8gbFWy#E zLdMFDy1A5HL7xp>A^Mlq_um=%cttK%*09Piv6h89Mm>tS{+M_rN(I#=rin)_cqcuv z8!_t}JkEXO%~o@dsaDb0LWAHWfora)=&IvTxJDjbtJU^wY=qsd`1%9kk%ZvwTKB`w z?3CU25k>DvO%BLj=uLT%mbO3s`@VLcnSQ&`ot2;{q6`*sh%dI-BeT;auei9PrdUVm z95{ua$nvfat&l)ma(#F=^5j=ZGJ?>D0%2PDP6>?s}E$K2BL zO~~2&vEeQ*A-2e~mfaTz5(1S@qk0-m_4dcVf7;viErQ@kdcbCWH_z;~hQ@-2;gJ_p zwGHT|zZ^q}9JEx=C!*YJ`jqIus(Ri`C*G8DUh#?&i!;_kziRkS`|%VHI^O(!Oe4Nz z>;2xqmYJL)&afYpV6lDsD$uDHm0x2oEqz|MHweah?Bwl}_#2~>BHAof<0@xoXG_th zp@BM0zXmgDeSLz6AXT?N=Xo|xP7>S(srxS-vg$?)jCAYxyz*aKS$W00ue$VGtBb3T zeNE|@=O&o&33Vt&H<_(h@lBbTPsIm6dpjB(aydTUH)QJr{#Cte4}n5&k1o5bhl$Ii zrKRQMf-7pXv9U4#>G{{DT=o7(7-<+6u)%ycy?w{Qd7{c(Z*jwjavFS-$P_{QAOnY_axhimFYKI zSLT`U*Ks_Jh6hHN&L1=?*cDDHl0Zo(1fQUrR#x~;P1E=f@VO5A#;#F9|EkO8dLQ0T zFd0L=uk4T0$xlAenS4z*DAn%b=FQtT8y#<``C{yIgnzzR63utc{rktxLE_o1&(_k{ zUn*0JO7ikle-7^+59JZ7aCv}Q-B&pO?R5CI)7m+w-I|3)U+o~NJ}xY5@MrD&!}93p zS2SWM>CV|Y9~F_VE-f~<+_iHhk7t6urTIK~d9oo!?Yt9e`eWTQ$*$|}qKQ?TfNzx$^ecXDskHJ2=e|LYswj7>)dQys4ssX(j zO%u)QDe`g2D(XRth6=oCzmjQok!r}Hp6@qKSe0?Ny_QZ>_>SEdC^}PVjul)ps#YA zDYWzU+szJlOcCZ0j~QS6r(4NCwHau-qQ_VEtT@}Rx460Y9A^s!tZ8X+>Jm-Uo?|^ z{MIqm)V~Bf!j9x~EIxvi%axZ<-#2H}ZFZK$oed6lovNNl*Ry<(s57*_d;8xb_k-H! ztI-^3y-D*!Ynrnb%u*IlrVUtVzrKJPJxWWnzjDCQ@wdcbZK}c1 zdsw8X-%0kv1xW5r7$CsdkMQBcxf4I3VGy1U^-Fi*|IVvV%~U7#-q)KnotgTu<9z8Z z)hxR9{_E)H%Ggtf=CfaaPq(o-8EsHO^pU+0nV@WFZFw0lV$kG_?y9-ETh$FUTrO_< zUNw8kCn#n(EtDxIN8sja=!nnN`)CR+ac#&X$GR)1zSJR@0L?D$z#k_F!)_UqRV)MtdiG{Y^CJVwxm}I$Tsbz2q`+;u}S5GBU%OdCwLnPK!BC%3Rc) z_r!tN5R_H)-8*dADO=h8Qg*3(kWy0C4`r9UH0H@*9cH0V2R+e{9H;YHr)6cie*aeF zT7B=N?KztDSfTgqk;d4zk6PDHqOlw&#vEBrk=raLLh3fT<`xwx7ZH4!@1*{E6ne>o zB!xDzy`)+W`|Zh)mNI)Nrt(@l^zu!=%sXdCy7Gml+Dhq~BGT?nMzgucSR$EA}} zvf>%hE`o|7$hmc4vw{5-|gRU*Mv zeC>p`()g;eoUu46f)g%J8=uH1JTWnzK4Hk#BD^hG*i=8Gb@#Y<_#s*>m)xN5-*I6z z3y2(QHHL;^cU@^ar;$oVhTunqN3Z+MUmQGk3>EYDSN$%RFM3{17`<>>c>IK1mqF3# z3gMmx=9>FnqEB@(bBBrXcFl3&32QWMyi|4~Fv*IG`Yk)kCH4>!+3n>S7 z+_Rftl~R58GgeAUO1z_N_71wX@crPTq>>q-a6wxECB+sk+{~JH-CdwS`fJe275(n0!ofL#6V=BDy8g<)F+^Q% zX=*-?Z~8oO93OLYI&IH@-KXhwVeZ*A|K%glwYdZh!}$2tY;GEYW>HM5h03p$`bPFw zU~!#RS=Vf?YA=X(7=9eo-!T~2jJ`R1c_;EmM(SmFxP54svBQkMOK(T(1dA7A{R2j(gls$HwdpKZNA;e9p*2P`1)ng)!1 z)%@DAyqeT5F#r6PA=~AuQuNr7q!Ww&fe}GP-UW`&x8&;CMxL#(N@e5v*=Ky<0J#Uv z5&tahll3$()FjjJ8^0cV*vQt|tr^>8xS15d=lJ~-^lTW1QMzm#eQnJvq{+<2E^Akl z0I3Kj?bQZzruF>t(7wlW7k7nC+9)>pF5%)QHoiMyc_8w~wVi0|YPxZ{ zq^7IN&xynD?Bf(2@wD=*2@+v^UZ${m;AD#aj@nz=*WKOyYFB!CdM8ZbYn|SHc@NvQ z?IAO{%5usj=*@XfKC@&W2Z$Ll{vNfuB3H&bkl+;1w6?R@I^2U(4(&dtqfv8cM_kFr z)2Eu;o*kyGq_zg6?g(Ig{4gYBEB*o6Yj5v8OZz2Pz4A-;pg2|FYn@25kJPADWUY8E zr-p0;dOh+iC!{z@P0c!09^UXIhp37kHhsoMxEH+*qHx0M%&w=^Y0`Jy z+0OfoKstM_){!FvUAiV=x@7U)I!|oAHoCL5clWc0#gm)`Qi~F#y}y3e1fa1m9A;8_ zrDXCFPi+Zx?4{AlV`()y{4WiK2iRKJWHZqj(Mc?HX7*y(Olb9t=HI`fm2#q9WPJ@< zXwBa?i##zF9!>jDQG-O7Y}NRih!Id4dw1LIJ1w)bbDdLD=I6^O`bUpSzCU^Lru+t*`qz#e zxOE4+;?OU&e9khz&u}rcWq1pEm@RfQo4PSYDdv{0R@Wj+=|s}>tN2X!SzadnfX+mH z^qXgK#3$86UQ6jGPe%Lughwuwerb+d`3vfh5@eobjm~ZkkTH04ltxn)p#A76RZ`+B zageDfkSyK*=yPRvOD?)#t~u=@y__H+zelfc$!uV#mSn-#&Ii9QgW1m7uqa-v8$BxlA;i1Ri z-1^G)Tggn~uQ2J)e5dy^rcTtT~c07wCI;&3;K~G?@p4w65{`iqL(L* zVl%(%Nm62f8?86t|&l zRD}jh8=75q_AWG@aOn1@?VEMBKD_smW!7fSL?Hp~B`%Y;=J0F5KJ~xRRBG+RhQW{{ z50jE+od(xBfAKMgHhYT;O!bC>n@2?hNQqaLG$!jFrGGgn1LMb*%~_X1z^X|S-c96SJ% zOS!N+Svi^0{waTw)YrL6HFFDqSMShPaO<9p3keC4oZa6U)^qlNbHi1wOQ~C#YZ^7( zP`#yu-uyFE1?L`0B~$3f)>VnA%_~1Q*ZurjXQK1gKL*`K+pE*AH+|d)7ze-RhONV+S~m72U=S{5A@e5M=)|QmJ@+JKiVQD=zj~!B?Fh;hp(M=T2DC^AmwXM3TK`U z7ENw!i4WoYl1v#wcdc1j_EkwNRZ$}rbtKQYy9c#}571~=pVc$8c5KfHM@`p0@96Ec z>nk{RY_pP>UQKfTh5ElK-_GYa{XJ-`9iNV}*Q{MbD^{aar!~#=yxwe2I>+teb5&hD zzA#4V*wT3|RNKWP!Dxq(N>}8kpfLkyUUl^j5%qn(zgr|4ez(79uRu?9mwcNwNRAIq zN;deGTwPI8Vw+vE_k*Fkm%%=E*7tHOyMhy#3`zpAYxraCWau$?oxf%GnA;{kUV(F* z7H@qQzgp44PYIL&$ArR%&iLQ@}-G$9mDN zqC%IiYrQsp^R~Y80Lt;~v~niiqNl*itGlQ8^pzWVQ!3T-4!k}jrE7w_Z$+a4635pJ zuIhhW@Ru`|{KHe}e#b*qVe@M@PetK(l5_db(QD^LM52M$lKPV zo>6B;ylB#QCmsL(-8E%%P9nd<&FB3eS*Nn9g)M$QRp`a~YGL8Y0i(&+UyM!;46v{y zC)uAY;^p<>?T%z=l6{48jK{T;Z^hi0ss30_`rg4m?s24>?{3*Bhm#2(Q_JV zOG$aNKDB4`I z+WKvgXpxfKyP~nl49EdP@uR}tZQDvzinr-rl0gg2Zv~jil$7We6rF7=%C{=7an zv2CIk74xZ%+Dj<8Tbo%zuq(Ndc%{0ZR$b7y{;7koq_W2(f{W8^3L#XN-I)f*4F)s} z9b+>iA?l~^-hEi)#^j_PzeG(+vdC^|@R{U1c|n`A8Wp##CS-E%$m_7Jt|sSmwB4Go z%a$}IzAGV+_zBhO%jjOh46)?SZHceuu16fAcH_EUxz9Lw$H@}pA4fXQ&)-A)$%W+o;sPA1=m=MNlkCZ3vSuh8c`#J6ZJ_~kq*My+6Y9Q8lv z=wVL8B+ldne9So(GfwRj+Vm(W*nN{AYW>s5>Z^t9=*1m7?1{@_Yu=K7`nr$3yDPdo z3iG@h+<;bRU*AOx^}7>(bE#;_Zn=1UzO`1&mZ}(aVrhNpvZxGdj!jIAdb#Q^ZC^Ix z=a0DV_+IL}A!p|&JIqcB0!iLO!|>>l$T;`_qu%2;{W7??9G!xFZkZM#yHtZ7!T#IV}W5rYG1$;maXZS~pc_%mEwoO>$O zJ@qa%Ej2YRuKhLs=WJzVeQs)dLgQ34imyp5PWMY3jE#tmNUM!bdfvBMlLTRt?O{XF zvDCUj^z}1Zd~)i$_UQJ~u&~C*FC&tlQzOFg6-7NIGwl@>vuI;NO-0SO#lCj@Vrp9L zaK!h)pHr{Cx3?$vHw{9FS~^-&S+cs2cE1AHflG5M{qEGXw2Cr(%f`9X&a{a&v@&@q z!oB?!e&*v0p4zcESF_$X`Eengnnnegj8~~BHLa8vJrlsel5J874mTD@1b61g3yz#Q}OHHk)uKD<&9iLi*D}XrmFKe7 zRjGJtY+!8cvyx`~+@n}XA~ltfN}YU7MN>zIex+04J5p2A(@N6X@1~}v7L7=;WAv6X zWO8#l+i-hR|1(-xG!>RElZ$78$d^`;5s}f}P{;+KyY{K6inWJhv9Iq_r;?{oT0+EB zX-!)6@SB(AVKv`Vo1}Tt_#;vW-=+<|OzTd^qtXJY3#An`6?0T-X-)b_8a{P!-My1g zGtgW3b2>GZiloBQ<;>mo_Sdh&9v2NJ;ag`H-B;tDrBh) zH#1`!-ME6ENwqCbNTj~aX>S_F^VP}>p}f5KM&9yIi_OC5dU1F0pSUz!I-{1WzoMC| zwuftlTcQ!SwG)R^+}hhxTV7sMGWq#$>b+WA!xsoYES!OMA*^w!ssHtKf4}r}E!>k| z5WKmlSz*a(sh>Zxi+skRtgmpjWuKS$<{#m3-x`P5T3^Vp#o_lBbHxZu)#A4PL6O=e zK`z1%xp!kNKYrvWt~}wuM^L%aGYRzXz0`<^bTo__H&9+35s{YsA_D*JPbYq~uOca} zqq-!lye%~>t^sXNh-gkLT<>^SUs+jB%}R|+eK=5&@VXr$Of&K2uka{TR_SyN?GZJt zxgt7s1Zt)u4d|5xagXBeK8h=EM@Rbm3On)b13%JwKa6H}HsY3e@#(2G)eCVUi|^mX z#i97S-)3GGET-ZgKE%zf-@ls{H?uT?@9l4|c@a1E=XERkqVsn@@(URW6WSTXe}A)@Twlh-(WX?0=#z2w|S7>#4K4kR?SCeQhvukk)%Yd8!EZy%dJH~T?_{uw!wZul zyzpx?1UCM{E&(rsstlHYPVz%`^lvV&L`lvgo1|2#aH|2!SMnMVWy|7|2lu z!4h-CaF`3(m4SGX&XWSMAd)y1ifv^uM>1Hb6om8ylV>m~U^}s0Iq00@yYrJ>fq^`8+m1;vWyP$i z^!~u5qMECzAq$Qw{g{)i3?3kDsD)lOcY>|+bme#bl zSF^Xa)*)X`40a2D{P^%q>8sG+bz}OxrelTY3R}YvF)l&DHI;4i^S$%o_#;PV6`<>- z+xVuSz)TlhP!IO>Y}xX;7DI4ut{8#@gA3A@oi{JP5_o0)49CN>n`d_qyVQL;{SyU2 z_F{Ohy*Yc29tE#uvdtb#4>E$Ao5T>d<>i=|D4UgKG7Uk4g6!<>+6@lEq;AaT6~GRz z!~_Qi$8>y-=-PfJxNC20KXSd zC*(Xb!h8HB?~K|bwh%X{`rB?6n1ux!o0Q}FdYZnzQhHs;UTv2<+PjT_7Q$gFFSU3JPo(rrheR zUy9cC<>szW9REJNx9j_~qa!JV_DYbtumBHOc0*wD{YHO$Hn!v0}C+hQ|)eHdaGE_`+VK`)s>e%qDixK~Rl9e;y%|5p)FY>WT+$_u5t08jEw^fUKAL zIeAD}*lF8wCx>M}EiVHoSbu|l=s2vbc%atS8CS0G4u19}lRpdb6yR|_4}Ea=WqqNz z)bsu^&iY+ooJCwx|7Dy94{jLe!42aS0OL#oIhB1I#_7r>CwKR*&s{DqDwXTyhH-ju z#XS${gcLg531Hm0)5cpU9<2`GGzR+Wn%@}h8x!mXRtSli4TPoiLynH*elpqVsE5b( z>))At7^Tvb4!-JNa zYwQRvu(lFoW99;5+1c5Od3lGS2AZ2;!5)?}neN=VHA+L!>FH4}C!X`LCnH1r1|TB{ z)0d{F+d;hpuKxZVu;RwFcBiyde^l`McL0b-NBhR^nOI*>h={;zxwS*SstXJaQ)J!H zg5_dV7Qkz^?F9+FSy_OSNLmHpUMA{q{ob1xG^d$0YSNw-J5e`0EI+V-t!s%dn*`z4 zHRaLP{qoGV`I7rZ95soj*a{LXhB<>S6>m*~hS_3uwY62U)!P~%dcD2x7o@cu%Pc(E znzD#>j<#CfFt@-N0miA7knldUIzbDZH=3H5nb$|_mY0_!B7z`!>ky6M*3=Z-M5X$A zYpv5)O_`ZTy4Zq1z*fuGBn&je6O33ajGx~`l2}%TFEbskt}YM(3Y|62u&HS;ST4)# zsZ)47zahxlR{Yn)Q|Od@mzKARcR_{q^fYmr7`!e^H8FYWfDD6m4(}OGPY1e1myS@9 z0~x{jPk?m-zGm5a)P>#!MLs$j5)vE2#s)!1%e~n=NlDqu(C#f)^Ych1(FXPh^SChw zak_!bmXUz|XOlHC5i*jMbtyePD?Qy;Yc@JMx?q)> zM1`Bm%JQS5SwtPY>~M&Re?fr@sIJQd5MqQpg@nXSy4VIB9SO3Ij$s90o=HjY_>qtx zkuHI9WM+1yE85ZlUsYiP&K#)Y>>mG#Ou*)KNm~$OW8Ii%-8>~6gHA0&(Y{+&$x=~~ zka*7q8wS|g@+f9Hy18XW$L>iIax`=lYC>Yk2>v%ImV)3kVTFuoPb_mRLkPk9`o=<6 z4hOFg-x3W%NEa+TtbdP%4ja!zEFRz5`mQxLw!J+zKNft&nI~eq$_7+q9mJ z3;U#Z;lYZaF>7zP7+*K;L$k8v6WsV6TU#TtBVb=@m84AD%uE#(H#b|GloZ<(M}lCh zj*dTMOiBv+YoVc<+q7gwMLTG;?6aAtAYb)t_2nVqk0d-^POw4Qxm&~tzl^*Ha|ExEhr+OJ;_Z<1zd-jq2!s;Kwo^{3~Vc6T1u)Y$FZ z0tBNwtgLA}H?!UWO;b^6ZME@zguTbJvRXm%24QNCWo|BV?&3wYTed<6ZyhQubPD-v z1+^+FVt);47B>r>BVHX{-D``khYxcdK6I_ANo#nUCriRH!SZtL&0wJIDJheaeW1WN z(JTx>PGX3V*Y*(Go;N+e0%5PI!vlwR*rb~!C(CA%5acD8=*#h!w{OF8rpQ{Eha4Hf zxgP=(y?AkU*3FkUT73>m){gFH+;vdOl z4Gm*sMZd2V6{&%Ve)Nro^+RQk4HNYwA*csKxx5^&t*k6oSC?8>$I=u4?4Dn|X2O-= zA(ftf8~s+eU7(juBy^FUlal}qeuDP+`1no$|M9<^8`9w5AiUtds;38MO-!CV z`3ibYCO^5&tx8AGz(6Tb+Up#QgEkK)fsIGdz4;sf!3TbFJSE2jtIk8=zMNNYRaakH zDn9D#drCsW?{w_XfPmQ0ZCc)2odle=sw0VDq8w)vDG08r1WdFmF*0$(L^U-NH%t^> z!9>4;5b_30)IbVGSO|}7qLtO~u*h&?A~$#9zCVS>bGQ4Ba zab(1*&X_W%aa%W%2?*Q6ke+?JH(j7ELr?81;Ns0NCcHfLjLJa(n6~-B{B*n-yN8Fu?c?lAPgKTnzke%5qNU3xOo&8t zD)pF}1ewgwY|;xHw*ldBZvo~9AIangEDs*&Dl6yqgN>edzkS=n@HX_|agB{UPzRlz z(7TodBhAXv*keHk$W!aF9~?tc&!10tOgsT{Jl^G|i_5!rQc@=@`1nqIOXMUL3C$~CqOo+8*p^=Gt0t~&mogLKFlw(V z%1Mh-K-qungG6iH%gmI_tojNVyyM4D7k`L+xg2XZ$2a3F`cjy_U}`GN2!)3zDBA}X#iPV)z8n*P=dc1 z%orCA10XT0-ri>MdTZ-1*7FA7upkXSMuqntMMhq~1vV<#-YzYz0a}1F#K1-sVFZeB zlIN@3s``#qDk!WgtUGm(nl zB=X+A&+}f_^Shq+y{`Aa=l$d7;yCB*z1Mi(d#`n``~IwJIlDTuU@@i6Tnr>FB;D6b=#Y=jTr(Z znLoh}x11L_ITfD#Jymt}s_)gSi!fkkzkK<}j~`1*+1XSaV=^|Dk~|uI16t0^%m^Y0 zE3QiH(7(C_=sRZd)g%Lhlok1uwe_v)H}v6@6Z*%bjvceHxw6#IkQN?3BV=1};DD`V z<+)qB`ginorvls=v_%8F31&TlAaK=Y%{V7mau}kc>C8K@XgWa_TcRA;vr0I`DMn}2 zWDzuDW!2N$j+Zg=@tNh*xh^8Q8BbcPKwn?qk%{w-6BBxSKB{U4B_-uIX2f>JVSlbO zZrQTsBNRk&QBhH`rnB?Fu>%KEqOM$-np&Eg90CuG$1N=|Ps(S?&wm~T_W}L`*<|59 z>g$v$yMe=p4=)`)JUR*^WG1NSt9Xg+!ub}ic40X%Jt{}%ewBS5hfvew?%KO|E7oj0 zK-o7QpsL#P0Cqli?&Ay$&)B!1u&|Ks;-_4GPoF;h@Igy!GdiktUhnwvQFr$fjr*jf z@0#hUu)cXiSk79{{fxFqOCuJ9{;aJX1i9t@#*1N`Di{P$j#*nD1m!$-?BKzJui$Rx z>FKnCiXkbf9p6%XeU2Sl30q-+x!Tu9yB|G*x;#IBad7bbmI|gF6A1|*q1aavhK<1g z(IcTOFns84=>lox>>T*=1%jzNI=+7HpkT}&KYnOw>4D~I0q__A-x}Tm4iA7B<)s@G zw^y$K#Rjf%fWKqT1^!)j!2y3rBQq-_Jsma@e3tr@O2W{SXV9J%B|t3u58Vrj@+|t*HlUT3vO`F%g!E3L34xB%h37uOasW$SMa2a{0jC zE2*f+&(F-weMPH!4KRUX0O*xKRiIBuOad=*va?@iXJ)3QJxfhbPtQtA%gM>eg_OYy zr?@z$3>2%loQ`4qw2F6c-qln?E_JlJ>iQ40)zwWD46DNlgt~@0GR9b0SyD!Uvx`6t zH)@y<7WPUi#s+mSFD)-F&IcJN&WGCLEW+?9FXwe>@hixs9KwZP<>kG4RroRwuy>$^ zg(Xnnl9I9wEvzD8Y-Ob-rKRP72*i1~fvNZQEi}cuntFgD;0fr%2jKDnim(+Zf9)L| zK>F)wYw7IlZ0Q0>U2{z>6sfreYC)sf+CwN9?5Xgk4F5obn`(8<1}qQ2@*mqb5QsfJ zZH?XCpSpWKed_)49~k16-oCye80U3&e;ydk^!yJPdCd_jg=wIpV#~4OODRPXXqvQQBxnvj`-&evt z1|yu&$sa$!n`UNa7OMYaZhrpf!mlYDJJHKJ`VHIG1NW%{R2aEDDT&yL8uT&FyrXJ zz$k8k4MD%M^$+yJvBAMUI>yg3Iy5vkJTykaM81EY{Pyj~^yK8s92942YHAJ)GjJ=N znf~$P*RLrG#`tS)Zjp>JEG>|*Jr4=9)6=gX6DB4`Nf;xYJ~T+hn1_Z2`#*ict2n+6 zeI18N(!Y;>Cu7);o%@)6OwG8N%B5(-_8*qq232n3^Bg;n0sL53FRhmz$M3^L=dk_k)W~C8Q4f1I1?FG z1b}VF03$;%+cN<=j)y9YAIHJGE!FVUA#yp&&+gOY<1d}Eyv^H>VwWXA*X}cwY392 z`JsyZR4R3adSj8m?ti@kd97>Z_#;|1mq+4&P6M0J?nJuLo%OIk#fvw_j?J1CiHEW9 zV}|H=u)K59*{Y2J)7mPoKt#uXga9y*jN~@Vzsb zMf0wz_1UvEyMb){1qOX1Xk`35FPJk#iq#dQ*zN4D3QzXK4p5bH@h}7DuVj9NjybZm zj!o_*INM*;B-#{ZMhN|`w`5Sgrtv29@Pl#(26XP^TH?RESI3Qmm&~eUf>>Bag=1D9 z^MCmK&A9ug@menLMF+3>m+5~4SvF_wrEzb{lY&8f2z$e(5VpXkC!$H4mG%0B3p%*8 zf_)Edi->TwvNF&6it0YiT(n6mxP|_uXy+a4JRhK+zp_HojXP5P#`PgX9ua+RYig^L z6ZQyR2Fu{Bn3x|X-@ctMsyQO}xBN~^i&PX#g?QYbD1}-yl+V!b_b7Sap#8RcF7JNW)e$!h{j#=y z>?mRK;qYVcaGHp9$CtkxbWm(I@k|eEEsq;xY z1(7>ezw-*brQXnx@R2{2mBu(}_;bYQVEuM!>DS?gM3z{RWcqH6jXjzBJxy8$^6%^-=7)hVAZZP6R|>+(op02^;@?_R*MXuoIR^8ujA>t z|FqO`wH+w9-W-`GU1F_GnK^p%rjDwrl*@9v+^Tc4hliJ!;G5|@#W`a{rSbr0%G9Au zCLy7`s>6tzTb_}T`4*eYLe5Q#ngk*W?o)Bo&|vfXH__2kS`@z$!j_d-XG+)I3%`{t|0 z-Hh7Crq=d$SX%*RWbA{)P&a!AXOAb9&h8&Rtmc2ZXLGKs_;WIWk(1N>606tS1~Arb z7l%85w1UFEeFtIwa`*^*I;v*q?i&pE1IC!F!dE1Wy%?ru!0USR<}C%LRPUQUh+i)% zD(Rkt3C+q1#&33~Ygt$K-o3=HW8b+*$Zog7K3kZNj&TbRM_4T{hQg8+%qLbdMurV9 z-fU|}$_Edss#-w*ws}hUzi&xPOHNi(^SOER=c`?LYsT%{#vRnRfelituxEh#;|MsJ^VZ)n3=D#> zqGHYqbOsEukw}*&`5$-xn0IhEuS1J$eESvmOz%s{Z06eYk7TX% z+ir$un$a>bAaaS%D2v(a)~&kt;8fMqx8Kq4c}7NN=J@!PE3qAWj2%8s{#aR)KX?A3 zj48KiP2`}gZt{R}_> z0oyEZ%Bco5dD!zAzN1@SP9~8$71=p$;$OG6{Ww(1ek1T&a`BYkoxpn(OeErd^kWs+ zvckjD%IX}fSbBT=2RwK#C{L|n6%;&f0Tvub$Dz*7uED`=4gvyMFJ5hM?BDL}N7eP; zz)*Y4{i^p(K{aRR8$pQ~Sy?Z+__qps+zO12Nk-4IV8_6clF|y`^Y{JvwfK8^Wd(>_ z&^k;^!lt-8?^Ul`VJTFoc)_c84PRy$c5Y~6MLVN{itrwl{kFDu?%gjd?-}_^U=$M* zx3lw3d0zAm)zmk@l)SIMf8xi?U&2-*!gop?;g*y0111ktT3Y>Oe0*_nWK>XayTcV%Z||V!=;Y_wAG)@x?BB0yg{%taT!)4r z;_AYh#EGm#tUtd3tF%Aw5e}xYtBcnJ_%WS%^v5o)6kN>l~<#$LD z*0(S)dvH&0SVF)*X~PV%xJ1V=Ch&=0{sUzJTm2f`i~gxJEPHNO zBjEzs1`b`iv;k`(x9&iU~;2kSS@M8<_CO13mz)$nbGi` z4r9|s3PymwCu59YiEE=XL)ULqxC_S^VH1Z>V8R1q)t4{*V2L7u7z_^4H!=mToC=23 zz<{;vf5Xek(f;o-@c)jN3)aK1EpYu6`2XK{x&IX}C%eIu+q3Z^xA7vs@dCKG|F1tz z7y|{YHyD9J-nc3SL!@Smgwfcx(MV(pMxnaV$TWgRuyO5C<`u&%X6?%<^Yq{OQA>>kv8GAxBwM)F<>Q;C~Zb0;K*_F1d?qjd&g)L ztc#XOn&~1T+<*~sF#@9)#zi6lJZ%I0MW@J8aBgPOoKY$1Fz(Ju#;8;%I1M-8$8I{b zWMQN^i)8X@rlu^7hDUnuRK*?e?Q{l=yR?^vQ7FI_f|vY!{2>z!+5#A3j8HG84H(}q zJdcTDgi%-V{XCRj>;#7SQYPq4U>Kv}Vk|U_ic!;`R?Orm3{t_*F;K`P12ziQ=H;=G zB)*G@l1U*=kZd@hiFj$yjFKcO=^n0(x$T55f&p7T=0>4mq<%2rk$kv4NNzZtws9iG ziUJ7}XjFg#GE*qfk8LDY0tK3vk(0)2j&WT4LIKYUGJcIfJ3+_Kup1ejF?~nIXv-8_ zoCyQB2e=%0mC6!HqaGv*Eu0cSq0(qHWwHuyB?&s<{oG!h!hmCt3XMd-gBUOx9){OL z2Qje1t$~7>liuJS%%wO^(=o!&uuzvtL=3kBb0$XGh0#feNEqulg%X0}O*AqWq(a&0 z16~19-~b$opi3yckV_>Lh~hyb8)@obZEUnk5^Wnze=mu~9S&P_&yh2ybZAD9Bfg)J zM29yRCynfdfow(5F{t=wOb;L6fQEfvN+yx06sr1P+?I-Q(E#yBp?Z;M7}n-S>V{V2 zrS#IswfG1t{qRGKPUQ;0ABq~mql$?En199$`Op@dDEU|=l*WzDOBtQpXmd;u8U%8K z7LOv6e93h9U`vWBwZ(8DZaVa+v*R*ON~Tcg?{~HkXd4YmAYmT3rRY_tEyj72>P#Y0 zO4BG5d;%wu1W_Ai4w46VdcYr2Jjf0-j21~1GdgG=POJUL+zvQ9OZiEz~w6ftqY-7OaqFp<66Ku z%Ww->JW%eqwL3aY7v||cH)qP~sLqt+QM1qrJKrGU;H7-!bBV^@N zi9l+TAc`I$5eb$1FM#@y;e0tUFY3w>hf*|>|(jP(;V{V!3~$i_F(zJ0cLk2uNRi;Mq!_~`8Cx&bVhE>1!6Jl*G)*L}_c>PD1oi0Ugn-GbBG)@r3?yFntUwe>XWEq9xY)%3e^3{M-Mn~i#0>tbk#g2mj z`1n%n?V`?|GJF^pNxbcf;vNvSiDxO~SjSLvTvJ!$0=>rLV(y=aQ^}}!8LY7g1 zWJ^hDSy^ePQ)Wjo=}_zYp$X)t33pB@X(o^NcEToJ!FDc^(3P^1VkI^SmSoH1#}Blf1FXnm;(z$@n+gGvH*Q>WK1$Y& z&`xlRzi50);t3rdDQfhS#`La@|J{Qd_aHTTbrT)KP*`f`%W(sx=bpJTeT9bC2i(t} zO1dVJe?_)%_hRBy;z^VT&M0?Kf1708bY9Fi)dF1>t*yi2KY2^zs{4vAU5!M8do`UW z+}iH38F=$aeDIwZ`<}IP=fmjG{YnYWUS|Lu9FsHNBx_}yrM zb#k-7C3Us_qsLKTgKl(e<){?(1NRz}d3(&Yl)}Q9Y_S3A!0b<-(}Y%ZH`Yx$a=4~W zqfDlMx9bJ-1qs{Zl;Ztg5=%XTFTB=aMy<_xDK=V+saYmcGj_r1r?w}Z8ox)7S#64s zZ&--(DMT`rxXy}tJ$wGS$Chn>b#~YVV!2{PTr`Kf3%Jmg@fgK2wtxR}UfogH zccJ>Hp_7kg{?|j>Xsh0w3%O~(P;x*4m*v1r3WH>!bM#2b>zfrr1fe3w;_q%Mi7^IG zB@p4XPwu%VkKac5zZ-lT-yyB~YW~Xb{%@C$Hw#(mN5xj5VW!<5CSM5%s2l3Jw0@@x zxXM0X>5bNxv?|Fkv!m+7 z3K3E1*(BngfOp@oUCZGMTz(s>K0qdaJ^1_I$C#st!OTnrwp@G1`-yYkezWKgbIPM8 zL&nArnQQs^`IoZWP}rQo^SeHdlpNhy0)NnNRi&IIy~JRbkk@ywX@u^6-H);!-G4T4<{ zbTTfV-x9ZqrA^vZk}q2I^g6rrqYs5uKP4r6w*GxvX&@w!f4@=w;~z_6#~)TJ^?cxSbf^;O9IgrMWG zCb7J+`P$%zDeT-E$tUQ@xw%6jWCZocSuP+2Al8Py|=4@xvVlOm`HVS`;3Yjc7nkWJR&`0Gj9nM^s z59HWqy;cx8oFTR6`w)tH^`XHznp2RJJh2<4OL(vIAUC~YebMIYqI|Q;vD;j^b8@)X zb7Z8{xSdpy+VL%~mDY`gMk`ET$Lk0BB%HkKQW0>PF?2-un2MmFHTw58op(?XqBr(8 z_8fmB#FSW+@)P{K>V6XO`2%Xa21tM_;lokgb1(g4_xSpj?3fJFIq+U*LjSkc`*%!d zz6eF3`+i?LO=cAcrxsm<3R;)nXd0W9#8cT5TrF$eZf+)YqQ4M6^E6GGnnz)dzE>;# zzK#~?(zvCn`Cih&id;RDI>b3!cR;2=XK3)k$>=P(-wKISo@dW?huXM%p3IokzB`OG zUpS2H`TTis^30!*?B1TRLAptfFFG0)s(Qn@P}zJP&A4pcs#T}DBLBDQe%+nxY0^7y z-73AxUjDt@j#UKvOhm^;tb;#zg30sH{^}AFDN`zg%4q+f!&ZkeH7_llnjgqpz3Onn zi@E0hOyz?)TN~!6sa4_g5l(l_dXgzMRQtP#)|o46HHVE|<5?3eZ;($FXO_196s?vo zvVE97<0toSP`Una{AEN!3K=8>;N_@zV0^+Zki*4oO}e#IRNgNuuW53_W)VegmZJ!Mv&9s#NcLkyx8OC*$NK z4pAJu9v;;p?y96cH_Ji&-^j|eeUjnxEq;nE^JUyCT{bJ6Va;Ge2)(y$UE&o~6yH0PC^=bT( z-aKp~(Xq0hjBbkU44fA#6?#-vCDPvd`jdC~&U&2ZQ3k1hf3oKu#&3u;9i1YoVl!v; z%+lzkbkTrAZ@b+Q2h1+fA}NsLak&K&4ogp$R=H4btmj(j6}Mh79{0O#-!+?uSA^x; zk%TV+@&4MzE%>47_f`yByu+(9uTrg2beWZv^~UMPyX<=zzN0G-7^5n!ZfY0({aejg zojT6Co9VS2-%FLG$c5=@4pvU&=_U}%+PF|wMyMI7RgsRzZ1!Z_$~a}5=}7b&ia3{n zD!Hy+wL4S77jnQpT<}2l-vu_K7|OPr{j@3KwOvp6k0UcH9W5`1w84HAQ_h|{BK@1D z^osv>PKQ16<#`JNV2h+Xt4(fn>vgsrsOozXqVu!GCS>dTgP)}>LZ2Q?c&f03T(VBy zUA@q-BJ2R#zHP$@XlmM_(P zgZ85PqYzayGjlhZvpVszdl=mQTJ;Z2?!8vbTPBtdaR9sWZDtM&F-0w+Si%$Wy!mUV zzldxf8#9-VGiaAeC&>p;moh9YB!xRH&}aZWIKSRLprS>VwAxy{#(VikCaPwUBdZ7> zCgm`FMOL?O-+o01*`eyCKe_Grw9L55mps!5Vg9p6c4fCuHVUBHimUuaq-!_6c+9%X z6pJ?f3_3hKEINMfYKOv+GnO)IDDkwpSr9^p7B4A!CD=3{B1&4<=xoVmtCvX{E|{zT zfEH!%y=&(7jYx~lN#MUo>#@w#(>pBAB-kft#@J>2!x2pq&UUqjhQuv^={x3}g#4tF zOH!#TpG*kfd+Nx~p-pI6Ng|;U=jPd!Z&4~os~{>Xoyp18-rKi5<;oP>@_|$6hEUoL zQ{BH`kLlZlbt1Qe*8{hF5ekU=LJ;ZHkL7eaj4t>O$M4;p;;zAUMD4JHv03iYfyb6& zVKFVybLk#Z(VeK?Qfer0jvjxgiK|xu7u;T{;90qIU?R$dx>u})YnBJKE>j#gM@3Qn z9$9cwnfwTQURN+ZlH3zkIiUY8EP)V@`gYpIvhC&OekOBzZT0J;G3SCqEym;C0z%q$ z=`yn!n~@qPiL>5NaAJDfJi*G~F7udCv8t-lxg&L&#dvoTVI4gW>z^#n5G{Kdad2Cb zhKAI`FQ=ZmFn63e^SN~Kcq4x)5>HB^l3>AOOk#>CO4u3bZjmdq+g3BHQURQ7_x$^U z?*IAjt)kA$_=A;jDKY=f1x89)b_liH@uwyKbBV^-{H?o0#4Mbg@SN<^`u`3ezWV&? z+Q%zwBMe|BASCAPUkcl$)v~5flPs5)mwyBgWYLk_JL2@k(+7l15q@^~3l?KB`=J2~ zXFN1XSY5rE0z1O06qU~%~!EyBQlS|53^Jh+hhH{08k9PkWd}Paw3Vb&ozQet8|K!OL!g)e> zuh@ixWL1mHX@P*gwa=%lk+H*(Tbc@UUr!Pt!Q8;?l zY0tGo=~CmU%CvlYQ`@S{>~Xtvk{acwI!|wyU1y!2h`5lL;&Svo+&QlTH4OK7X;=Hl$N? zHPP}>qLQ>*+OhkTPLXrJcPZ>s^1V&u<5SD+e`|mai07U*7&dd)soTrSI(Abv$H4dT zgQ`~V$B}H|?!}DWNS^#BAh`JACe^w^_7EERw1k+`{GV!~)q@8LGK#JTT+!uL*~)>w zGa{q_ZM*G9LQR&q-g!2?ed=^MIIYO>-sb&CPQ2gb+xqfqkL%QQ1P9VFkGQWeJks^( zjrg5a?xXcfNO^Hs;r``EvvTI$YZI=gm3KC#?29qm)pLKp5H0knhyjYu$!b^n@`91$ zzSb4HtdBaz>3(DV*Ao-&XI*#tc!#hVNoDNd6!KJ^x}fbh>yC zCue?)`*4eL@#*?(Pydu$nvE+r(w5(K?w(`xgJXPCCL*OZ{Min)*FAj9f7{-@oAnf( zQbSo}@jO%0*P3a%Vp58I2@VA;&x3+gykreInzIaIV!BaeEn4kyFp!oOmW2()I7vy@ zOzVR_vKP%fZ|raNo9#sqfBrG6MoZ8AZm`=jD>@?5ar?QziOVN-gEx!c|7p^SB4*Mb zTxvxnE#~|lUE+6k^xV2ZJ7qnoFt7db_6Z1GK@2Y5H^;q8QAjG#@x8MfqJ@k5pdN#o~CC28H340xLD)GgV%_skzPTQ-_ z61I7kX24$V>%%wg{4edd=R z<3}zRZ+?G)_vbgWJJ=gM@O*xT(W?2-^;e(5U(CZG zj#>PSTl1Gr_rfR5)%LTqT@Ct8^FtN{c_}`P(G=FiVMOaGJXBKc{23UD@+SsZTJMWb z=kJR%EYk9QnUsrUmr<8aM3Im7h`&j{_~ti7X_w#jm6nQJFWceUb6WO9u|FHitpo`m z)LhriJM+(0i?%HCtRP=B_#_^qpIr3Jo+%f7X1mU#bTo9=IWZ^sV|hK>jv53$e#Gu3 zQu$@--@hq7CG@s3aO$dgNwd+{J0m*>j#-C)<-K&IAw>S^v3t*)A~ki;k+`gT_bO+) z_knuzVNW_a*Qu;czUo{+b`#cyVIWry~$ zO-Nz#?ZGjiSX;68=ExCI+#87y!nqVe0}(bXy!ZM39#r}LEP_2;>_|E-o=A3fOF@HxMIX5{nH3x|>9 zio=y&&-6>Kj~R)im!oa|j`n8S(~=QK3tM7p_Mdu*`exe4rWg1B@pUn*?DFw^rn-H1 z#mR%2HFdvh_H1Uk1DySofT16PJE|mo%G}78A8O^jJkJ??#&=-acgJGxuIy~YMgTvF zbMD8}@4dTC=@lS9yqz8PJkZ?6T>tiqP0eE`&;jR*aa;NM6UnX7!^2vA#3^^JQknJ} zFFz3(Z#6GcKBAwJ4{zPMf5*!uK29q+Iqj0TZzV^Q?sT=IeD;oAQ+jU^SwqC9?Dn&V zX@#a{V#0f8)Oor6r?*_cjPdXU?2tbE9R1Rv#PEEG@iM1$C)`_fR^rTzBb9WcN5LP#vS=~EFEn;sT<7^z z(cwpT=0a71{UjUi@?2Q?Hh6^>soVQWMj4MX1^CxieJyOcnpq!~6SBN8{&`beCcG?`|?}6CGKV~h08l_ zJBmf;J`_HvtL=v;(y)W<~mY)yFln#dxnctX$w3 znaS@QNhrK?u3+kdLHw8Faqjz2LN-W1!X;|Th}g`YBflOi2fUn2J+#v1vtVKP^5)eY z$Z7EeyZo6Z5g&q#Mp~ztyz`Sy=~w#-?ksp|aB*n_G@$)yN*ew*yz*WveGO1#Z&7)m z6mtCO^y4R&W3PxkThVSu+1c?we)u|1oVdZS2UEN)zq!wC`zg%GZtlEq-@bp&lgRc8 zU(+VdM-!uc(Qi!4rtVyM_;4uit_JgH;m7t}G0eNs2wnP4h^4XkP967K9emFXYE~Em zl3vpMpL4I~oPT>q1SN3ZUYFMjfrz4~O1IQVitU>})o%T5xxXa#p~acQJY49G?A>z* zLQ@YMShLlQipmbsj}aNnxf?OG4Z|1~?wleZ1x*&QDX|eHrW!gE)99$u@w>nGo%+an zpM9c1e?*`2J=%JJi+hQIL+Zq7wb1XJ=X?ncWo>(>-b`_D_|4CRP(zT$-P+XI^B#S@ zl0OHy_)Hf*@16M>ZaZxuFuV|aamo8PI;yd(ws&R4@A%l?Gw3hJ>Sf_wCq|0JUZkWek@sjc0Qrc3#ow72-AmGxe@vNf@JO{erIvMF z3)wlIu#^e1RXX3#7#ytrkBJRa5A-8`jckt2`L$2ynl`fw(cHZ6It zF*=DVk-kdAA6#gt|M1U&CZZpe6&0n6ME;b2{=D^5!IIQpvf2)RWi~dx(IZwRqWLqC z3tPn{#8CEG>=)m@ee#qMJ3G0cl=pp6!k$mDPg@s=AK7#X^ms`n+s6lBN)h%(Id7lOjNu1{Z=W`=G@-)2^GS4)5V zV9$Bj*f$!L^kn36Lu13i_m8={y_LA?&5*K5*rvOMo114+5AugH@l*MIZ-((Z%N{yw zYM~)(j+W4p)jyBw!-vVK$T~dhsKC*!9<%q4W<(3-#E|mUg{)p8jdP#;mbiIDG#7Zg7m4U%x|CL(3Z8$-xT(wiWl@^vTUK9xC=dGpHZept}YFu-ouVG`e%5 zyi@O9-iJk%I$wdA#9#NFouxxV7uJvZjNjcGETWGp&(d^93U~7zz>0%hFZPu-TKxE7 z@u@MvXR}2vqY#@3y6tYR+}&hldV5vbB;moS(v(ua*T$-g`yIcru?1Re4@L16LY|(U zC+zQ;#;I2;#PwA{bY_3m$-O0~O|FLC`p1nPvm}pRGwJgd7G{6h*cwPlJAJLePbtnc z&R-YREOSJnvS%HO-#<)rzAo-D}})DJYHFNJHsCS<}A>U7gcw#jRD_pM);OC8PyWgTkYO!qbC-hJK*Y zxXjR?nxF^yj`Kf~5|eVH!*f{*7itRWf7%=8ctguje&|R-enWon`-QUSZ{uE-MTQmu zh+}26tZFLsL1hpG$rC;daaELru2eA82&^PDM!qUcZfr`Zp9-&!2@VZSMvb4t-~5_b zi3}ZQ{rjfqb7SK#!b&AWWnE-XWLrlhp&nJ#vMq=6&xtF9m;L77(#}m-<>=z*Kc+hZ zOu$AsA0IDywe3pEVQJeBA*A^%4^M`mN?D3#jw^Lba?;#%B;2Qi-);$C;=zp1Q5 z1^)p8b}dg32)QLeV;LRwrPHrER{-{uQvc=cP%rDq=c3idj>tw>`Yi4Jmmbthjdt`4 zAAc}dTagl3(-`#NLuh++IP>z)iB?oSTTxQ;?hg&Jc<|=S+Jn%F656Md?r$|8H_|I9 zK_w}*bVB&9P27#4q3fN|#pU(C+un4R6Ml+{a{qigpRl9b4ik%i!|uL|c@*w@=w z7{2FMU?n0YKPoEfuy(X*>2G5~Pt3~FP-jVJN$~Rrp}|Qo@v921t_e=6o=8OXPhP~A z)z=km3dULze|+UFc{$H8HpaVY=1pkm+V4(g6iMd~4h_ysjSQWj24{Q)KTr5Q71F2#uVC*@~1rLgV)pMCg3=q`haNWp8mdXk>k{USJ9TvgVNEn5g4w$-aR!2z?4o7EXCJWFHVMi|G40HlUI-Z5+lJamSoR!Q#XL>{54PkKPekM00M?jHBz-98C#?>=-mW$PA-Ma2W|E0{ND zBrNRa4-Ye4A{{AdwdO2t`L+{%`}R$H#Dj@R(ZWKJJ1|+Guxe)W)vKLq{l9*hnm&7` z!MsgM%Jj8AIPzpfMna_TIvr0$Rjz7kuIpscI|3?petyczC-wptPTlW^ZE4^AkKIbP zXsN~}Xtr`+!t%0YD&s)P$I)vrf#BV4d$R8Wtrr+toKfIRIHJ{9sc&~1>cJefxaj4+ zf@9l?id@dI&5G*jeOoIipq)w*mQ;VUz8(~Ghm0Y(;^da$eVZg1WSQS~_cJXjrmZYDn`ve35pXjIpTI#XT}GIzWf9qQU_>BgU`uDMQrNOl|k;87lz+ zMa5$4RvRA@5Zg8Ky)i5}IQW4@aY~@`VO?GQXZM~5+!zZI!dywDBorH)5y#=^NaX0$ zCX*v0w{0^tF#*@7iVFAzQ@b$JjH;#bi znk4B)iwrox2K)pLZH_>JuufOexBTkrF!gS0Q>t-s30h8F20p{dl)gu!yplP67kZg@ zFi&^&j*PgtXlaQ;>kA5&mVW&BF%n!liIR|(JB~4mtw>u!p;+xUpqe!-R}69R*0C2`CCmBDL5Hva&WY zG!;gkxPWaX0hKHN54fBI5LB)xE-Pz)OU1a``1trXY+N1WoZ*W)9em~P+_@|RO~YEf zhb>Qt<#H;x9)s)r8NQLevu9;B4>{>mAj~Oc8)TUZK|6ULgIsEq?C$s3sxMnwihlq8 zHZtk<@XY5D%WPqg>D&6}OK zZgqCH(9oE;C0xUD2pF+b=g6S9>YL6zdq$#=Fl-a(Ek)%5l$8iuoUj}AJo!2G{>jfL zPoBKdZztcr&HZkBmXTk0MTH;IWIGxpjz+{RMCw+a7Q%w7wxpz|sq4AEU(Z{51({|#32 z&UVhvchZ7*I=s1O$z%b6cyaK)Lf^@l%B8~T9d~1|Z>KzDpEJE@2;K%VbKlDzomL0& z-C9dsXms`0dR<$~`0e%U*D+=eoi~42wLt59>+SVuF!+z&e$pAe$`c%H2C*`oH(Okm z?Df~zo0=lY7=kO!!zhDuW|N+o6T` zu57DbE!G5iVGz>X&|5AB#M60ko+5WB?(g5h!Qbz9`u~JV`A=)F)YXAUycqIkOOViv ziP2ShH}#~3XkL1py13W^NTsKrk)vZ8G8$lIou9vcJ)pQaIM`%l1b#uzBp5r3i|>Lo z+H6&RVAFNsBlPeH=%`kd! zrElH(#pbozw<|*eDZ;e!W@q<1m!zcy1?8D-rIL~;NgH~*Zmh=(B9=IH{l#xem)2{T znLl%=kZ30C&l2X2XJuV?8g#i(N5gVJZ;OgpL2rSC_2^Qgjtd<+dZFLCIB0mmyJV~G5}PeS+PH) z#L7(w3po?t$?Kc zbY*3wF7k#LDwc`Vg!dA=nwX%6c^W}&Vc0O=I4`cCAm#5jmkRRisDv zZyt=%d6}7coSd9PD-m!zW*!^cDJiK9?Hvz7Whf$o4kg-9Ff`vKz{5H=_K=;OpWnkH z@5M?uCuiENTbC}ST)T!$Lll^ow6*J#ZAlo*0;esBbj_FqUizT5#-7$V#zi=Cq?>0_ zoD`oZLrRo9#i-zvIgBGDM^GJaZ7e8go#vd=TU{smTKmcHRw=_O=%lxI$J_!kR9?*_ zMwz2wpcx(Ul~hn5R#g#^wIsLRpiBMeD1oP7uB4>9``Wemf{2Lj1tJMULdM2=dU_}0 zd3kvUpYZZN$*d=!<^bN_UOzvQH#|ELe{YD3X)dpK&5>qr?ec&KOAOo`Ao{PFR62 z-NIJWS+hs!ptOHRHhVLR^x`LZWn>^q!*{)d6HsmS z^-fE2iKJY@sIm(!HdY*|s+K4FnwpmQS|_uy#l{*NHv>(vc@+3_L?TdRd>O^zGl=4O zX3v_wRO@sc!xBMhd0`LMqUQ8;UIpN6*z*=tZG^di6bSXL`Vw3?te+4_lKlL@z915h zXqT4<_|Ex)($X?rm@QMChMSK`7I+s-ib|H33kj8%6F_NOTbGvLAp*V6P?8O45kP5e zQSK3FymBE^7pOP}w@Krg^wH6~Bo|OxKmNW{C#Ou$SYA-ti1G1p-VLR#nftRK>O z(h|R6`fx;JlxM-hlT@Kb?J7Mdr)$^7nVTmI3P=STN_)GzcL~Rk!rhuoh9j|-q6G!q zGAExnZCxGCbn`6m_P%|R_oQKRDwYjOo1VT3N((?r1-(Q|OKa2p=m-c_?l>ygP}<1? zIw&oXXgX~QEk&AJsA-)uZ6#qm2>BMJmpA|U(;CmJEdB@QXUL3#EH{`S;=KXo+DF`Y zb6xnW{QL+(LBqzz3*Fr=z1$r{Ht0Qg~#m1Hg1o-*2wkquBt!HCX zH8kw&tJ2WOGaUA3Nm^QRlKl=hVwT^$-?wjn89@3^dm9TjEuOx8daI!-84QQ z3J<||%)9KSr}u_+ZVl*;{Q4CZrYbK_{7$<6@|;nX%jf5?I{Ekdbp!QL>&-2ywh=n4 zd1cXy081`OV}+px5>nay)j%` za`0T*TGGYQq{6uLs-$Gb0r)?y?;>X{w{^!r>o_nlWbYdSwUw0>a`Jme6BFBBqonj= zt3Kc4^9gtN>S|*$hTsa5&K{}eP_2Z7dw!xv73PJTZjuv@A2%7_10C>}^{-k$oj}!& zN@!u8^P_>so4DzqwgKE5YAeGMPFo`dHi%1CadNh{MyR&D@qtRcaro;}RRz0*zD)9} zT-SI%HRYxq)->Pc;BhJQkcEXldiU;z#mvOSiy4o=y1Ko4nV8suf)o{X?%#)B*ilG> zkt-;u0fcdsGaBvxMTAisHVkT;TUu&Zf33Qj@DP}I2NFv2ZomO2P+KpanJsZgJ=SO^ zlVf6vi;E%7>eBuDx^9VXptfo;Ih(XfJV+{xuXtN{Pba);Z50yQwTlL7yJwG*5(JrP zf!e;f0BOO@g3kIW zEE8dIRn=ej{v}^;i-gNDmK`0mmoMLFX^AT;N;`3iwX#9ynQSd7Np=9q!KxbcqhR2H zu4ecbFQ2e{{P#25J*v1zz`SL`{T?Dd;^1KeLqP`GE_l|$Zg+vZ0>7J^+vN#=dNn>i z-kn!i{3mkD&n=*~rJiMLq(~E6dl!LF-Fx<$1w(sJ z*weNZ7T(v@?dUKw6E1SuGdpXx^nbDUCV*6STjTh1%=0`?=Lng~JY~o+PYD@QawH=2 zJX9P>X%J36pOIQTfO^K8qZ<9%g#-a^ z3k!jxe~hrKtZcKstgQZ##Bnz3%C?@Knws_oU}?i8dqVH(HXV_14~}WOJT&RM(?7GN zI}czG!>3>-y-$H!oZG-@LiR90mfW zwUM_ysw@aGloJ3HW_;7|HvCFF;1`%;JWgy+-$@?V-zh8N(R(F5ePAGdshWE$s1$c) zu-MED2tZ~&{HnFCBFECwUzAU;(Uf6Gd&hiD%@H^q-P=(idir9aOGU3=&&<4gxBJc= zc6J^{M(`g@<5m!-+_`fUaO0HEzHQPf<>{b>0qZe-kBn3Z|KQ{l%LP7(Dx8kYehUEP z2#B6IRwyjQ_mcj2j*_G#a7Yw;O?G)`XtIU11&FOC=we*S@O56aoB-4i1r#&qzst zYc{7cPHxGzo-PI~Mj;YbB6s#?DdxqC@_yRwDLr)BCbrx@M~{;E=qM*Aet>d;-16|K z0=X3y7Is%`9~huKbt%RibaOc=G0Hf({iY1sfn;T6MtlZP>XdMFsp9z=Cm^>mG3gfz z%2Cgs$6pb!XZHjq_A8tOvfjIQ?VEcDRS>?Kdv9iRZM zcjIYqZ`mNIJ<_lv@hl(6rR_4bLt=igC-?}c?92l{^Wi%}4|0HC@Eq{7SYBR+jb{V7 z1!lNxwyc7dmiki{-v{7BQo*Mw{oRhj9Kt|uNlR~LCWaRjgo6lmY+|;7p!kuI>I)Zw z0m0({S4~V$3*CCprB@ErjQWXH?T;vFu#C=NWo|1-yMTO@#wptqG1QPa>OAqr}082F>5r3c&s zqH+QdErZlch)U`;86y+;WSW@;a2Nvv1qBFm$O=9kW@F=E=LA8Lxw*M{`A`r!Fpm%j z{w)Fsh#Gv8E(Erx77*a!0YX9~EG&WsV-YY(AOgw&B_Vj3nVA?E=twv?fF(oD$0svJ z#LmtS7!BkS6%SuAWg{0A1!`PUQVL)#4agwM2fhf|CX9lJg!n*0QZ`mr9zI?$g$P6N z0{J8ci~CH7I!Z=bDk@4!a&i)43`7irtt_J;*mpn{zWI3&e|!$m3q%^9Sz02cprB-+ z1Rp_zUkVBm5`-iKX2zf;6bRQgGlRRFMMESIG4V8xbd*Sl0R|%@CHek+@jIaO;v9^O z5?NkYfKz{rIAoEm&Vz{bYK4fHD?9Pktg0#JmXU!E9HgAb4u)DD5*;9x>R z#O&;hjQs2n9TI{uaAiTn+`POX12OmlTNqS|aw|VCFApat&lX_>1QQhl8N|T{F@QjT zvO;1&5{bpPOGreRK{5ahSs)d1V9i)TQE3M#0}N+XKrT5IAcc@B8iIgH4edlj3K($K z0Dqc*o~o)KScQ^U_=-sz06!@;qIhKP}ZUA<9Bh@a0poIG9uR(heT;Q4n z>Vw*Fz*J~}Bm`W@g1uPOBAP|+fObY;SwPt$SPof)HOdHKk0munFks0T!DGaBEHM&63;7{Hj5q`}aAAV7II`ea zG6=~BxDdG=Ns0m`P$FPBB#5Z6bf6?n@QZLnfMo-O4`wgQ0Tv0xgP9YKPIkTM98ELtongxrlKV**@> z5y5gvVad#q_t2yma)c9t1VM)e@`>dHN*2TgB*tt*P=bO{2rL;hmKA&;j08h8ln8

    k* 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 z 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 literal 0 HcmV?d00001 From d9b339ee183f2dd276d5153d39b92ccd033e0f1f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Jul 2020 20:09:59 +0200 Subject: [PATCH 011/224] Add some more tests for hdf5 --- tests/data/test_history.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 003e90fbb..2e703c72b 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -713,6 +713,41 @@ def test_hdf5datahandler_trades_get_pairs(testdatadir): assert set(pairs) == {'XRP/ETH'} +def test_hdf5datahandler_trades_load(mocker, testdatadir, caplog): + dh = HDF5DataHandler(testdatadir) + trades = dh.trades_load('XRP/ETH') + assert isinstance(trades, list) + + +def test_hdf5datahandler_trades_store(mocker, testdatadir, caplog): + dh = HDF5DataHandler(testdatadir) + trades = dh.trades_load('XRP/ETH') + + dh.trades_store('XRP/NEW', trades) + file = testdatadir / 'XRP_NEW-trades.h5' + assert file.is_file() + # Load trades back + trades_new = dh.trades_load('XRP/NEW') + + assert len(trades_new) == len(trades) + assert trades[0][0] == trades_new[0][0] + assert trades[0][1] == trades_new[0][1] + # assert trades[0][2] == trades_new[0][2] # This is nan - so comparison does not make sense + assert trades[0][3] == trades_new[0][3] + assert trades[0][4] == trades_new[0][4] + assert trades[0][5] == trades_new[0][5] + assert trades[0][6] == trades_new[0][6] + assert trades[-1][0] == trades_new[-1][0] + assert trades[-1][1] == trades_new[-1][1] + # assert trades[-1][2] == trades_new[-1][2] # This is nan - so comparison does not make sense + assert trades[-1][3] == trades_new[-1][3] + assert trades[-1][4] == trades_new[-1][4] + assert trades[-1][5] == trades_new[-1][5] + assert trades[-1][6] == trades_new[-1][6] + + _clean_test_file(file) + + def test_gethandlerclass(): cl = get_datahandlerclass('json') assert cl == JsonDataHandler From ed33d4781d9bce146e85ee23ddc46e840e9c20c8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Jul 2020 20:19:34 +0200 Subject: [PATCH 012/224] Add more hdf5 tests --- tests/data/test_history.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 2e703c72b..3b20bd61d 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -670,7 +670,7 @@ def test_jsondatahandler_ohlcv_purge(mocker, testdatadir): assert unlinkmock.call_count == 1 -def test_jsondatahandler_trades_load(mocker, testdatadir, caplog): +def test_jsondatahandler_trades_load(testdatadir, caplog): dh = JsonGzDataHandler(testdatadir) logmsg = "Old trades format detected - converting" dh.trades_load('XRP/ETH') @@ -713,13 +713,13 @@ def test_hdf5datahandler_trades_get_pairs(testdatadir): assert set(pairs) == {'XRP/ETH'} -def test_hdf5datahandler_trades_load(mocker, testdatadir, caplog): +def test_hdf5datahandler_trades_load(testdatadir): dh = HDF5DataHandler(testdatadir) trades = dh.trades_load('XRP/ETH') assert isinstance(trades, list) -def test_hdf5datahandler_trades_store(mocker, testdatadir, caplog): +def test_hdf5datahandler_trades_store(testdatadir): dh = HDF5DataHandler(testdatadir) trades = dh.trades_load('XRP/ETH') @@ -748,6 +748,26 @@ def test_hdf5datahandler_trades_store(mocker, testdatadir, caplog): _clean_test_file(file) +def test_hdf5datahandler_ohlcv_load_and_resave(testdatadir): + dh = HDF5DataHandler(testdatadir) + ohlcv = dh.ohlcv_load('UNITTEST/BTC', '5m') + assert isinstance(ohlcv, DataFrame) + assert len(ohlcv) > 0 + + file = testdatadir / 'UNITTEST_NEW-5m.h5' + assert not file.is_file() + + dh.ohlcv_store('UNITTEST/NEW', '5m', ohlcv) + assert file.is_file() + + ohlcv1 = dh.ohlcv_load('UNITTEST/NEW', '5m') + # Account for the automatically dropped last candle + assert len(ohlcv) - 1 == len(ohlcv1) + assert ohlcv.iloc[:-1].equals(ohlcv1) + + _clean_test_file(file) + + def test_gethandlerclass(): cl = get_datahandlerclass('json') assert cl == JsonDataHandler From ae1c99bdd010c7f4e1bc122eb3e68a539a4f9912 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Jul 2020 20:36:30 +0200 Subject: [PATCH 013/224] more tests --- tests/data/test_history.py | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 3b20bd61d..41b20e35b 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -718,6 +718,23 @@ def test_hdf5datahandler_trades_load(testdatadir): trades = dh.trades_load('XRP/ETH') assert isinstance(trades, list) + trades1 = dh.trades_load('UNITTEST/NONEXIST') + assert trades1 == [] + # data goes from 2019-10-11 - 2019-10-13 + timerange = TimeRange.parse_timerange('20191011-20191012') + + trades2 = dh._trades_load('XRP/ETH', timerange) + assert len(trades) > len(trades2) + + # unfiltered load has trades before starttime + assert len([t for t in trades if t[0] < timerange.startts * 1000]) >= 0 + # filtered list does not have trades before starttime + assert len([t for t in trades2 if t[0] < timerange.startts * 1000]) == 0 + # unfiltered load has trades after endtime + assert len([t for t in trades if t[0] > timerange.stopts * 1000]) > 0 + # filtered list does not have trades after endtime + assert len([t for t in trades2 if t[0] > timerange.stopts * 1000]) == 0 + def test_hdf5datahandler_trades_store(testdatadir): dh = HDF5DataHandler(testdatadir) @@ -760,13 +777,25 @@ def test_hdf5datahandler_ohlcv_load_and_resave(testdatadir): dh.ohlcv_store('UNITTEST/NEW', '5m', ohlcv) assert file.is_file() - ohlcv1 = dh.ohlcv_load('UNITTEST/NEW', '5m') - # Account for the automatically dropped last candle - assert len(ohlcv) - 1 == len(ohlcv1) - assert ohlcv.iloc[:-1].equals(ohlcv1) + assert not ohlcv[ohlcv['date'] < '2018-01-15'].empty + + # Data gores from 2018-01-10 - 2018-01-30 + timerange = TimeRange.parse_timerange('20180115-20180119') + + # Call private function to ensure timerange is filtered in hdf5 + ohlcv = dh._ohlcv_load('UNITTEST/BTC', '5m', timerange) + ohlcv1 = dh._ohlcv_load('UNITTEST/NEW', '5m', timerange) + assert len(ohlcv) == len(ohlcv1) + assert ohlcv.equals(ohlcv1) + assert ohlcv[ohlcv['date'] < '2018-01-15'].empty + assert ohlcv[ohlcv['date'] > '2018-01-19'].empty _clean_test_file(file) + # Try loading inexisting file + ohlcv = dh.ohlcv_load('UNITTEST/NONEXIST', '5m') + assert ohlcv.empty + def test_gethandlerclass(): cl = get_datahandlerclass('json') From edb582e5229b18052dd82ee930d0af2fa859732d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Jul 2020 20:40:07 +0200 Subject: [PATCH 014/224] Add more tests --- tests/data/test_history.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 41b20e35b..c89156f4c 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -765,6 +765,18 @@ def test_hdf5datahandler_trades_store(testdatadir): _clean_test_file(file) +def test_hdf5datahandler_trades_purge(mocker, testdatadir): + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + unlinkmock = mocker.patch.object(Path, "unlink", MagicMock()) + dh = HDF5DataHandler(testdatadir) + assert not dh.trades_purge('UNITTEST/NONEXIST') + assert unlinkmock.call_count == 0 + + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + assert dh.trades_purge('UNITTEST/NONEXIST') + assert unlinkmock.call_count == 1 + + def test_hdf5datahandler_ohlcv_load_and_resave(testdatadir): dh = HDF5DataHandler(testdatadir) ohlcv = dh.ohlcv_load('UNITTEST/BTC', '5m') @@ -797,6 +809,18 @@ def test_hdf5datahandler_ohlcv_load_and_resave(testdatadir): assert ohlcv.empty +def test_hdf5datahandler_ohlcv_purge(mocker, testdatadir): + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + unlinkmock = mocker.patch.object(Path, "unlink", MagicMock()) + dh = HDF5DataHandler(testdatadir) + assert not dh.ohlcv_purge('UNITTEST/NONEXIST', '5m') + assert unlinkmock.call_count == 0 + + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + assert dh.ohlcv_purge('UNITTEST/NONEXIST', '5m') + assert unlinkmock.call_count == 1 + + def test_gethandlerclass(): cl = get_datahandlerclass('json') assert cl == JsonDataHandler From 119bf2a8ea66ecafc900677f998be2317c458f5b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Jul 2020 17:06:58 +0200 Subject: [PATCH 015/224] Document hdf5 dataformat --- docs/data-download.md | 118 ++++++++++++++-------- freqtrade/data/history/hdf5datahandler.py | 2 + freqtrade/data/history/history_utils.py | 8 +- 3 files changed, 84 insertions(+), 44 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index a2bbec837..0b22ec9ce 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -15,61 +15,91 @@ Otherwise `--exchange` becomes mandatory. ### Usage ``` -usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-p PAIRS [PAIRS ...]] - [--pairs-file FILE] [--days INT] [--dl-trades] [--exchange EXCHANGE] +usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] + [-d PATH] [--userdir PATH] + [-p PAIRS [PAIRS ...]] [--pairs-file FILE] + [--days INT] [--dl-trades] + [--exchange EXCHANGE] [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...]] - [--erase] [--data-format-ohlcv {json,jsongz}] [--data-format-trades {json,jsongz}] + [--erase] + [--data-format-ohlcv {json,jsongz,hdf5}] + [--data-format-trades {json,jsongz,hdf5}] optional arguments: -h, --help show this help message and exit -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] - Show profits for only these pairs. Pairs are space-separated. + Show profits for only these pairs. Pairs are space- + separated. --pairs-file FILE File containing a list of pairs to download. --days INT Download data for given number of days. - --dl-trades Download trades instead of OHLCV data. The bot will resample trades to the desired timeframe as specified as - --timeframes/-t. - --exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no config is provided. + --dl-trades Download trades instead of OHLCV data. The bot will + resample trades to the desired timeframe as specified + as --timeframes/-t. + --exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no + config is provided. -t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...] - Specify which tickers to download. Space-separated list. Default: `1m 5m`. - --erase Clean all existing data for the selected exchange/pairs/timeframes. - --data-format-ohlcv {json,jsongz} - Storage format for downloaded candle (OHLCV) data. (default: `json`). - --data-format-trades {json,jsongz} - Storage format for downloaded trades data. (default: `jsongz`). + Specify which tickers to download. Space-separated + list. Default: `1m 5m`. + --erase Clean all existing data for the selected + exchange/pairs/timeframes. + --data-format-ohlcv {json,jsongz,hdf5} + Storage format for downloaded candle (OHLCV) data. + (default: `json`). + --data-format-trades {json,jsongz,hdf5} + Storage format for downloaded trades data. (default: + `jsongz`). Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --logfile FILE Log to the file specified. Special values are: 'syslog', 'journald'. See the documentation for more details. + --logfile FILE Log to the file specified. Special values are: + 'syslog', 'journald'. See the documentation for more + details. -V, --version show program's version number and exit -c PATH, --config PATH - Specify configuration file (default: `config.json`). Multiple --config options may be used. Can be set to `-` - to read config from stdin. + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. -d PATH, --datadir PATH Path to directory with historical backtesting data. --userdir PATH, --user-data-dir PATH Path to userdata directory. + ``` ### Data format -Freqtrade currently supports 2 dataformats, `json` (plain "text" json files) and `jsongz` (a gzipped version of json files). +Freqtrade currently supports 3 data-formats for both OHLCV and trades data: + +* `json` (plain "text" json files) +* `jsongz` (a gzip-zipped version of json files) +* `hdf5` (a high performance datastore) + By default, OHLCV data is stored as `json` data, while trades data is stored as `jsongz` data. -This can be changed via the `--data-format-ohlcv` and `--data-format-trades` parameters respectivly. +This can be changed via the `--data-format-ohlcv` and `--data-format-trades` command line arguments respectively. +To persist this change, you can should also add the following snippet to your configuration, so you don't have to insert the above arguments each time: -If the default dataformat has been changed during download, then the keys `dataformat_ohlcv` and `dataformat_trades` in the configuration file need to be adjusted to the selected dataformat as well. +``` jsonc + // ... + "dataformat_ohlcv": "hdf5", + "dataformat_trades": "hdf5", + // ... +``` + +If the default data-format has been changed during download, then the keys `dataformat_ohlcv` and `dataformat_trades` in the configuration file need to be adjusted to the selected dataformat as well. !!! Note - You can convert between data-formats using the [convert-data](#subcommand-convert-data) and [convert-trade-data](#subcommand-convert-trade-data) methods. + You can convert between data-formats using the [convert-data](#sub-command-convert-data) and [convert-trade-data](#sub-command-convert-trade-data) methods. -#### Subcommand convert data +#### Sub-command convert data ``` usage: freqtrade convert-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-p PAIRS [PAIRS ...]] --format-from - {json,jsongz} --format-to {json,jsongz} - [--erase] + {json,jsongz,hdf5} --format-to + {json,jsongz,hdf5} [--erase] [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...]] optional arguments: @@ -77,9 +107,9 @@ optional arguments: -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] Show profits for only these pairs. Pairs are space- separated. - --format-from {json,jsongz} + --format-from {json,jsongz,hdf5} Source format for data conversion. - --format-to {json,jsongz} + --format-to {json,jsongz,hdf5} Destination format for data conversion. --erase Clean all existing data for the selected exchange/pairs/timeframes. @@ -94,9 +124,10 @@ Common arguments: details. -V, --version show program's version number and exit -c PATH, --config PATH - Specify configuration file (default: `config.json`). - Multiple --config options may be used. Can be set to - `-` to read config from stdin. + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. -d PATH, --datadir PATH Path to directory with historical backtesting data. --userdir PATH, --user-data-dir PATH @@ -112,23 +143,23 @@ It'll also remove original json data files (`--erase` parameter). freqtrade convert-data --format-from json --format-to jsongz --datadir ~/.freqtrade/data/binance -t 5m 15m --erase ``` -#### Subcommand convert-trade data +#### Sub-command convert trade data ``` usage: freqtrade convert-trade-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-p PAIRS [PAIRS ...]] --format-from - {json,jsongz} --format-to {json,jsongz} - [--erase] + {json,jsongz,hdf5} --format-to + {json,jsongz,hdf5} [--erase] optional arguments: -h, --help show this help message and exit -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] Show profits for only these pairs. Pairs are space- separated. - --format-from {json,jsongz} + --format-from {json,jsongz,hdf5} Source format for data conversion. - --format-to {json,jsongz} + --format-to {json,jsongz,hdf5} Destination format for data conversion. --erase Clean all existing data for the selected exchange/pairs/timeframes. @@ -140,13 +171,15 @@ Common arguments: details. -V, --version show program's version number and exit -c PATH, --config PATH - Specify configuration file (default: `config.json`). - Multiple --config options may be used. Can be set to - `-` to read config from stdin. + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. -d PATH, --datadir PATH Path to directory with historical backtesting data. --userdir PATH, --user-data-dir PATH Path to userdata directory. + ``` ##### Example converting trades @@ -158,21 +191,21 @@ It'll also remove original jsongz data files (`--erase` parameter). freqtrade convert-trade-data --format-from jsongz --format-to json --datadir ~/.freqtrade/data/kraken --erase ``` -### Subcommand list-data +### Sub-command list-data -You can get a list of downloaded data using the `list-data` subcommand. +You can get a list of downloaded data using the `list-data` sub-command. ``` usage: freqtrade list-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [--exchange EXCHANGE] - [--data-format-ohlcv {json,jsongz}] + [--data-format-ohlcv {json,jsongz,hdf5}] [-p PAIRS [PAIRS ...]] optional arguments: -h, --help show this help message and exit --exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no config is provided. - --data-format-ohlcv {json,jsongz} + --data-format-ohlcv {json,jsongz,hdf5} Storage format for downloaded candle (OHLCV) data. (default: `json`). -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] @@ -194,6 +227,7 @@ Common arguments: Path to directory with historical backtesting data. --userdir PATH, --user-data-dir PATH Path to userdata directory. + ``` #### Example list-data @@ -249,7 +283,7 @@ This will download historical candle (OHLCV) data for all the currency pairs you ### Other Notes - To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`. -- To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust ratelimits etc.) +- To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust rate limits etc.) - To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`. - To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days). - Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data. @@ -257,7 +291,7 @@ This will download historical candle (OHLCV) data for all the currency pairs you ### Trades (tick) data -By default, `download-data` subcommand downloads Candles (OHLCV) data. Some exchanges also provide historic trade-data via their API. +By default, `download-data` sub-command downloads Candles (OHLCV) data. Some exchanges also provide historic trade-data via their API. This data can be useful if you need many different timeframes, since it is only downloaded once, and then resampled locally to the desired timeframes. Since this data is large by default, the files use gzip by default. They are stored in your data-directory with the naming convention of `-trades.json.gz` (`ETH_BTC-trades.json.gz`). Incremental mode is also supported, as for historic OHLCV data, so downloading the data once per week with `--days 8` will create an incremental data-repository. diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index 6a4f45fa9..c55c0c1e5 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -59,6 +59,7 @@ class HDF5DataHandler(IDataHandler): _data = data.copy() filename = self._pair_data_filename(self._datadir, pair, timeframe) + ds = pd.HDFStore(filename, mode='a', complevel=9, complib='blosc') ds.put(key, _data.loc[:, self._columns], format='table', data_columns=['date']) @@ -139,6 +140,7 @@ class HDF5DataHandler(IDataHandler): column sequence as in DEFAULT_TRADES_COLUMNS """ key = self._pair_trades_key(pair) + ds = pd.HDFStore(self._pair_trades_filename(self._datadir, pair), mode='a', complevel=9, complib='blosc') ds.put(key, pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS), diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 58bd752ea..dd09c4c05 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -9,7 +9,8 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS -from freqtrade.data.converter import (ohlcv_to_dataframe, +from freqtrade.data.converter import (clean_ohlcv_dataframe, + ohlcv_to_dataframe, trades_remove_duplicates, trades_to_ohlcv) from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler @@ -202,7 +203,10 @@ def _download_pair_history(datadir: Path, if data.empty: data = new_dataframe else: - data = data.append(new_dataframe) + # Run cleaning again to ensure there were no duplicate candles + # Especially between existing and new data. + data = clean_ohlcv_dataframe(data.append(new_dataframe), timeframe, pair, + fill_missing=False, drop_incomplete=False) logger.debug("New Start: %s", f"{data.iloc[0]['date']:%Y-%m-%d %H:%M:%S}" if not data.empty else 'None') From bad89307dd752195100855a1c4f9cc942b9ef59d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Jul 2020 17:19:41 +0200 Subject: [PATCH 016/224] Fix mypy error --- freqtrade/data/history/hdf5datahandler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index c55c0c1e5..594a1598a 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -191,11 +191,11 @@ class HDF5DataHandler(IDataHandler): return False @classmethod - def _pair_ohlcv_key(cls, pair: str, timeframe: str) -> Path: + def _pair_ohlcv_key(cls, pair: str, timeframe: str) -> str: return f"{pair}/ohlcv/tf_{timeframe}" @classmethod - def _pair_trades_key(cls, pair: str) -> Path: + def _pair_trades_key(cls, pair: str) -> str: return f"{pair}/trades" @classmethod From 044df880e68d513d5e3baf2d451e656631e9194d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Aug 2020 08:28:46 +0200 Subject: [PATCH 017/224] Move persistence into it's own submodule --- freqtrade/persistence/__init__.py | 3 +++ freqtrade/{persistence.py => persistence/models.py} | 0 tests/test_persistence.py | 12 ++++++------ 3 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 freqtrade/persistence/__init__.py rename freqtrade/{persistence.py => persistence/models.py} (100%) diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py new file mode 100644 index 000000000..0973fab3f --- /dev/null +++ b/freqtrade/persistence/__init__.py @@ -0,0 +1,3 @@ +# flake8: noqa: F401 + +from freqtrade.persistence.models import Trade, clean_dry_run_db, cleanup, init diff --git a/freqtrade/persistence.py b/freqtrade/persistence/models.py similarity index 100% rename from freqtrade/persistence.py rename to freqtrade/persistence/models.py diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 65c83e05b..dbb62e636 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -22,7 +22,7 @@ def test_init_create_session(default_conf): def test_init_custom_db_url(default_conf, mocker): # Update path to a value other than default, but still in-memory default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'}) - create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) + create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) init(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 @@ -40,7 +40,7 @@ def test_init_prod_db(default_conf, mocker): default_conf.update({'dry_run': False}) default_conf.update({'db_url': constants.DEFAULT_DB_PROD_URL}) - create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) + create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) init(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 @@ -51,7 +51,7 @@ def test_init_dryrun_db(default_conf, mocker): default_conf.update({'dry_run': True}) default_conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL}) - create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) + create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) init(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 @@ -440,7 +440,7 @@ def test_migrate_old(mocker, default_conf, fee): amount=amount ) engine = create_engine('sqlite://') - mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine) + mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine) # Create table using the old format engine.execute(create_table_old) @@ -524,7 +524,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): amount=amount ) engine = create_engine('sqlite://') - mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine) + mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine) # Create table using the old format engine.execute(create_table_old) @@ -601,7 +601,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): amount=amount ) engine = create_engine('sqlite://') - mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine) + mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine) # Create table using the old format engine.execute(create_table_old) From 7d03a067ee107a7f2cd3dc48653a61b82e1988da Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Aug 2020 08:33:46 +0200 Subject: [PATCH 018/224] Extract migrations ot seperate module --- freqtrade/persistence/migrations.py | 113 ++++++++++++++++++++++++++++ freqtrade/persistence/models.py | 112 +-------------------------- 2 files changed, 116 insertions(+), 109 deletions(-) create mode 100644 freqtrade/persistence/migrations.py diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py new file mode 100644 index 000000000..bd7dd103f --- /dev/null +++ b/freqtrade/persistence/migrations.py @@ -0,0 +1,113 @@ +import logging +from typing import List + +from sqlalchemy import inspect + +logger = logging.getLogger(__name__) + + +def has_column(columns: List, searchname: str) -> bool: + return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1 + + +def get_column_def(columns: List, column: str, default: str) -> str: + return default if not has_column(columns, column) else column + + +def check_migrate(engine, decl_base) -> None: + """ + Checks if migration is necessary and migrates if necessary + """ + inspector = inspect(engine) + + cols = inspector.get_columns('trades') + tabs = inspector.get_table_names() + table_back_name = 'trades_bak' + for i, table_back_name in enumerate(tabs): + table_back_name = f'trades_bak{i}' + logger.debug(f'trying {table_back_name}') + + # Check for latest column + if not has_column(cols, 'amount_requested'): + logger.info(f'Running database migration - backup available as {table_back_name}') + + fee_open = get_column_def(cols, 'fee_open', 'fee') + fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null') + fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null') + fee_close = get_column_def(cols, 'fee_close', 'fee') + fee_close_cost = get_column_def(cols, 'fee_close_cost', 'null') + fee_close_currency = get_column_def(cols, 'fee_close_currency', 'null') + open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null') + close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null') + stop_loss = get_column_def(cols, 'stop_loss', '0.0') + stop_loss_pct = get_column_def(cols, 'stop_loss_pct', 'null') + initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0') + initial_stop_loss_pct = get_column_def(cols, 'initial_stop_loss_pct', 'null') + stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null') + stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null') + max_rate = get_column_def(cols, 'max_rate', '0.0') + min_rate = get_column_def(cols, 'min_rate', 'null') + sell_reason = get_column_def(cols, 'sell_reason', 'null') + strategy = get_column_def(cols, 'strategy', 'null') + # If ticker-interval existed use that, else null. + if has_column(cols, 'ticker_interval'): + timeframe = get_column_def(cols, 'timeframe', 'ticker_interval') + else: + timeframe = get_column_def(cols, 'timeframe', 'null') + + open_trade_price = get_column_def(cols, 'open_trade_price', + f'amount * open_rate * (1 + {fee_open})') + close_profit_abs = get_column_def( + cols, 'close_profit_abs', + f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}") + sell_order_status = get_column_def(cols, 'sell_order_status', 'null') + amount_requested = get_column_def(cols, 'amount_requested', 'amount') + + # Schema migration necessary + engine.execute(f"alter table trades rename to {table_back_name}") + # drop indexes on backup table + for index in inspector.get_indexes(table_back_name): + engine.execute(f"drop index {index['name']}") + # let SQLAlchemy create the schema as required + decl_base.metadata.create_all(engine) + + # Copy data back - following the correct schema + engine.execute(f"""insert into trades + (id, exchange, pair, is_open, + fee_open, fee_open_cost, fee_open_currency, + fee_close, fee_close_cost, fee_open_currency, open_rate, + open_rate_requested, close_rate, close_rate_requested, close_profit, + stake_amount, amount, amount_requested, open_date, close_date, open_order_id, + stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, + stoploss_order_id, stoploss_last_update, + max_rate, min_rate, sell_reason, sell_order_status, strategy, + timeframe, open_trade_price, close_profit_abs + ) + select id, lower(exchange), + case + when instr(pair, '_') != 0 then + substr(pair, instr(pair, '_') + 1) || '/' || + substr(pair, 1, instr(pair, '_') - 1) + else pair + end + pair, + is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost, + {fee_open_currency} fee_open_currency, {fee_close} fee_close, + {fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency, + open_rate, {open_rate_requested} open_rate_requested, close_rate, + {close_rate_requested} close_rate_requested, close_profit, + stake_amount, amount, {amount_requested}, open_date, close_date, open_order_id, + {stop_loss} stop_loss, {stop_loss_pct} stop_loss_pct, + {initial_stop_loss} initial_stop_loss, + {initial_stop_loss_pct} initial_stop_loss_pct, + {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, + {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, + {sell_order_status} sell_order_status, + {strategy} strategy, {timeframe} timeframe, + {open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs + from {table_back_name} + """) + + # Reread columns - the above recreated the table! + inspector = inspect(engine) + cols = inspector.get_columns('trades') diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 28753ed48..f56ff47fe 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional import arrow from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String, - create_engine, desc, func, inspect) + create_engine, desc, func) from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import Query @@ -18,6 +18,7 @@ from sqlalchemy.pool import StaticPool from freqtrade.exceptions import OperationalException from freqtrade.misc import safe_value_fallback +from freqtrade.persistence.migrations import check_migrate logger = logging.getLogger(__name__) @@ -58,120 +59,13 @@ def init(db_url: str, clean_open_orders: bool = False) -> None: Trade.session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) Trade.query = Trade.session.query_property() _DECL_BASE.metadata.create_all(engine) - check_migrate(engine) + check_migrate(engine, decl_base=_DECL_BASE) # Clean dry_run DB if the db is not in-memory if clean_open_orders and db_url != 'sqlite://': clean_dry_run_db() -def has_column(columns: List, searchname: str) -> bool: - return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1 - - -def get_column_def(columns: List, column: str, default: str) -> str: - return default if not has_column(columns, column) else column - - -def check_migrate(engine) -> None: - """ - Checks if migration is necessary and migrates if necessary - """ - inspector = inspect(engine) - - cols = inspector.get_columns('trades') - tabs = inspector.get_table_names() - table_back_name = 'trades_bak' - for i, table_back_name in enumerate(tabs): - table_back_name = f'trades_bak{i}' - logger.debug(f'trying {table_back_name}') - - # Check for latest column - if not has_column(cols, 'amount_requested'): - logger.info(f'Running database migration - backup available as {table_back_name}') - - fee_open = get_column_def(cols, 'fee_open', 'fee') - fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null') - fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null') - fee_close = get_column_def(cols, 'fee_close', 'fee') - fee_close_cost = get_column_def(cols, 'fee_close_cost', 'null') - fee_close_currency = get_column_def(cols, 'fee_close_currency', 'null') - open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null') - close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null') - stop_loss = get_column_def(cols, 'stop_loss', '0.0') - stop_loss_pct = get_column_def(cols, 'stop_loss_pct', 'null') - initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0') - initial_stop_loss_pct = get_column_def(cols, 'initial_stop_loss_pct', 'null') - stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null') - stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null') - max_rate = get_column_def(cols, 'max_rate', '0.0') - min_rate = get_column_def(cols, 'min_rate', 'null') - sell_reason = get_column_def(cols, 'sell_reason', 'null') - strategy = get_column_def(cols, 'strategy', 'null') - # If ticker-interval existed use that, else null. - if has_column(cols, 'ticker_interval'): - timeframe = get_column_def(cols, 'timeframe', 'ticker_interval') - else: - timeframe = get_column_def(cols, 'timeframe', 'null') - - open_trade_price = get_column_def(cols, 'open_trade_price', - f'amount * open_rate * (1 + {fee_open})') - close_profit_abs = get_column_def( - cols, 'close_profit_abs', - f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}") - sell_order_status = get_column_def(cols, 'sell_order_status', 'null') - amount_requested = get_column_def(cols, 'amount_requested', 'amount') - - # Schema migration necessary - engine.execute(f"alter table trades rename to {table_back_name}") - # drop indexes on backup table - for index in inspector.get_indexes(table_back_name): - engine.execute(f"drop index {index['name']}") - # let SQLAlchemy create the schema as required - _DECL_BASE.metadata.create_all(engine) - - # Copy data back - following the correct schema - engine.execute(f"""insert into trades - (id, exchange, pair, is_open, - fee_open, fee_open_cost, fee_open_currency, - fee_close, fee_close_cost, fee_open_currency, open_rate, - open_rate_requested, close_rate, close_rate_requested, close_profit, - stake_amount, amount, amount_requested, open_date, close_date, open_order_id, - stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, - stoploss_order_id, stoploss_last_update, - max_rate, min_rate, sell_reason, sell_order_status, strategy, - timeframe, open_trade_price, close_profit_abs - ) - select id, lower(exchange), - case - when instr(pair, '_') != 0 then - substr(pair, instr(pair, '_') + 1) || '/' || - substr(pair, 1, instr(pair, '_') - 1) - else pair - end - pair, - is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost, - {fee_open_currency} fee_open_currency, {fee_close} fee_close, - {fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency, - open_rate, {open_rate_requested} open_rate_requested, close_rate, - {close_rate_requested} close_rate_requested, close_profit, - stake_amount, amount, {amount_requested}, open_date, close_date, open_order_id, - {stop_loss} stop_loss, {stop_loss_pct} stop_loss_pct, - {initial_stop_loss} initial_stop_loss, - {initial_stop_loss_pct} initial_stop_loss_pct, - {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, - {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, - {sell_order_status} sell_order_status, - {strategy} strategy, {timeframe} timeframe, - {open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs - from {table_back_name} - """) - - # Reread columns - the above recreated the table! - inspector = inspect(engine) - cols = inspector.get_columns('trades') - - def cleanup() -> None: """ Flushes all pending operations to disk. From 171a52b21a076f6c20e21408b891258e40738bd6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Aug 2020 09:34:53 +0200 Subject: [PATCH 019/224] Introduce Order database model --- freqtrade/exchange/exchange.py | 1 + freqtrade/freqtradebot.py | 12 +++++- freqtrade/persistence/__init__.py | 3 +- freqtrade/persistence/models.py | 64 +++++++++++++++++++++++++++++-- 4 files changed, 74 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d32f79a3f..34d57ae4d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -487,6 +487,7 @@ class Exchange: 'side': side, 'remaining': _amount, 'datetime': arrow.utcnow().isoformat(), + 'timestamp': arrow.utcnow().timestamp, 'status': "closed" if ordertype == "market" else "open", 'fee': None, 'info': {} diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2a95f58fc..ff282aa77 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -22,7 +22,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.pairlist.pairlistmanager import PairListManager -from freqtrade.persistence import Trade +from freqtrade.persistence import Order, Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.state import State @@ -527,6 +527,7 @@ class FreqtradeBot: order = self.exchange.buy(pair=pair, ordertype=order_type, amount=amount, rate=buy_limit_requested, time_in_force=time_in_force) + order_obj = Order.parse_from_ccxt_object(order, pair) order_id = order['id'] order_status = order.get('status', None) @@ -580,6 +581,7 @@ class FreqtradeBot: strategy=self.strategy.get_strategy_name(), timeframe=timeframe_to_minutes(self.config['timeframe']) ) + trade.orders.append(order_obj) # Update fees if order is closed if order_status == 'closed': @@ -781,6 +783,9 @@ class FreqtradeBot: stoploss_order = self.exchange.stoploss(pair=trade.pair, amount=trade.amount, stop_price=stop_price, order_types=self.strategy.order_types) + + order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair) + trade.orders.append(order_obj) trade.stoploss_order_id = str(stoploss_order['id']) return True except InvalidOrderException as e: @@ -1123,12 +1128,15 @@ class FreqtradeBot: return False # Execute sell and update trade record - order = self.exchange.sell(pair=str(trade.pair), + order = self.exchange.sell(pair=trade.pair, ordertype=order_type, amount=amount, rate=limit, time_in_force=time_in_force ) + order_obj = Order.parse_from_ccxt_object(order, trade.pair) + trade.orders.append(order_obj) + trade.open_order_id = order['id'] trade.close_rate_requested = limit trade.sell_reason = sell_reason.value diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index 0973fab3f..764856f2b 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,3 +1,4 @@ # flake8: noqa: F401 -from freqtrade.persistence.models import Trade, clean_dry_run_db, cleanup, init +from freqtrade.persistence.models import (Order, Trade, clean_dry_run_db, + cleanup, init) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index f56ff47fe..3b77438ea 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -7,11 +7,11 @@ from decimal import Decimal from typing import Any, Dict, List, Optional import arrow -from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String, +from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String, ForeignKey, create_engine, desc, func) from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import Query +from sqlalchemy.orm import Query, relationship from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.session import sessionmaker from sqlalchemy.pool import StaticPool @@ -85,13 +85,71 @@ def clean_dry_run_db() -> None: trade.open_order_id = None +class Order(_DECL_BASE): + """ + Order database model + Keeps a record of all orders placed on the exchange + + One to many relationship with Trades: + - One trade can have many orders + - One Order can only be associated with one Trade + + Mirrors CCXT Order structure + """ + __tablename__ = 'orders' + + id = Column(Integer, primary_key=True) + trade_id = Column(Integer, ForeignKey('trades.id'), index=True) + + order_id = Column(String, nullable=False, index=True) + status = Column(String, nullable=False) + symbol = Column(String, nullable=False) + order_type = Column(String, nullable=False) + side = Column(String, nullable=False) + price = Column(Float, nullable=False) + amount = Column(Float, nullable=False) + filled = Column(Float, nullable=True) + remaining = Column(Float, nullable=True) + cost = Column(Float, nullable=True) + order_date = Column(DateTime, nullable=False, default=datetime.utcnow) + order_filled_date = Column(DateTime, nullable=True) + + @staticmethod + def parse_from_ccxt_object(order, pair) -> 'Order': + """ + Parse an order from a ccxt object and return a new order Object. + """ + o = Order(order_id=str(order['id'])) + + o.status = order['status'] + o.symbol = order.get('symbol', pair) + o.order_type = order['type'] + o.side = order['side'] + o.price = order['price'] + o.amount = order['amount'] + o.filled = order.get('filled') + o.remaining = order.get('remaining') + o.cost = order.get('cost') + o.order_date = datetime.fromtimestamp(order['timestamp']) + return o + + def __repr__(self): + + return (f'Order(id={self.id}, trade_id={self.trade_id}, side={self.side}, ' + f'status={self.status})') + + class Trade(_DECL_BASE): """ - Class used to define a trade structure + Trade database model. + Also handles updating and querying trades """ __tablename__ = 'trades' id = Column(Integer, primary_key=True) + + orders = relationship("Order", order_by="Order.id") + exchange = Column(String, nullable=False) pair = Column(String, nullable=False, index=True) is_open = Column(Boolean, nullable=False, default=True, index=True) From ed87abd93abe991f127fa9e0f1c8e03972e871d4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Aug 2020 09:41:12 +0200 Subject: [PATCH 020/224] Allow selecting only a certain table range in migration --- freqtrade/persistence/migrations.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index bd7dd103f..1bce8fef2 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -5,6 +5,9 @@ from sqlalchemy import inspect logger = logging.getLogger(__name__) +def get_table_names_for_table(inspector, tabletype): + return [t for t in inspector.get_table_names() if t.startswith(tabletype)] + def has_column(columns: List, searchname: str) -> bool: return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1 @@ -21,7 +24,7 @@ def check_migrate(engine, decl_base) -> None: inspector = inspect(engine) cols = inspector.get_columns('trades') - tabs = inspector.get_table_names() + tabs = get_table_names_for_table(inspector, 'trades') table_back_name = 'trades_bak' for i, table_back_name in enumerate(tabs): table_back_name = f'trades_bak{i}' From a66a3d047f0d65643c09d5fdd805307fc360bdc0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Aug 2020 09:43:48 +0200 Subject: [PATCH 021/224] Remove unneeded mocks --- tests/rpc/test_rpc_telegram.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index bfa774856..113232add 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -249,7 +249,6 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': 'mocked_order_id'}), get_fee=fee, ) msg_mock = MagicMock() @@ -1002,7 +1001,6 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': 'mocked_order_id'}), get_fee=fee, ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) From 420a8c2b1c20d0b2f9d87363f68731daccfff172 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Aug 2020 11:36:58 +0200 Subject: [PATCH 022/224] Improve tests for rpc/forcebuy --- tests/conftest.py | 1 + tests/rpc/test_rpc.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1ac8256a8..b8367f36d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -827,6 +827,7 @@ def limit_buy_order(): 'side': 'buy', 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), + 'timestamp': arrow.utcnow().timestamp, 'price': 0.00001099, 'amount': 90.99181073, 'filled': 90.99181073, diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 9bbd34672..5af6a42cb 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -817,7 +817,8 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None: def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order) -> None: default_conf['forcebuy_enable'] = True mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - buy_mm = MagicMock(return_value={'id': limit_buy_order['id']}) + limit_buy_order['status'] = 'open' + buy_mm = MagicMock(return_value=limit_buy_order) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), From ee7b235cdc0f3b782baaffd8fff185a18a86aeee Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Aug 2020 13:33:45 +0200 Subject: [PATCH 023/224] Improve tests to use open_order mock where applicable --- tests/conftest.py | 35 +++++++-- tests/rpc/test_rpc.py | 5 +- tests/test_freqtradebot.py | 150 ++++++++++++++++++------------------- 3 files changed, 103 insertions(+), 87 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b8367f36d..68b499dda 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -820,7 +820,7 @@ def markets_empty(): @pytest.fixture(scope='function') -def limit_buy_order(): +def limit_buy_order_open(): return { 'id': 'mocked_limit_buy', 'type': 'limit', @@ -830,13 +830,22 @@ def limit_buy_order(): 'timestamp': arrow.utcnow().timestamp, 'price': 0.00001099, 'amount': 90.99181073, - 'filled': 90.99181073, + 'filled': 0.0, 'cost': 0.0009999, - 'remaining': 0.0, - 'status': 'closed' + 'remaining': 90.99181073, + 'status': 'open' } +@pytest.fixture(scope='function') +def limit_buy_order(limit_buy_order_open): + order = deepcopy(limit_buy_order_open) + order['status'] = 'closed' + order['filled'] = order['amount'] + order['remaining'] = 0.0 + return order + + @pytest.fixture(scope='function') def market_buy_order(): return { @@ -1019,21 +1028,31 @@ def limit_buy_order_canceled_empty(request): @pytest.fixture -def limit_sell_order(): +def limit_sell_order_open(): return { 'id': 'mocked_limit_sell', 'type': 'limit', 'side': 'sell', 'pair': 'mocked', 'datetime': arrow.utcnow().isoformat(), + 'timestamp': arrow.utcnow().timestamp, 'price': 0.00001173, 'amount': 90.99181073, - 'filled': 90.99181073, - 'remaining': 0.0, - 'status': 'closed' + 'filled': 0.0, + 'remaining': 90.99181073, + 'status': 'open' } +@pytest.fixture +def limit_sell_order(limit_sell_order_open): + order = deepcopy(limit_sell_order_open) + order['remaining'] = 0.0 + order['filled'] = order['amount'] + order['status'] = 'closed' + return order + + @pytest.fixture def order_book_l2(): return MagicMock(return_value={ diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 5af6a42cb..9a3e50a9e 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -814,11 +814,10 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None: assert counts["current"] == 1 -def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order) -> None: +def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) -> None: default_conf['forcebuy_enable'] = True mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - limit_buy_order['status'] = 'open' - buy_mm = MagicMock(return_value=limit_buy_order) + buy_mm = MagicMock(return_value=limit_buy_order_open) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3f42aa889..40340fcd0 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -170,7 +170,7 @@ def test_get_trade_stake_amount(default_conf, ticker, mocker) -> None: (True, 0.0027, 3, 0.5, [0.001, 0.001, 0.000673]), (True, 0.0022, 3, 1, [0.001, 0.001, 0.0]), ]) -def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_buy_order, +def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_buy_order_open, amend_last, wallet, max_open, lsamr, expected) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -178,7 +178,7 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_balance=MagicMock(return_value=default_conf['stake_amount'] * 2), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee ) default_conf['dry_run_wallet'] = wallet @@ -216,13 +216,13 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: (0.50, 0.0025), ]) def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1, - limit_buy_order, fee, mocker) -> None: + limit_buy_order_open, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee ) @@ -303,7 +303,6 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf 'ask': buy_price * 0.79, 'last': buy_price * 0.79 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) ############################################# @@ -343,7 +342,6 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, 'ask': buy_price * 0.85, 'last': buy_price * 0.85 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) ############################################# @@ -362,8 +360,7 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, assert freqtrade.handle_trade(trade) is False -def test_total_open_trades_stakes(mocker, default_conf, ticker, - limit_buy_order, fee) -> None: +def test_total_open_trades_stakes(mocker, default_conf, ticker, fee) -> None: patch_RPCManager(mocker) patch_exchange(mocker) default_conf['stake_amount'] = 0.00098751 @@ -371,7 +368,6 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -534,7 +530,6 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> Non mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -568,7 +563,6 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -578,11 +572,11 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, freqtrade.create_trade('ETH/BTC') -def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, +def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) + buy_mock = MagicMock(return_value=limit_buy_order_open) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, @@ -598,11 +592,11 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, assert rate * amount <= default_conf['stake_amount'] -def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order, +def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order_open, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) + buy_mock = MagicMock(return_value=limit_buy_order_open) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, @@ -618,14 +612,14 @@ def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_ord assert not freqtrade.create_trade('ETH/BTC') -def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order, - fee, markets, mocker) -> None: +def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open, + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_balance=MagicMock(return_value=default_conf['stake_amount']), get_fee=fee, ) @@ -639,14 +633,14 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order, assert freqtrade.get_trade_stake_amount('ETH/BTC') == 0 -def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order, fee, +def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) @@ -702,7 +696,7 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None: @pytest.mark.parametrize("max_open", range(0, 5)) @pytest.mark.parametrize("tradable_balance_ratio,modifier", [(1.0, 1), (0.99, 0.8), (0.5, 0.5)]) -def test_create_trades_multiple_trades(default_conf, ticker, fee, mocker, +def test_create_trades_multiple_trades(default_conf, ticker, fee, mocker, limit_buy_order_open, max_open, tradable_balance_ratio, modifier) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -713,7 +707,7 @@ def test_create_trades_multiple_trades(default_conf, ticker, fee, mocker, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': "12355555"}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -727,14 +721,14 @@ def test_create_trades_multiple_trades(default_conf, ticker, fee, mocker, assert len(trades) == max(int(max_open * modifier), 0) -def test_create_trades_preopen(default_conf, ticker, fee, mocker) -> None: +def test_create_trades_preopen(default_conf, ticker, fee, mocker, limit_buy_order_open) -> None: patch_RPCManager(mocker) patch_exchange(mocker) default_conf['max_open_trades'] = 4 mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': "12355555"}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -754,14 +748,14 @@ def test_create_trades_preopen(default_conf, ticker, fee, mocker) -> None: assert len(trades) == 4 -def test_process_trade_creation(default_conf, ticker, limit_buy_order, +def test_process_trade_creation(default_conf, ticker, limit_buy_order, limit_buy_order_open, fee, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), fetch_order=MagicMock(return_value=limit_buy_order), get_fee=fee, ) @@ -824,14 +818,14 @@ def test_process_operational_exception(default_conf, ticker, mocker) -> None: assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]['status'] -def test_process_trade_handling(default_conf, ticker, limit_buy_order, fee, mocker) -> None: +def test_process_trade_handling(default_conf, ticker, limit_buy_order_open, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': limit_buy_order['id']}), - fetch_order=MagicMock(return_value=limit_buy_order), + buy=MagicMock(return_value=limit_buy_order_open), + fetch_order=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -970,7 +964,7 @@ def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid, assert not log_has("Using cached buy rate for ETH/BTC.", caplog) -def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: +def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order_open) -> None: patch_RPCManager(mocker) patch_exchange(mocker) freqtrade = FreqtradeBot(default_conf) @@ -983,7 +977,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: get_buy_rate=buy_rate_mock, _get_min_pair_stake_amount=MagicMock(return_value=1) ) - buy_mm = MagicMock(return_value={'id': limit_buy_order['id']}) + buy_mm = MagicMock(return_value=limit_buy_order_open) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=MagicMock(return_value={ @@ -1277,7 +1271,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, - limit_buy_order, limit_sell_order): + limit_buy_order_open, limit_sell_order): rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) sell_mock = MagicMock(return_value={'id': limit_sell_order['id']}) @@ -1288,7 +1282,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, 'ask': 0.00001173, 'last': 0.00001172 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), sell=sell_mock, get_fee=fee, fetch_order=MagicMock(return_value={'status': 'canceled'}), @@ -1829,7 +1823,7 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde assert not trade.is_open -def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mocker) -> None: +def test_handle_trade(default_conf, limit_buy_order, limit_sell_order_open, limit_sell_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -1839,8 +1833,8 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mock 'ask': 0.00001173, 'last': 0.00001172 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), - sell=MagicMock(return_value={'id': limit_sell_order['id']}), + buy=MagicMock(return_value=limit_buy_order), + sell=MagicMock(return_value=limit_sell_order_open), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -1869,13 +1863,13 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mock assert trade.close_date is not None -def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order, fee, mocker) -> None: +def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) @@ -1920,7 +1914,7 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order, fee, assert freqtrade.handle_trade(trades[0]) is True -def test_handle_trade_roi(default_conf, ticker, limit_buy_order, +def test_handle_trade_roi(default_conf, ticker, limit_buy_order_open, fee, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) @@ -1928,7 +1922,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) @@ -1953,14 +1947,14 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, def test_handle_trade_use_sell_signal( - default_conf, ticker, limit_buy_order, fee, mocker, caplog) -> None: + default_conf, ticker, limit_buy_order_open, fee, mocker, caplog) -> None: # use_sell_signal is True buy default caplog.set_level(logging.DEBUG) patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) @@ -1981,14 +1975,14 @@ def test_handle_trade_use_sell_signal( caplog) -def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, +def test_close_trade(default_conf, ticker, limit_buy_order, limit_buy_order_open, limit_sell_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -2804,7 +2798,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, fee, - limit_buy_order, mocker) -> None: + mocker) -> None: default_conf['exchange']['name'] = 'binance' rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) @@ -2926,7 +2920,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, } == last_msg -def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, +def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy_order_open, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -2937,7 +2931,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, 'ask': 0.00002173, 'last': 0.00002172 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) default_conf['ask_strategy'] = { @@ -2958,7 +2952,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, assert trade.sell_reason == SellType.SELL_SIGNAL.value -def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, +def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, limit_buy_order_open, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -2969,7 +2963,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, 'ask': 0.00002173, 'last': 0.00002172 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) default_conf['ask_strategy'] = { @@ -2989,7 +2983,8 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, assert trade.sell_reason == SellType.SELL_SIGNAL.value -def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker) -> None: +def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_order_open, + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -2999,7 +2994,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker 'ask': 0.00000173, 'last': 0.00000172 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) default_conf['ask_strategy'] = { @@ -3018,7 +3013,8 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker assert freqtrade.handle_trade(trade) is False -def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocker) -> None: +def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, limit_buy_order_open, + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -3028,7 +3024,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocke 'ask': 0.0000173, 'last': 0.0000172 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) default_conf['ask_strategy'] = { @@ -3050,7 +3046,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocke assert trade.sell_reason == SellType.SELL_SIGNAL.value -def test_sell_not_enough_balance(default_conf, limit_buy_order, +def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_open, fee, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -3061,7 +3057,7 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, 'ask': 0.00002173, 'last': 0.00002172 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) @@ -3169,7 +3165,8 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo assert log_has(f"Pair {trade.pair} is currently locked.", caplog) -def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) -> None: +def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_buy_order_open, + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -3179,7 +3176,7 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) -> 'ask': 0.0000173, 'last': 0.0000172 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) default_conf['ask_strategy'] = { @@ -3203,7 +3200,8 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) -> assert trade.sell_reason == SellType.ROI.value -def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) -> None: +def test_trailing_stop_loss(default_conf, limit_buy_order_open, limit_buy_order, + fee, caplog, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -3213,7 +3211,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) 'ask': 0.00001099, 'last': 0.00001099 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) default_conf['trailing_stop'] = True @@ -3253,7 +3251,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value -def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, +def test_trailing_stop_loss_positive(default_conf, limit_buy_order, limit_buy_order_open, fee, caplog, mocker) -> None: buy_price = limit_buy_order['price'] patch_RPCManager(mocker) @@ -3265,7 +3263,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, 'ask': buy_price - 0.000001, 'last': buy_price - 0.000001 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) default_conf['trailing_stop'] = True @@ -3310,7 +3308,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, f"initial stoploss was at 0.000010, trade opened at 0.000011", caplog) -def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, +def test_trailing_stop_loss_offset(default_conf, limit_buy_order, limit_buy_order_open, fee, caplog, mocker) -> None: buy_price = limit_buy_order['price'] patch_RPCManager(mocker) @@ -3322,7 +3320,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, 'ask': buy_price - 0.000001, 'last': buy_price - 0.000001 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) patch_whitelist(mocker, default_conf) @@ -3368,7 +3366,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value -def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, +def test_tsl_only_offset_reached(default_conf, limit_buy_order, limit_buy_order_open, fee, caplog, mocker) -> None: buy_price = limit_buy_order['price'] # buy_price: 0.00001099 @@ -3382,7 +3380,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, 'ask': buy_price, 'last': buy_price }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) patch_whitelist(mocker, default_conf) @@ -3431,7 +3429,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, assert trade.stop_loss == 0.0000117705 -def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, +def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_buy_order_open, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -3442,7 +3440,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, 'ask': 0.00000173, 'last': 0.00000172 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) default_conf['ask_strategy'] = { @@ -3784,8 +3782,8 @@ def test_apply_fee_conditional(default_conf, fee, caplog, mocker, assert walletmock.call_count == 1 -def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, mocker, - order_book_l2): +def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order_open, limit_buy_order, + fee, mocker, order_book_l2): default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = 0.1 patch_RPCManager(mocker) @@ -3794,7 +3792,7 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) @@ -3909,8 +3907,8 @@ def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> None: assert freqtrade._check_depth_of_market_buy('ETH/BTC', conf) is False -def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order, - fee, mocker, order_book_l2, caplog) -> None: +def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_order, fee, + limit_sell_order_open, mocker, order_book_l2, caplog) -> None: """ test order book ask strategy """ @@ -3929,8 +3927,8 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order 'ask': 0.00001173, 'last': 0.00001172 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), - sell=MagicMock(return_value={'id': limit_sell_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), + sell=MagicMock(return_value=limit_sell_order_open), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -4072,7 +4070,7 @@ def test_startup_trade_reinit(default_conf, edge_conf, mocker): @pytest.mark.usefixtures("init_persistence") -def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order, caplog): +def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order_open, caplog): default_conf['dry_run'] = True # Initialize to 2 times stake amount default_conf['dry_run_wallet'] = 0.002 @@ -4082,7 +4080,7 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + buy=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) From 4924d8487ee25f84e4d1e4b2dcce62603744f4ed Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Aug 2020 13:39:36 +0200 Subject: [PATCH 024/224] Extract "update order from ccxt" to it's onw function --- freqtrade/freqtradebot.py | 6 ++-- freqtrade/persistence/models.py | 49 +++++++++++++++++++++------------ tests/test_freqtradebot.py | 8 ++++-- 3 files changed, 39 insertions(+), 24 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ff282aa77..c1a898c30 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -527,7 +527,7 @@ class FreqtradeBot: order = self.exchange.buy(pair=pair, ordertype=order_type, amount=amount, rate=buy_limit_requested, time_in_force=time_in_force) - order_obj = Order.parse_from_ccxt_object(order, pair) + order_obj = Order.parse_from_ccxt_object(order, 'buy') order_id = order['id'] order_status = order.get('status', None) @@ -784,7 +784,7 @@ class FreqtradeBot: stop_price=stop_price, order_types=self.strategy.order_types) - order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair) + order_obj = Order.parse_from_ccxt_object(stoploss_order, 'stoploss') trade.orders.append(order_obj) trade.stoploss_order_id = str(stoploss_order['id']) return True @@ -1134,7 +1134,7 @@ class FreqtradeBot: time_in_force=time_in_force ) - order_obj = Order.parse_from_ccxt_object(order, trade.pair) + order_obj = Order.parse_from_ccxt_object(order, 'sell') trade.orders.append(order_obj) trade.open_order_id = order['id'] diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 3b77438ea..c50fcbe88 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -101,36 +101,49 @@ class Order(_DECL_BASE): id = Column(Integer, primary_key=True) trade_id = Column(Integer, ForeignKey('trades.id'), index=True) + ft_order_side = Column(String, nullable=False) + order_id = Column(String, nullable=False, index=True) - status = Column(String, nullable=False) - symbol = Column(String, nullable=False) - order_type = Column(String, nullable=False) - side = Column(String, nullable=False) - price = Column(Float, nullable=False) - amount = Column(Float, nullable=False) + status = Column(String, nullable=True) + symbol = Column(String, nullable=True) + order_type = Column(String, nullable=True) + side = Column(String, nullable=True) + price = Column(Float, nullable=True) + amount = Column(Float, nullable=True) filled = Column(Float, nullable=True) remaining = Column(Float, nullable=True) cost = Column(Float, nullable=True) order_date = Column(DateTime, nullable=False, default=datetime.utcnow) order_filled_date = Column(DateTime, nullable=True) + def update_from_ccxt_object(self, order): + """ + Update Order from ccxt response + Only updates if fields are available from ccxt - + """ + if self.order_id != str(order['id']): + return OperationalException("Order-id's don't match") + + self.status = order.get('status', self.status) + self.symbol = order.get('symbol', self.symbol) + self.order_type = order.get('type', self.order_type) + self.side = order.get('side', self.side) + self.price = order.get('price', self.price) + self.amount = order.get('amount', self.amount) + self.filled = order.get('filled', self.filled) + self.remaining = order.get('remaining', self.remaining) + self.cost = order.get('cost', self.cost) + if 'timestamp' in order and order['timestamp'] is not None: + self.order_date = datetime.fromtimestamp(order['timestamp']) + @staticmethod - def parse_from_ccxt_object(order, pair) -> 'Order': + def parse_from_ccxt_object(order: Dict[str, Any], side: str) -> 'Order': """ Parse an order from a ccxt object and return a new order Object. """ - o = Order(order_id=str(order['id'])) + o = Order(order_id=str(order['id']), ft_order_side=side) - o.status = order['status'] - o.symbol = order.get('symbol', pair) - o.order_type = order['type'] - o.side = order['side'] - o.price = order['price'] - o.amount = order['amount'] - o.filled = order.get('filled') - o.remaining = order.get('remaining') - o.cost = order.get('cost') - o.order_date = datetime.fromtimestamp(order['timestamp']) + o.update_from_ccxt_object(order) return o def __repr__(self): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 40340fcd0..1621be6e5 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1313,7 +1313,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, assert rpc_mock.call_args_list[1][0][0]['order_type'] == 'market' -def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, +def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, limit_buy_order, limit_sell_order) -> None: # When trailing stoploss is set stoploss = MagicMock(return_value={'id': 13434334}) @@ -1823,7 +1823,8 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde assert not trade.is_open -def test_handle_trade(default_conf, limit_buy_order, limit_sell_order_open, limit_sell_order, fee, mocker) -> None: +def test_handle_trade(default_conf, limit_buy_order, limit_sell_order_open, limit_sell_order, + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -1863,7 +1864,8 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order_open, limi assert trade.close_date is not None -def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, fee, mocker) -> None: +def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( From 396e781bf43acc00776fde1545bfd91deac2dab2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Aug 2020 14:13:58 +0200 Subject: [PATCH 025/224] Update orders --- freqtrade/exchange/exchange.py | 7 +++---- freqtrade/freqtradebot.py | 1 + freqtrade/persistence/models.py | 25 +++++++++++++++++++------ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 34d57ae4d..533377746 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -8,7 +8,6 @@ import logging from copy import deepcopy from datetime import datetime, timezone from math import ceil -from random import randint from typing import Any, Dict, List, Optional, Tuple import arrow @@ -474,11 +473,11 @@ class Exchange: def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, params: Dict = {}) -> Dict[str, Any]: - order_id = f'dry_run_{side}_{randint(0, 10**6)}' + order_id = f'dry_run_{side}_{datetime.now().timestamp()}' _amount = self.amount_to_precision(pair, amount) dry_order = { - "id": order_id, - 'pair': pair, + 'id': order_id, + 'symbol': pair, 'price': rate, 'average': rate, 'amount': _amount, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c1a898c30..e2d504916 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1260,6 +1260,7 @@ class FreqtradeBot: except InvalidOrderException as exception: logger.warning('Unable to fetch order %s: %s', order_id, exception) return False + Order.update_order(order) # Try update amount (binance-fix) try: new_amount = self.get_real_amount(trade, order, order_amount) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index c50fcbe88..12bbd8adc 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -58,6 +58,10 @@ def init(db_url: str, clean_open_orders: bool = False) -> None: # We should use the scoped_session object - not a seperately initialized version Trade.session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) Trade.query = Trade.session.query_property() + # Copy session attributes to order object too + Order.session = Trade.session + Order.query = Order.session.query_property() + _DECL_BASE.metadata.create_all(engine) check_migrate(engine, decl_base=_DECL_BASE) @@ -103,7 +107,7 @@ class Order(_DECL_BASE): ft_order_side = Column(String, nullable=False) - order_id = Column(String, nullable=False, index=True) + order_id = Column(String, nullable=False, unique=True, index=True) status = Column(String, nullable=True) symbol = Column(String, nullable=True) order_type = Column(String, nullable=True) @@ -115,6 +119,12 @@ class Order(_DECL_BASE): cost = Column(Float, nullable=True) order_date = Column(DateTime, nullable=False, default=datetime.utcnow) order_filled_date = Column(DateTime, nullable=True) + order_update_date = Column(DateTime, nullable=True) + + def __repr__(self): + + return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.trade_id}, ' + f'side={self.side}, status={self.status})') def update_from_ccxt_object(self, order): """ @@ -136,6 +146,14 @@ class Order(_DECL_BASE): if 'timestamp' in order and order['timestamp'] is not None: self.order_date = datetime.fromtimestamp(order['timestamp']) + @staticmethod + def update_order(order: Dict[str, Any]): + """ + """ + oobj = Order.query.filter(Order.order_id == order['id']).first() + oobj.update_from_ccxt_object(order) + oobj.order_update_date = datetime.now() + @staticmethod def parse_from_ccxt_object(order: Dict[str, Any], side: str) -> 'Order': """ @@ -146,11 +164,6 @@ class Order(_DECL_BASE): o.update_from_ccxt_object(order) return o - def __repr__(self): - - return (f'Order(id={self.id}, trade_id={self.trade_id}, side={self.side}, ' - f'status={self.status})') - class Trade(_DECL_BASE): """ From 73182bb2ddc41e05067bc317265ed445d5a2196a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Aug 2020 14:50:57 +0200 Subject: [PATCH 026/224] Update migrations to populate Orders table for open orders --- freqtrade/persistence/migrations.py | 201 ++++++++++++++++------------ freqtrade/persistence/models.py | 10 +- tests/test_persistence.py | 34 +++-- 3 files changed, 148 insertions(+), 97 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 1bce8fef2..55825436d 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -5,6 +5,7 @@ from sqlalchemy import inspect logger = logging.getLogger(__name__) + def get_table_names_for_table(inspector, tabletype): return [t for t in inspector.get_table_names() if t.startswith(tabletype)] @@ -17,7 +18,110 @@ def get_column_def(columns: List, column: str, default: str) -> str: return default if not has_column(columns, column) else column -def check_migrate(engine, decl_base) -> None: +def get_backup_name(tabs, backup_prefix: str): + table_back_name = backup_prefix + for i, table_back_name in enumerate(tabs): + table_back_name = f'{backup_prefix}{i}' + logger.debug(f'trying {table_back_name}') + + return table_back_name + + +def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, cols: List): + fee_open = get_column_def(cols, 'fee_open', 'fee') + fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null') + fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null') + fee_close = get_column_def(cols, 'fee_close', 'fee') + fee_close_cost = get_column_def(cols, 'fee_close_cost', 'null') + fee_close_currency = get_column_def(cols, 'fee_close_currency', 'null') + open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null') + close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null') + stop_loss = get_column_def(cols, 'stop_loss', '0.0') + stop_loss_pct = get_column_def(cols, 'stop_loss_pct', 'null') + initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0') + initial_stop_loss_pct = get_column_def(cols, 'initial_stop_loss_pct', 'null') + stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null') + stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null') + max_rate = get_column_def(cols, 'max_rate', '0.0') + min_rate = get_column_def(cols, 'min_rate', 'null') + sell_reason = get_column_def(cols, 'sell_reason', 'null') + strategy = get_column_def(cols, 'strategy', 'null') + # If ticker-interval existed use that, else null. + if has_column(cols, 'ticker_interval'): + timeframe = get_column_def(cols, 'timeframe', 'ticker_interval') + else: + timeframe = get_column_def(cols, 'timeframe', 'null') + + open_trade_price = get_column_def(cols, 'open_trade_price', + f'amount * open_rate * (1 + {fee_open})') + close_profit_abs = get_column_def( + cols, 'close_profit_abs', + f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}") + sell_order_status = get_column_def(cols, 'sell_order_status', 'null') + amount_requested = get_column_def(cols, 'amount_requested', 'amount') + + # Schema migration necessary + engine.execute(f"alter table trades rename to {table_back_name}") + # drop indexes on backup table + for index in inspector.get_indexes(table_back_name): + engine.execute(f"drop index {index['name']}") + # let SQLAlchemy create the schema as required + decl_base.metadata.create_all(engine) + + # Copy data back - following the correct schema + engine.execute(f"""insert into trades + (id, exchange, pair, is_open, + fee_open, fee_open_cost, fee_open_currency, + fee_close, fee_close_cost, fee_open_currency, open_rate, + open_rate_requested, close_rate, close_rate_requested, close_profit, + stake_amount, amount, amount_requested, open_date, close_date, open_order_id, + stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, + stoploss_order_id, stoploss_last_update, + max_rate, min_rate, sell_reason, sell_order_status, strategy, + timeframe, open_trade_price, close_profit_abs + ) + select id, lower(exchange), + case + when instr(pair, '_') != 0 then + substr(pair, instr(pair, '_') + 1) || '/' || + substr(pair, 1, instr(pair, '_') - 1) + else pair + end + pair, + is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost, + {fee_open_currency} fee_open_currency, {fee_close} fee_close, + {fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency, + open_rate, {open_rate_requested} open_rate_requested, close_rate, + {close_rate_requested} close_rate_requested, close_profit, + stake_amount, amount, {amount_requested}, open_date, close_date, open_order_id, + {stop_loss} stop_loss, {stop_loss_pct} stop_loss_pct, + {initial_stop_loss} initial_stop_loss, + {initial_stop_loss_pct} initial_stop_loss_pct, + {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, + {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, + {sell_order_status} sell_order_status, + {strategy} strategy, {timeframe} timeframe, + {open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs + from {table_back_name} + """) + + +def migrate_open_orders_to_trades(engine): + engine.execute(""" + insert into orders (trade_id, order_id, ft_order_side) + select id, open_order_id, + case when close_rate_requested is null then 'buy' + else 'sell' end ft_order_side + from trades + where open_order_id is not null + union all + select id, stoploss_order_id, 'stoploss' + from trades + where stoploss_order_id is not null + """) + + +def check_migrate(engine, decl_base, previous_tables) -> None: """ Checks if migration is necessary and migrates if necessary """ @@ -25,92 +129,21 @@ def check_migrate(engine, decl_base) -> None: cols = inspector.get_columns('trades') tabs = get_table_names_for_table(inspector, 'trades') - table_back_name = 'trades_bak' - for i, table_back_name in enumerate(tabs): - table_back_name = f'trades_bak{i}' - logger.debug(f'trying {table_back_name}') + table_back_name = get_backup_name(tabs, 'trades_bak') # Check for latest column if not has_column(cols, 'amount_requested'): - logger.info(f'Running database migration - backup available as {table_back_name}') - - fee_open = get_column_def(cols, 'fee_open', 'fee') - fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null') - fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null') - fee_close = get_column_def(cols, 'fee_close', 'fee') - fee_close_cost = get_column_def(cols, 'fee_close_cost', 'null') - fee_close_currency = get_column_def(cols, 'fee_close_currency', 'null') - open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null') - close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null') - stop_loss = get_column_def(cols, 'stop_loss', '0.0') - stop_loss_pct = get_column_def(cols, 'stop_loss_pct', 'null') - initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0') - initial_stop_loss_pct = get_column_def(cols, 'initial_stop_loss_pct', 'null') - stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null') - stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null') - max_rate = get_column_def(cols, 'max_rate', '0.0') - min_rate = get_column_def(cols, 'min_rate', 'null') - sell_reason = get_column_def(cols, 'sell_reason', 'null') - strategy = get_column_def(cols, 'strategy', 'null') - # If ticker-interval existed use that, else null. - if has_column(cols, 'ticker_interval'): - timeframe = get_column_def(cols, 'timeframe', 'ticker_interval') - else: - timeframe = get_column_def(cols, 'timeframe', 'null') - - open_trade_price = get_column_def(cols, 'open_trade_price', - f'amount * open_rate * (1 + {fee_open})') - close_profit_abs = get_column_def( - cols, 'close_profit_abs', - f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}") - sell_order_status = get_column_def(cols, 'sell_order_status', 'null') - amount_requested = get_column_def(cols, 'amount_requested', 'amount') - - # Schema migration necessary - engine.execute(f"alter table trades rename to {table_back_name}") - # drop indexes on backup table - for index in inspector.get_indexes(table_back_name): - engine.execute(f"drop index {index['name']}") - # let SQLAlchemy create the schema as required - decl_base.metadata.create_all(engine) - - # Copy data back - following the correct schema - engine.execute(f"""insert into trades - (id, exchange, pair, is_open, - fee_open, fee_open_cost, fee_open_currency, - fee_close, fee_close_cost, fee_open_currency, open_rate, - open_rate_requested, close_rate, close_rate_requested, close_profit, - stake_amount, amount, amount_requested, open_date, close_date, open_order_id, - stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, - stoploss_order_id, stoploss_last_update, - max_rate, min_rate, sell_reason, sell_order_status, strategy, - timeframe, open_trade_price, close_profit_abs - ) - select id, lower(exchange), - case - when instr(pair, '_') != 0 then - substr(pair, instr(pair, '_') + 1) || '/' || - substr(pair, 1, instr(pair, '_') - 1) - else pair - end - pair, - is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost, - {fee_open_currency} fee_open_currency, {fee_close} fee_close, - {fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency, - open_rate, {open_rate_requested} open_rate_requested, close_rate, - {close_rate_requested} close_rate_requested, close_profit, - stake_amount, amount, {amount_requested}, open_date, close_date, open_order_id, - {stop_loss} stop_loss, {stop_loss_pct} stop_loss_pct, - {initial_stop_loss} initial_stop_loss, - {initial_stop_loss_pct} initial_stop_loss_pct, - {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, - {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, - {sell_order_status} sell_order_status, - {strategy} strategy, {timeframe} timeframe, - {open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs - from {table_back_name} - """) - + logger.info(f'Running database migration for trades - backup: {table_back_name}') + migrate_trades_table(decl_base, inspector, engine, table_back_name, cols) # Reread columns - the above recreated the table! inspector = inspect(engine) cols = inspector.get_columns('trades') + + if 'orders' not in previous_tables: + logger.info('Moving open orders to Orders table.') + migrate_open_orders_to_trades(engine) + else: + logger.info(f'Running database migration for orders - backup: {table_back_name}') + pass + # Empty for now - as there is only one iteration of the orders table so far. + # table_back_name = get_backup_name(tabs, 'orders_bak') diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 12bbd8adc..b0bde02fe 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -7,8 +7,8 @@ from decimal import Decimal from typing import Any, Dict, List, Optional import arrow -from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String, ForeignKey, - create_engine, desc, func) +from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, + String, create_engine, desc, func, inspect) from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import Query, relationship @@ -61,9 +61,9 @@ def init(db_url: str, clean_open_orders: bool = False) -> None: # Copy session attributes to order object too Order.session = Trade.session Order.query = Order.session.query_property() - + previous_tables = inspect(engine).get_table_names() _DECL_BASE.metadata.create_all(engine) - check_migrate(engine, decl_base=_DECL_BASE) + check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables) # Clean dry_run DB if the db is not in-memory if clean_open_orders and db_url != 'sqlite://': @@ -117,7 +117,7 @@ class Order(_DECL_BASE): filled = Column(Float, nullable=True) remaining = Column(Float, nullable=True) cost = Column(Float, nullable=True) - order_date = Column(DateTime, nullable=False, default=datetime.utcnow) + order_date = Column(DateTime, nullable=True, default=datetime.utcnow) order_filled_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index dbb62e636..c812c496f 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -8,7 +8,7 @@ from sqlalchemy import create_engine from freqtrade import constants from freqtrade.exceptions import OperationalException -from freqtrade.persistence import Trade, clean_dry_run_db, init +from freqtrade.persistence import Trade, Order, clean_dry_run_db, init from tests.conftest import log_has, create_mock_trades @@ -421,9 +421,9 @@ def test_migrate_old(mocker, default_conf, fee): PRIMARY KEY (id), CHECK (is_open IN (0, 1)) );""" - insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee, + insert_table_old = """INSERT INTO trades (exchange, pair, is_open, open_order_id, fee, open_rate, stake_amount, amount, open_date) - VALUES ('BITTREX', 'BTC_ETC', 1, {fee}, + VALUES ('BITTREX', 'BTC_ETC', 1, '123123', {fee}, 0.00258580, {stake}, {amount}, '2017-11-28 12:44:24.000000') """.format(fee=fee.return_value, @@ -481,6 +481,12 @@ def test_migrate_old(mocker, default_conf, fee): assert pytest.approx(trade.close_profit_abs) == trade.calc_profit() assert trade.sell_order_status is None + # Should've created one order + assert len(Order.query.all()) == 1 + order = Order.query.first() + assert order.order_id == '123123' + assert order.ft_order_side == 'buy' + def test_migrate_new(mocker, default_conf, fee, caplog): """ @@ -509,16 +515,19 @@ def test_migrate_new(mocker, default_conf, fee, caplog): sell_reason VARCHAR, strategy VARCHAR, ticker_interval INTEGER, + stoploss_order_id VARCHAR, PRIMARY KEY (id), CHECK (is_open IN (0, 1)) );""" insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee, open_rate, stake_amount, amount, open_date, - stop_loss, initial_stop_loss, max_rate, ticker_interval) + stop_loss, initial_stop_loss, max_rate, ticker_interval, + open_order_id, stoploss_order_id) VALUES ('binance', 'ETC/BTC', 1, {fee}, 0.00258580, {stake}, {amount}, '2019-11-28 12:44:24.000000', - 0.0, 0.0, 0.0, '5m') + 0.0, 0.0, 0.0, '5m', + 'buy_order', 'stop_order_id222') """.format(fee=fee.return_value, stake=default_conf.get("stake_amount"), amount=amount @@ -558,14 +567,23 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert trade.sell_reason is None assert trade.strategy is None assert trade.timeframe == '5m' - assert trade.stoploss_order_id is None + assert trade.stoploss_order_id == 'stop_order_id222' assert trade.stoploss_last_update is None assert log_has("trying trades_bak1", caplog) assert log_has("trying trades_bak2", caplog) - assert log_has("Running database migration - backup available as trades_bak2", caplog) + assert log_has("Running database migration for trades - backup: trades_bak2", caplog) assert trade.open_trade_price == trade._calc_open_trade_price() assert trade.close_profit_abs is None + assert log_has("Moving open orders to Orders table.", caplog) + orders = Order.query.all() + assert len(orders) == 2 + assert orders[0].order_id == 'buy_order' + assert orders[0].ft_order_side == 'buy' + + assert orders[1].order_id == 'stop_order_id222' + assert orders[1].ft_order_side == 'stoploss' + def test_migrate_mid_state(mocker, default_conf, fee, caplog): """ @@ -626,7 +644,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): assert trade.initial_stop_loss == 0.0 assert trade.open_trade_price == trade._calc_open_trade_price() assert log_has("trying trades_bak0", caplog) - assert log_has("Running database migration - backup available as trades_bak0", caplog) + assert log_has("Running database migration for trades - backup: trades_bak0", caplog) def test_adjust_stop_loss(fee): From 0af9e913d4a3bd0c66b387e9d79fdd949d9da1d2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Aug 2020 15:39:29 +0200 Subject: [PATCH 027/224] Timestamps are in ms --- freqtrade/exchange/exchange.py | 2 +- freqtrade/freqtradebot.py | 5 ++++- freqtrade/persistence/models.py | 12 ++++++++---- tests/exchange/test_exchange.py | 2 +- tests/test_freqtradebot.py | 1 + 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 533377746..bcb511010 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -486,7 +486,7 @@ class Exchange: 'side': side, 'remaining': _amount, 'datetime': arrow.utcnow().isoformat(), - 'timestamp': arrow.utcnow().timestamp, + 'timestamp': int(arrow.utcnow().timestamp * 1000), 'status': "closed" if ordertype == "market" else "open", 'fee': None, 'info': {} diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e2d504916..18d39e471 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -817,6 +817,9 @@ class FreqtradeBot: except InvalidOrderException as exception: logger.warning('Unable to fetch stoploss order: %s', exception) + if stoploss_order: + trade.update_order(stoploss_order) + # We check if stoploss order is fulfilled if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value @@ -1260,7 +1263,7 @@ class FreqtradeBot: except InvalidOrderException as exception: logger.warning('Unable to fetch order %s: %s', order_id, exception) return False - Order.update_order(order) + trade.update_order(order) # Try update amount (binance-fix) try: new_amount = self.get_real_amount(trade, order, order_amount) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index b0bde02fe..5f85cdb4c 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -107,7 +107,7 @@ class Order(_DECL_BASE): ft_order_side = Column(String, nullable=False) - order_id = Column(String, nullable=False, unique=True, index=True) + order_id = Column(String, nullable=False, index=True) status = Column(String, nullable=True) symbol = Column(String, nullable=True) order_type = Column(String, nullable=True) @@ -144,13 +144,14 @@ class Order(_DECL_BASE): self.remaining = order.get('remaining', self.remaining) self.cost = order.get('cost', self.cost) if 'timestamp' in order and order['timestamp'] is not None: - self.order_date = datetime.fromtimestamp(order['timestamp']) + self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000) @staticmethod - def update_order(order: Dict[str, Any]): + def update_orders(orders: List['Order'], order: Dict[str, Any]): """ """ - oobj = Order.query.filter(Order.order_id == order['id']).first() + filtered_orders = [o for o in orders if o.order_id == order['id']] + oobj = filtered_orders[0] if filtered_orders else None oobj.update_from_ccxt_object(order) oobj.order_update_date = datetime.now() @@ -417,6 +418,9 @@ class Trade(_DECL_BASE): else: return False + def update_order(self, order: Dict) -> None: + Order.update_orders(self.orders, order) + def _calc_open_trade_price(self) -> float: """ Calculate the open_rate including open_fee. diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 350c2d3cb..d0e303f5f 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -807,7 +807,7 @@ def test_dry_run_order(default_conf, mocker, side, exchange_name): assert f'dry_run_{side}_' in order["id"] assert order["side"] == side assert order["type"] == "limit" - assert order["pair"] == "ETH/BTC" + assert order["symbol"] == "ETH/BTC" @pytest.mark.parametrize("side", [ diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 1621be6e5..7257c73b9 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1194,6 +1194,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, assert trade stoploss_order_hit = MagicMock(return_value={ + 'id': 100, 'status': 'closed', 'type': 'stop_loss_limit', 'price': 3, From ebd755e36a842f63a62044ebbd68d6779720bdd0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Aug 2020 15:54:36 +0200 Subject: [PATCH 028/224] Improve order handling --- freqtrade/persistence/models.py | 9 ++++++--- tests/test_freqtradebot.py | 11 +++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 5f85cdb4c..d930fbeb7 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -151,9 +151,12 @@ class Order(_DECL_BASE): """ """ filtered_orders = [o for o in orders if o.order_id == order['id']] - oobj = filtered_orders[0] if filtered_orders else None - oobj.update_from_ccxt_object(order) - oobj.order_update_date = datetime.now() + if filtered_orders: + oobj = filtered_orders[0] + oobj.update_from_ccxt_object(order) + oobj.order_update_date = datetime.now() + else: + logger.warning(f"Did not find order for {order['id']}.") @staticmethod def parse_from_ccxt_object(order: Dict[str, Any], side: str) -> 'Order': diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 7257c73b9..1f7984a43 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments +from freqtrade.persistence.models import Order import logging import time from copy import deepcopy @@ -1252,7 +1253,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - fetch_stoploss_order=MagicMock(return_value={'status': 'canceled'}), + fetch_stoploss_order=MagicMock(return_value={'status': 'canceled', 'id': 100}), stoploss=MagicMock(side_effect=ExchangeError()), ) freqtrade = FreqtradeBot(default_conf) @@ -1794,7 +1795,8 @@ def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None assert log_has(f'Unable to fetch order {trade.open_order_id}: ', caplog) -def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_order, mocker): +def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_order_open, + limit_sell_order, mocker): mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) # fetch_order should not be called!! mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) @@ -1817,11 +1819,16 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde open_order_id="123456", is_open=True, ) + order = Order.parse_from_ccxt_object(limit_sell_order_open, 'sell') + trade.orders.append(order) + assert order.status == 'open' freqtrade.update_trade_state(trade, limit_sell_order) assert trade.amount == limit_sell_order['amount'] # Wallet needs to be updated after closing a limit-sell order to reenable buying assert wallet_mock.call_count == 1 assert not trade.is_open + # Order is updated by update_trade_state + assert order.status == 'closed' def test_handle_trade(default_conf, limit_buy_order, limit_sell_order_open, limit_sell_order, From 4434a54d598b1a1ec3ea1fa2122d90834dfa91cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Aug 2020 16:14:28 +0200 Subject: [PATCH 029/224] Add unique key to order-Model --- freqtrade/freqtradebot.py | 6 +++--- freqtrade/persistence/models.py | 9 +++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 18d39e471..038bf1f4f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -527,7 +527,7 @@ class FreqtradeBot: order = self.exchange.buy(pair=pair, ordertype=order_type, amount=amount, rate=buy_limit_requested, time_in_force=time_in_force) - order_obj = Order.parse_from_ccxt_object(order, 'buy') + order_obj = Order.parse_from_ccxt_object(order, pair, 'buy') order_id = order['id'] order_status = order.get('status', None) @@ -784,7 +784,7 @@ class FreqtradeBot: stop_price=stop_price, order_types=self.strategy.order_types) - order_obj = Order.parse_from_ccxt_object(stoploss_order, 'stoploss') + order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss') trade.orders.append(order_obj) trade.stoploss_order_id = str(stoploss_order['id']) return True @@ -1137,7 +1137,7 @@ class FreqtradeBot: time_in_force=time_in_force ) - order_obj = Order.parse_from_ccxt_object(order, 'sell') + order_obj = Order.parse_from_ccxt_object(order, trade.pair, 'sell') trade.orders.append(order_obj) trade.open_order_id = order['id'] diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index d930fbeb7..14e2b0da8 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -15,6 +15,7 @@ from sqlalchemy.orm import Query, relationship from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.session import sessionmaker from sqlalchemy.pool import StaticPool +from sqlalchemy.sql.schema import UniqueConstraint from freqtrade.exceptions import OperationalException from freqtrade.misc import safe_value_fallback @@ -101,11 +102,15 @@ class Order(_DECL_BASE): Mirrors CCXT Order structure """ __tablename__ = 'orders' + # Uniqueness should be ensured over pair, order_id + # its likely that order_id is unique per Pair on some exchanges. + __table_args__ = (UniqueConstraint('ft_pair', 'order_id'),) id = Column(Integer, primary_key=True) trade_id = Column(Integer, ForeignKey('trades.id'), index=True) ft_order_side = Column(String, nullable=False) + ft_pair = Column(String, nullable=False) order_id = Column(String, nullable=False, index=True) status = Column(String, nullable=True) @@ -159,11 +164,11 @@ class Order(_DECL_BASE): logger.warning(f"Did not find order for {order['id']}.") @staticmethod - def parse_from_ccxt_object(order: Dict[str, Any], side: str) -> 'Order': + def parse_from_ccxt_object(order: Dict[str, Any], pair: str, side: str) -> 'Order': """ Parse an order from a ccxt object and return a new order Object. """ - o = Order(order_id=str(order['id']), ft_order_side=side) + o = Order(order_id=str(order['id']), ft_order_side=side, ft_pair=pair) o.update_from_ccxt_object(order) return o From 2ca6547bafb88026b45e3075fef33858b879f1a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Aug 2020 16:14:41 +0200 Subject: [PATCH 030/224] Update tests to have unique ordernumbers --- tests/test_freqtradebot.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 1f7984a43..17d1a7ea3 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -192,6 +192,7 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b for i in range(0, max_open): if expected[i] is not None: + limit_buy_order_open['id'] = str(i) result = freqtrade.get_trade_stake_amount('ETH/BTC') assert pytest.approx(result) == expected[i] freqtrade.execute_buy('ETH/BTC', result) @@ -740,6 +741,8 @@ def test_create_trades_preopen(default_conf, ticker, fee, mocker, limit_buy_orde freqtrade.execute_buy('NEO/BTC', default_conf['stake_amount']) assert len(Trade.get_open_trades()) == 2 + # Change order_id for new orders + limit_buy_order_open['id'] = '123444' # Create 2 new trades using create_trades assert freqtrade.create_trade('ETH/BTC') @@ -997,6 +1000,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order assert freqtrade.strategy.confirm_trade_entry.call_count == 1 buy_rate_mock.reset_mock() + limit_buy_order_open['id'] = '22' freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) assert freqtrade.execute_buy(pair, stake_amount) assert buy_rate_mock.call_count == 1 @@ -1012,9 +1016,10 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order trade = Trade.query.first() assert trade assert trade.is_open is True - assert trade.open_order_id == limit_buy_order['id'] + assert trade.open_order_id == '22' # Test calling with price + limit_buy_order_open['id'] = '33' fix_price = 0.06 assert freqtrade.execute_buy(pair, stake_amount, fix_price) # Make sure get_buy_rate wasn't called again @@ -1030,6 +1035,8 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order limit_buy_order['status'] = 'closed' limit_buy_order['price'] = 10 limit_buy_order['cost'] = 100 + limit_buy_order['id'] = '444' + mocker.patch('freqtrade.exchange.Exchange.buy', MagicMock(return_value=limit_buy_order)) assert freqtrade.execute_buy(pair, stake_amount) trade = Trade.query.all()[2] @@ -1045,6 +1052,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order limit_buy_order['remaining'] = 10.00 limit_buy_order['price'] = 0.5 limit_buy_order['cost'] = 40.495905365 + limit_buy_order['id'] = '555' mocker.patch('freqtrade.exchange.Exchange.buy', MagicMock(return_value=limit_buy_order)) assert freqtrade.execute_buy(pair, stake_amount) trade = Trade.query.all()[3] @@ -1060,6 +1068,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order limit_buy_order['remaining'] = 90.99181073 limit_buy_order['price'] = 0.5 limit_buy_order['cost'] = 0.0 + limit_buy_order['id'] = '66' mocker.patch('freqtrade.exchange.Exchange.buy', MagicMock(return_value=limit_buy_order)) assert not freqtrade.execute_buy(pair, stake_amount) @@ -1087,9 +1096,11 @@ def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) - freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=ValueError) assert freqtrade.execute_buy(pair, stake_amount) + limit_buy_order['id'] = '222' freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=Exception) assert freqtrade.execute_buy(pair, stake_amount) + limit_buy_order['id'] = '2223' freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) assert freqtrade.execute_buy(pair, stake_amount) @@ -1315,6 +1326,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, assert rpc_mock.call_args_list[1][0][0]['order_type'] == 'market' +@pytest.mark.usefixtures("init_persistence") def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, limit_buy_order, limit_sell_order) -> None: # When trailing stoploss is set @@ -1819,7 +1831,7 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde open_order_id="123456", is_open=True, ) - order = Order.parse_from_ccxt_object(limit_sell_order_open, 'sell') + order = Order.parse_from_ccxt_object(limit_sell_order_open, 'LTC/ETH', 'sell') trade.orders.append(order) assert order.status == 'open' freqtrade.update_trade_state(trade, limit_sell_order) From 1a305ea8b0f20376faf0f7f650fa22d4ca505be4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Aug 2020 16:18:03 +0200 Subject: [PATCH 031/224] Fix migrations to use unique key --- freqtrade/freqtradebot.py | 2 ++ freqtrade/persistence/migrations.py | 6 +++--- tests/test_freqtradebot.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 038bf1f4f..dac3b7ce1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1263,7 +1263,9 @@ class FreqtradeBot: except InvalidOrderException as exception: logger.warning('Unable to fetch order %s: %s', order_id, exception) return False + trade.update_order(order) + # Try update amount (binance-fix) try: new_amount = self.get_real_amount(trade, order, order_amount) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 55825436d..15e5f56bf 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -108,14 +108,14 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col def migrate_open_orders_to_trades(engine): engine.execute(""" - insert into orders (trade_id, order_id, ft_order_side) - select id, open_order_id, + insert into orders (trade_id, ft_pair, order_id, ft_order_side) + select id trade_id, pair ft_pair, open_order_id, case when close_rate_requested is null then 'buy' else 'sell' end ft_order_side from trades where open_order_id is not null union all - select id, stoploss_order_id, 'stoploss' + select id trade_id, pair ft_pair, stoploss_order_id order_id, 'stoploss' ft_order_side from trades where stoploss_order_id is not null """) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 17d1a7ea3..a907ce5c2 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1396,7 +1396,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, })) cancel_order_mock = MagicMock() - stoploss_order_mock = MagicMock() + stoploss_order_mock = MagicMock(return_value={'id': 13434334}) mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock) mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock) From da2a515d0be4597e5282dd58a3eedf28db281bf6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Aug 2020 16:55:43 +0200 Subject: [PATCH 032/224] Add delete cascade to alchemy model --- freqtrade/persistence/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 14e2b0da8..4efdacef8 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -183,7 +183,7 @@ class Trade(_DECL_BASE): id = Column(Integer, primary_key=True) - orders = relationship("Order", order_by="Order.id") + orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan") exchange = Column(String, nullable=False) pair = Column(String, nullable=False, index=True) From c4d7aff5c33b8e069ff9d6f910c262f3c1a26ad4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Aug 2020 17:17:52 +0200 Subject: [PATCH 033/224] Order should have a "is_open" flag --- freqtrade/persistence/migrations.py | 9 +++++---- freqtrade/persistence/models.py | 12 ++++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 15e5f56bf..96b3a0db6 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -108,14 +108,15 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col def migrate_open_orders_to_trades(engine): engine.execute(""" - insert into orders (trade_id, ft_pair, order_id, ft_order_side) - select id trade_id, pair ft_pair, open_order_id, + insert into orders (ft_trade_id, ft_pair, order_id, ft_order_side, ft_is_open) + select id ft_trade_id, pair ft_pair, open_order_id, case when close_rate_requested is null then 'buy' - else 'sell' end ft_order_side + else 'sell' end ft_order_side, true ft_is_open from trades where open_order_id is not null union all - select id trade_id, pair ft_pair, stoploss_order_id order_id, 'stoploss' ft_order_side + select id ft_trade_id, pair ft_pair, stoploss_order_id order_id, + 'stoploss' ft_order_side, true ft_is_open from trades where stoploss_order_id is not null """) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 4efdacef8..eb4d42ef5 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -104,13 +104,14 @@ class Order(_DECL_BASE): __tablename__ = 'orders' # Uniqueness should be ensured over pair, order_id # its likely that order_id is unique per Pair on some exchanges. - __table_args__ = (UniqueConstraint('ft_pair', 'order_id'),) + __table_args__ = (UniqueConstraint('ft_pair', 'order_id', name="_order_pair_order_id"),) id = Column(Integer, primary_key=True) - trade_id = Column(Integer, ForeignKey('trades.id'), index=True) + ft_trade_id = Column(Integer, ForeignKey('trades.id'), index=True) ft_order_side = Column(String, nullable=False) ft_pair = Column(String, nullable=False) + ft_is_open = Column(Boolean, nullable=False, default=True, index=True) order_id = Column(String, nullable=False, index=True) status = Column(String, nullable=True) @@ -128,7 +129,7 @@ class Order(_DECL_BASE): def __repr__(self): - return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.trade_id}, ' + return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' f'side={self.side}, status={self.status})') def update_from_ccxt_object(self, order): @@ -151,6 +152,10 @@ class Order(_DECL_BASE): if 'timestamp' in order and order['timestamp'] is not None: self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000) + if self.status in ('closed', 'canceled', 'cancelled'): + self.ft_is_open = False + self.order_update_date = datetime.now() + @staticmethod def update_orders(orders: List['Order'], order: Dict[str, Any]): """ @@ -159,7 +164,6 @@ class Order(_DECL_BASE): if filtered_orders: oobj = filtered_orders[0] oobj.update_from_ccxt_object(order) - oobj.order_update_date = datetime.now() else: logger.warning(f"Did not find order for {order['id']}.") From 95efc0d688556d80b869ae0c4533d9870414b58b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Aug 2020 17:18:56 +0200 Subject: [PATCH 034/224] Add open_order_updater --- freqtrade/freqtradebot.py | 16 ++++++++++++++++ freqtrade/persistence/models.py | 7 +++++++ 2 files changed, 23 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index dac3b7ce1..db0c852fe 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -227,6 +227,22 @@ class FreqtradeBot: open_trades = len(Trade.get_open_trades()) return max(0, self.config['max_open_trades'] - open_trades) + def update_open_orders(self): + """ + Updates open orders based on order list kept in the database + """ + orders = Order.get_open_orders() + logger.info(f"Updating {len(orders)} open orders.") + for order in orders: + try: + if order.ft_order_side == 'stoposs': + fo = self.exchange.fetch_stoploss_order(order.order_id, order.ft_pair) + else: + fo = self.exchange.fetch_order(order.order_id, order.ft_pair) + order.update_from_ccxt_object(fo) + except ExchangeError: + logger.warning(f"Error updating {order.order_id}") + # # BUY / enter positions / open trades logic and methods # diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index eb4d42ef5..b6a96b8f3 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -159,6 +159,7 @@ class Order(_DECL_BASE): @staticmethod def update_orders(orders: List['Order'], order: Dict[str, Any]): """ + Get all non-closed orders - useful when trying to batch-update orders """ filtered_orders = [o for o in orders if o.order_id == order['id']] if filtered_orders: @@ -177,6 +178,12 @@ class Order(_DECL_BASE): o.update_from_ccxt_object(order) return o + @staticmethod + def get_open_orders(): + """ + """ + return Order.query.filter(Order.ft_is_open.is_(True)).all() + class Trade(_DECL_BASE): """ From 8458a380b8d8ffc0eb5cda78d317c9de17d247ed Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Aug 2020 19:37:41 +0200 Subject: [PATCH 035/224] Improve order catchup --- freqtrade/freqtradebot.py | 8 ++++++-- freqtrade/persistence/migrations.py | 25 ++++++++++++------------- freqtrade/persistence/models.py | 4 +++- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index db0c852fe..490169790 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -134,6 +134,8 @@ class FreqtradeBot: # Adjust stoploss if it was changed Trade.stoploss_reinitialization(self.strategy.stoploss) + self.update_open_orders() + def process(self) -> None: """ Queries the persistence layer for open trades and handles them, @@ -235,11 +237,13 @@ class FreqtradeBot: logger.info(f"Updating {len(orders)} open orders.") for order in orders: try: - if order.ft_order_side == 'stoposs': + if order.ft_order_side == 'stoploss': fo = self.exchange.fetch_stoploss_order(order.order_id, order.ft_pair) else: fo = self.exchange.fetch_order(order.order_id, order.ft_pair) - order.update_from_ccxt_object(fo) + + self.update_trade_state(order.trade, fo, sl_order=order.ft_order_side == 'stoploss') + except ExchangeError: logger.warning(f"Error updating {order.order_id}") diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 96b3a0db6..b6fa8f9ae 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -108,18 +108,18 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col def migrate_open_orders_to_trades(engine): engine.execute(""" - insert into orders (ft_trade_id, ft_pair, order_id, ft_order_side, ft_is_open) - select id ft_trade_id, pair ft_pair, open_order_id, - case when close_rate_requested is null then 'buy' - else 'sell' end ft_order_side, true ft_is_open - from trades - where open_order_id is not null - union all - select id ft_trade_id, pair ft_pair, stoploss_order_id order_id, - 'stoploss' ft_order_side, true ft_is_open - from trades - where stoploss_order_id is not null - """) + insert into orders (ft_trade_id, ft_pair, order_id, ft_order_side, ft_is_open) + select id ft_trade_id, pair ft_pair, open_order_id, + case when close_rate_requested is null then 'buy' + else 'sell' end ft_order_side, true ft_is_open + from trades + where open_order_id is not null + union all + select id ft_trade_id, pair ft_pair, stoploss_order_id order_id, + 'stoploss' ft_order_side, true ft_is_open + from trades + where stoploss_order_id is not null + """) def check_migrate(engine, decl_base, previous_tables) -> None: @@ -144,7 +144,6 @@ def check_migrate(engine, decl_base, previous_tables) -> None: logger.info('Moving open orders to Orders table.') migrate_open_orders_to_trades(engine) else: - logger.info(f'Running database migration for orders - backup: {table_back_name}') pass # Empty for now - as there is only one iteration of the orders table so far. # table_back_name = get_backup_name(tabs, 'orders_bak') diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index b6a96b8f3..3f918670a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -109,6 +109,8 @@ class Order(_DECL_BASE): id = Column(Integer, primary_key=True) ft_trade_id = Column(Integer, ForeignKey('trades.id'), index=True) + trade = relationship("Trade", back_populates="orders") + ft_order_side = Column(String, nullable=False) ft_pair = Column(String, nullable=False) ft_is_open = Column(Boolean, nullable=False, default=True, index=True) @@ -179,7 +181,7 @@ class Order(_DECL_BASE): return o @staticmethod - def get_open_orders(): + def get_open_orders() -> List['Order']: """ """ return Order.query.filter(Order.ft_is_open.is_(True)).all() From a6fc922f2830481c1d4b18b40a79acf9204c2b85 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 14 Aug 2020 09:56:48 +0200 Subject: [PATCH 036/224] Introduce insufficientFunds Exception --- docs/developer.md | 2 ++ freqtrade/exceptions.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/docs/developer.md b/docs/developer.md index f09ae2c76..b79930061 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -110,6 +110,8 @@ Below is an outline of exception inheritance hierarchy: | +---+ InvalidOrderException | | | +---+ RetryableOrderError +| | +| +---+ InsufficientFundsError | +---+ StrategyError ``` diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py index e2bc969a9..caf970606 100644 --- a/freqtrade/exceptions.py +++ b/freqtrade/exceptions.py @@ -51,6 +51,13 @@ class RetryableOrderError(InvalidOrderException): """ +class InsufficientFundsError(InvalidOrderException): + """ + This error is used when there are not enough funds available on the exchange + to create an order. + """ + + class TemporaryError(ExchangeError): """ Temporary network or exchange related error. From 22af82631a912b0e07c0b35138f2395d9db71d09 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 14 Aug 2020 09:57:13 +0200 Subject: [PATCH 037/224] Introduce InsufficientFundsError exception --- freqtrade/exchange/binance.py | 4 ++-- freqtrade/exchange/exchange.py | 3 ++- freqtrade/exchange/ftx.py | 3 ++- freqtrade/exchange/kraken.py | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index f2fe1d6ad..d7da34482 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -4,7 +4,7 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DDosProtection, ExchangeError, +from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -80,7 +80,7 @@ class Binance(Exchange): 'stop price: %s. limit: %s', pair, stop_price, rate) return order except ccxt.InsufficientFunds as e: - raise ExchangeError( + raise InsufficientFundsError( f'Insufficient funds to create {ordertype} sell order on market {pair}. ' f'Tried to sell amount {amount} at rate {rate}. ' f'Message: {e}') from e diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index bcb511010..578d753a4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -20,6 +20,7 @@ from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list from freqtrade.exceptions import (DDosProtection, ExchangeError, + InsufficientFundsError, InvalidOrderException, OperationalException, RetryableOrderError, TemporaryError) from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async @@ -525,7 +526,7 @@ class Exchange: amount, rate_for_order, params) except ccxt.InsufficientFunds as e: - raise ExchangeError( + raise InsufficientFundsError( f'Insufficient funds to create {ordertype} {side} order on market {pair}. ' f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 01e8267ad..9c506c88e 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -5,6 +5,7 @@ from typing import Dict import ccxt from freqtrade.exceptions import (DDosProtection, ExchangeError, + InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -61,7 +62,7 @@ class Ftx(Exchange): 'stop price: %s.', pair, stop_price) return order except ccxt.InsufficientFunds as e: - raise ExchangeError( + raise InsufficientFundsError( f'Insufficient funds to create {ordertype} sell order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 7b9d0f09b..17e181527 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -4,7 +4,7 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DDosProtection, ExchangeError, +from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -88,7 +88,7 @@ class Kraken(Exchange): 'stop price: %s.', pair, stop_price) return order except ccxt.InsufficientFunds as e: - raise ExchangeError( + raise InsufficientFundsError( f'Insufficient funds to create {ordertype} sell order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e From 552aaf79459fbc66ad02e4c517bfd5d69ff2d090 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 14 Aug 2020 10:59:55 +0200 Subject: [PATCH 038/224] add refind order logic --- freqtrade/freqtradebot.py | 55 ++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 490169790..91adbc598 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -17,7 +17,7 @@ from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge -from freqtrade.exceptions import (DependencyException, ExchangeError, +from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.misc import safe_value_fallback, safe_value_fallback2 @@ -247,6 +247,36 @@ class FreqtradeBot: except ExchangeError: logger.warning(f"Error updating {order.order_id}") + def refind_lost_order(self, trade): + """ + Try refinding a lost trade. + Only used when InsufficientFunds appears on sell orders (stoploss or sell). + Tries to walk the stored orders and sell them off eventually. + """ + logger.info(f"Trying to refind lost order for {trade}") + for order in trade.orders: + logger.info(f"Trying to refind {order}") + fo = None + try: + if order.ft_order_side == 'stoploss': + fo = self.exchange.fetch_stoploss_order(order.order_id, order.ft_pair) + if fo and fo['status'] == 'open': + # Assume this as the open stoploss order + trade.stoploss_order_id = order.order_id + elif order.ft_order_side == 'sell': + fo = self.exchange.fetch_order(order.order_id, order.ft_pair) + if fo and fo['status'] == 'open': + # Assume this as the open order + trade.open_order_id = order.order_id + else: + # No action for buy orders ... + continue + if fo: + self.update_trade_state(trade, fo, sl_order=order.ft_order_side == 'stoploss') + + except ExchangeError: + logger.warning(f"Error updating {order.order_id}") + # # BUY / enter positions / open trades logic and methods # @@ -808,6 +838,11 @@ class FreqtradeBot: trade.orders.append(order_obj) trade.stoploss_order_id = str(stoploss_order['id']) return True + except InsufficientFundsError as e: + logger.warning(f"Unable to place stoploss order {e}.") + # Try refinding stoploss order + self.refind_lost_order(trade) + except InvalidOrderException as e: trade.stoploss_order_id = None logger.error(f'Unable to place a stoploss order on exchange. {e}') @@ -1150,12 +1185,18 @@ class FreqtradeBot: logger.info(f"User requested abortion of selling {trade.pair}") return False - # Execute sell and update trade record - order = self.exchange.sell(pair=trade.pair, - ordertype=order_type, - amount=amount, rate=limit, - time_in_force=time_in_force - ) + try: + # Execute sell and update trade record + order = self.exchange.sell(pair=trade.pair, + ordertype=order_type, + amount=amount, rate=limit, + time_in_force=time_in_force + ) + except InsufficientFundsError as e: + logger.warning(f"Unable to place order {e}.") + # Try refinding "lost" orders + self.refind_lost_order(trade) + return False order_obj = Order.parse_from_ccxt_object(order, trade.pair, 'sell') trade.orders.append(order_obj) From b25267ad3d78d1bf815d69ac0bc68a468e31ece4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 14 Aug 2020 11:13:55 +0200 Subject: [PATCH 039/224] Build docker image for db_keep_orders branch --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 239576c61..54f0e4444 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: - master - develop - github_actions_tests + - db_keep_orders tags: release: types: [published] From cfa352ecf2c275ce46defeac53460ef26d5b5c23 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 14 Aug 2020 11:25:20 +0200 Subject: [PATCH 040/224] Disable refind_lost_order for now --- freqtrade/freqtradebot.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 91adbc598..0dac4d888 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -841,7 +841,8 @@ class FreqtradeBot: except InsufficientFundsError as e: logger.warning(f"Unable to place stoploss order {e}.") # Try refinding stoploss order - self.refind_lost_order(trade) + # TODO: Currently disabled to allow testing without this first + # self.refind_lost_order(trade) except InvalidOrderException as e: trade.stoploss_order_id = None @@ -933,7 +934,8 @@ class FreqtradeBot: logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s}) ' 'in order to add another one ...', order['id']) try: - self.exchange.cancel_stoploss_order(order['id'], trade.pair) + co = self.exchange.cancel_stoploss_order(order['id'], trade.pair) + trade.update_order(co) except InvalidOrderException: logger.exception(f"Could not cancel stoploss order {order['id']} " f"for pair {trade.pair}") @@ -1195,7 +1197,8 @@ class FreqtradeBot: except InsufficientFundsError as e: logger.warning(f"Unable to place order {e}.") # Try refinding "lost" orders - self.refind_lost_order(trade) + # TODO: Currently disabled to allow testing without this first + # self.refind_lost_order(trade) return False order_obj = Order.parse_from_ccxt_object(order, trade.pair, 'sell') From d8fdd32b548222e3852adc401e14e1019258ee1c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 14 Aug 2020 11:25:40 +0200 Subject: [PATCH 041/224] FIx migrations --- freqtrade/persistence/migrations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index b6fa8f9ae..5089953b2 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -111,12 +111,12 @@ def migrate_open_orders_to_trades(engine): insert into orders (ft_trade_id, ft_pair, order_id, ft_order_side, ft_is_open) select id ft_trade_id, pair ft_pair, open_order_id, case when close_rate_requested is null then 'buy' - else 'sell' end ft_order_side, true ft_is_open + else 'sell' end ft_order_side, 1 ft_is_open from trades where open_order_id is not null union all select id ft_trade_id, pair ft_pair, stoploss_order_id order_id, - 'stoploss' ft_order_side, true ft_is_open + 'stoploss' ft_order_side, 1 ft_is_open from trades where stoploss_order_id is not null """) From 06125df10c5b9f041ad4d113e2fa673385895bf0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 14 Aug 2020 11:31:02 +0200 Subject: [PATCH 042/224] Remove unused import --- freqtrade/exchange/ftx.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 9c506c88e..27051a945 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -4,8 +4,7 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DDosProtection, ExchangeError, - InsufficientFundsError, +from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange From 838985f6a06d7f1851282c30ef202eb8eeb214c3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Aug 2020 07:13:13 +0200 Subject: [PATCH 043/224] Don't reset open-order-id just yet it's needed to get the fees --- freqtrade/freqtradebot.py | 1 - tests/test_freqtradebot.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0dac4d888..fd5847a94 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -606,7 +606,6 @@ class FreqtradeBot: stake_amount = order['cost'] amount = safe_value_fallback(order, 'filled', 'amount') buy_limit_filled_price = safe_value_fallback(order, 'average', 'price') - order_id = None # in case of FOK the order may be filled immediately and fully elif order_status == 'closed': diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a907ce5c2..9a3dcbd78 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1057,7 +1057,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order assert freqtrade.execute_buy(pair, stake_amount) trade = Trade.query.all()[3] assert trade - assert trade.open_order_id is None + assert trade.open_order_id == '555' assert trade.open_rate == 0.5 assert trade.stake_amount == 40.495905365 From 0b6014fae3aea892a85bc5e85a6c52f41796a652 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Aug 2020 07:17:52 +0200 Subject: [PATCH 044/224] update_trade_state should take the order id directly - not from the trade object --- freqtrade/freqtradebot.py | 29 +++++++++++++---------------- tests/test_freqtradebot.py | 18 +++++++++--------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fd5847a94..929066c18 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -242,7 +242,7 @@ class FreqtradeBot: else: fo = self.exchange.fetch_order(order.order_id, order.ft_pair) - self.update_trade_state(order.trade, fo, sl_order=order.ft_order_side == 'stoploss') + self.update_trade_state(order.trade, order.order_id, fo) except ExchangeError: logger.warning(f"Error updating {order.order_id}") @@ -272,7 +272,7 @@ class FreqtradeBot: # No action for buy orders ... continue if fo: - self.update_trade_state(trade, fo, sl_order=order.ft_order_side == 'stoploss') + self.update_trade_state(trade, order.order_id, fo) except ExchangeError: logger.warning(f"Error updating {order.order_id}") @@ -634,7 +634,7 @@ class FreqtradeBot: # Update fees if order is closed if order_status == 'closed': - self.update_trade_state(trade, order) + self.update_trade_state(trade, order_id, order) Trade.session.add(trade) Trade.session.flush() @@ -878,7 +878,7 @@ class FreqtradeBot: # We check if stoploss order is fulfilled if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value - self.update_trade_state(trade, stoploss_order, sl_order=True) + self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order) # Lock pair for one candle to prevent immediate rebuys self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe'])) @@ -989,7 +989,7 @@ class FreqtradeBot: logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) continue - fully_cancelled = self.update_trade_state(trade, order) + fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order) if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and ( fully_cancelled @@ -1070,7 +1070,7 @@ class FreqtradeBot: # we need to fall back to the values from order if corder does not contain these keys. trade.amount = filled_amount trade.stake_amount = trade.amount * trade.open_rate - self.update_trade_state(trade, corder, trade.amount) + self.update_trade_state(trade, trade.open_order_id, corder, trade.amount) trade.open_order_id = None logger.info('Partial buy order timeout for %s.', trade) @@ -1208,7 +1208,7 @@ class FreqtradeBot: trade.sell_reason = sell_reason.value # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') == 'closed': - self.update_trade_state(trade, order) + self.update_trade_state(trade, trade.open_order_id, order) Trade.session.flush() # Lock pair for one candle to prevent immediate rebuys @@ -1305,20 +1305,17 @@ class FreqtradeBot: # Common update trade state methods # - def update_trade_state(self, trade: Trade, action_order: dict = None, - order_amount: float = None, sl_order: bool = False) -> bool: + def update_trade_state(self, trade: Trade, order_id: str, action_order: dict = None, + order_amount: float = None) -> bool: """ Checks trades with open orders and updates the amount if necessary Handles closing both buy and sell orders. :return: True if order has been cancelled without being filled partially, False otherwise """ - # Get order details for actual price per unit - if trade.open_order_id: - order_id = trade.open_order_id - elif trade.stoploss_order_id and sl_order: - order_id = trade.stoploss_order_id - else: - return False + if not order_id: + logger.warning(f'Orderid for trade {trade} is empty.') + False + # Update trade with order values logger.info('Found open order for %s', trade) try: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 9a3dcbd78..93b71f176 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1699,7 +1699,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No amount=11, ) # Add datetime explicitly since sqlalchemy defaults apply only once written to database - freqtrade.update_trade_state(trade) + freqtrade.update_trade_state(trade, '123') # Test amount not modified by fee-logic assert not log_has_re(r'Applying fee to .*', caplog) assert trade.open_order_id is None @@ -1709,14 +1709,14 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81) assert trade.amount != 90.81 # test amount modified by fee-logic - freqtrade.update_trade_state(trade) + freqtrade.update_trade_state(trade, '123') assert trade.amount == 90.81 assert trade.open_order_id is None trade.is_open = True trade.open_order_id = None # Assert we call handle_trade() if trade is feasible for execution - freqtrade.update_trade_state(trade) + freqtrade.update_trade_state(trade, '123') assert log_has_re('Found open order for.*', caplog) @@ -1741,7 +1741,7 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ open_order_id="123456", is_open=True, ) - freqtrade.update_trade_state(trade, limit_buy_order) + freqtrade.update_trade_state(trade, '123456', limit_buy_order) assert trade.amount != amount assert trade.amount == limit_buy_order['amount'] @@ -1763,11 +1763,11 @@ def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_ open_rate=0.245441, fee_open=fee.return_value, fee_close=fee.return_value, - open_order_id="123456", + open_order_id='123456', is_open=True, open_date=arrow.utcnow().datetime, ) - freqtrade.update_trade_state(trade, limit_buy_order) + freqtrade.update_trade_state(trade, '123456', limit_buy_order) assert trade.amount != amount assert trade.amount == limit_buy_order['amount'] assert log_has_re(r'Applying fee on amount for .*', caplog) @@ -1787,7 +1787,7 @@ def test_update_trade_state_exception(mocker, default_conf, 'freqtrade.freqtradebot.FreqtradeBot.get_real_amount', side_effect=DependencyException() ) - freqtrade.update_trade_state(trade) + freqtrade.update_trade_state(trade, trade.open_order_id) assert log_has('Could not update trade amount: ', caplog) @@ -1802,7 +1802,7 @@ def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None # Test raise of OperationalException exception grm_mock = mocker.patch("freqtrade.freqtradebot.FreqtradeBot.get_real_amount", MagicMock()) - freqtrade.update_trade_state(trade) + freqtrade.update_trade_state(trade, trade.open_order_id) assert grm_mock.call_count == 0 assert log_has(f'Unable to fetch order {trade.open_order_id}: ', caplog) @@ -1834,7 +1834,7 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde order = Order.parse_from_ccxt_object(limit_sell_order_open, 'LTC/ETH', 'sell') trade.orders.append(order) assert order.status == 'open' - freqtrade.update_trade_state(trade, limit_sell_order) + freqtrade.update_trade_state(trade, trade.open_order_id, limit_sell_order) assert trade.amount == limit_sell_order['amount'] # Wallet needs to be updated after closing a limit-sell order to reenable buying assert wallet_mock.call_count == 1 From 3be14933d4cc048096177b3274d78dde47f9ed87 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Aug 2020 07:24:49 +0200 Subject: [PATCH 045/224] Add comment explaining update_open_orders --- freqtrade/freqtradebot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 929066c18..3305bb584 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -134,6 +134,8 @@ class FreqtradeBot: # Adjust stoploss if it was changed Trade.stoploss_reinitialization(self.strategy.stoploss) + # Only update open orders on startup + # This will update the database after the initial migration self.update_open_orders() def process(self) -> None: From 357d7714ec9fa6da153c3ca1d87599ea2adc7419 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Aug 2020 07:31:22 +0200 Subject: [PATCH 046/224] Add docstring to update_trade_state --- freqtrade/freqtradebot.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3305bb584..4a660d867 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1312,6 +1312,11 @@ class FreqtradeBot: """ Checks trades with open orders and updates the amount if necessary Handles closing both buy and sell orders. + :param trade: Trade object of the trade we're analyzing + :param order_id: Order-id of the order we're analyzing + :param action_order: Already aquired order object + :param order_amount: Order-amount - only used in case of partially cancelled buy order + TODO: Investigate if this is really needed, or covered by getting filled in here again. :return: True if order has been cancelled without being filled partially, False otherwise """ if not order_id: From 2d6bcbb454fe2c485148bfcf02a067cba2e57077 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Aug 2020 19:51:31 +0200 Subject: [PATCH 047/224] Fix small error in trades updating --- freqtrade/freqtradebot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4a660d867..c4147e7e4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -254,6 +254,8 @@ class FreqtradeBot: Try refinding a lost trade. Only used when InsufficientFunds appears on sell orders (stoploss or sell). Tries to walk the stored orders and sell them off eventually. + + TODO: maybe remove this method again. """ logger.info(f"Trying to refind lost order for {trade}") for order in trade.orders: @@ -1407,7 +1409,7 @@ class FreqtradeBot: """ fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee. """ - trades = self.exchange.get_trades_for_order(trade.open_order_id, trade.pair, + trades = self.exchange.get_trades_for_order(order['id'], trade.pair, trade.open_date) if len(trades) == 0: From fc42d552ab63f0d6bfb0c7646e088561e6168cfc Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Aug 2020 20:13:06 +0200 Subject: [PATCH 048/224] Convert logs to fstrings --- freqtrade/persistence/models.py | 6 +++--- tests/test_freqtradebot.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 3f918670a..31f40f713 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -379,15 +379,15 @@ class Trade(_DECL_BASE): self.open_rate = Decimal(safe_value_fallback(order, 'average', 'price')) self.amount = Decimal(safe_value_fallback(order, 'filled', 'amount')) self.recalc_open_trade_price() - logger.info('%s_BUY has been fulfilled for %s.', order_type.upper(), self) + logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') self.open_order_id = None elif order_type in ('market', 'limit') and order['side'] == 'sell': self.close(safe_value_fallback(order, 'average', 'price')) - logger.info('%s_SELL has been fulfilled for %s.', order_type.upper(), self) + logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.') elif order_type in ('stop_loss_limit', 'stop-loss', 'stop'): self.stoploss_order_id = None self.close_rate_requested = self.stop_loss - logger.info('%s is hit for %s.', order_type.upper(), self) + logger.info(f'{order_type.upper()} is hit for {self}.') self.close(order['average']) else: raise ValueError(f'Unknown order type: {order_type}') diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 93b71f176..aefaebad5 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1215,7 +1215,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, }) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True - assert log_has('STOP_LOSS_LIMIT is hit for {}.'.format(trade), caplog) + assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog) assert trade.stoploss_order_id is None assert trade.is_open is False From 39beb5c8375c8abe0fbce95c13f2346e7fe16bee Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Aug 2020 08:39:10 +0200 Subject: [PATCH 049/224] Add method to update fees on closed trades --- freqtrade/freqtradebot.py | 22 +++++++++++++++++++++- freqtrade/persistence/models.py | 24 +++++++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c4147e7e4..3783908a2 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -138,6 +138,8 @@ class FreqtradeBot: # This will update the database after the initial migration self.update_open_orders() + self.update_closed_trades_without_assigned_fees() + def process(self) -> None: """ Queries the persistence layer for open trades and handles them, @@ -148,6 +150,8 @@ class FreqtradeBot: # Check whether markets have to be reloaded and reload them when it's needed self.exchange.reload_markets() + self.update_closed_trades_without_assigned_fees() + # Query trades from persistence layer trades = Trade.get_open_trades() @@ -233,7 +237,8 @@ class FreqtradeBot: def update_open_orders(self): """ - Updates open orders based on order list kept in the database + Updates open orders based on order list kept in the database. + Mainly updates the state of orders - but may also close trades """ orders = Order.get_open_orders() logger.info(f"Updating {len(orders)} open orders.") @@ -249,6 +254,21 @@ class FreqtradeBot: except ExchangeError: logger.warning(f"Error updating {order.order_id}") + def update_closed_trades_without_assigned_fees(self): + """ + Update closed trades without close fees assigned. + Only works when Orders are in the database, otherwise the last orderid is unknown. + """ + trades: List[Trade] = Trade.get_sold_trades_without_assigned_fees() + for trade in trades: + + if not trade.is_open and not trade.fee_updated('sell'): + # Get sell fee + order = trade.select_order('sell', 'closed') + if order: + logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.") + self.update_trade_state(trade, order.order_id) + def refind_lost_order(self, trade): """ Try refinding a lost trade. diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 31f40f713..01d2286f9 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional import arrow from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, - String, create_engine, desc, func, inspect) + String, create_engine, desc, func, inspect, or_) from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import Query, relationship @@ -508,6 +508,17 @@ class Trade(_DECL_BASE): profit_ratio = (close_trade_price / self.open_trade_price) - 1 return float(f"{profit_ratio:.8f}") + def select_order(self, order_side: str, status: str): + """ + Returns latest order for this orderside and status + Returns None if nothing is found + """ + orders = [o for o in self.orders if o.side == order_side and o.status == status] + if len(orders) > 0: + return orders[-1] + else: + return None + @staticmethod def get_trades(trade_filter=None) -> Query: """ @@ -539,6 +550,17 @@ class Trade(_DECL_BASE): """ return Trade.get_trades(Trade.open_order_id.isnot(None)).all() + @staticmethod + def get_sold_trades_without_assigned_fees(): + """ + Returns all closed trades which don't have fees set correctly + """ + return Trade.get_trades([Trade.fee_close_currency.is_(None), + Trade.id == 100, + Trade.orders.any(), + Trade.is_open.is_(False), + ]).all() + @staticmethod def total_open_trades_stakes() -> float: """ From fc2104bfad5715d3a9db6e9a5644071dafce6c8f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Aug 2020 09:12:09 +0200 Subject: [PATCH 050/224] Fix bug with time when updating order_date --- freqtrade/persistence/models.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 01d2286f9..a5c047cf4 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -2,7 +2,7 @@ This module contains the class to persist trades into SQLite """ import logging -from datetime import datetime +from datetime import datetime, timezone from decimal import Decimal from typing import Any, Dict, List, Optional @@ -152,11 +152,13 @@ class Order(_DECL_BASE): self.remaining = order.get('remaining', self.remaining) self.cost = order.get('cost', self.cost) if 'timestamp' in order and order['timestamp'] is not None: - self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000) + self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc) if self.status in ('closed', 'canceled', 'cancelled'): self.ft_is_open = False - self.order_update_date = datetime.now() + if order.get('filled', 0) > 0: + self.order_filled_date = arrow.utcnow().datetime + self.order_update_date = arrow.utcnow().datetime @staticmethod def update_orders(orders: List['Order'], order: Dict[str, Any]): From f2b390a27113be687d488df18bd73c014380073d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Aug 2020 09:24:14 +0200 Subject: [PATCH 051/224] Add fetch_order_or_stoploss wrapper --- freqtrade/exchange/exchange.py | 11 +++++++++++ tests/exchange/test_exchange.py | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 578d753a4..64d1a75de 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1039,6 +1039,17 @@ class Exchange: # Assign method to fetch_stoploss_order to allow easy overriding in other classes fetch_stoploss_order = fetch_order + def fetch_order_or_stoploss_order(self, order_id: str, pair: str, + stoploss_order: bool = False) -> Dict: + """ + Simple wrapper calling either fetch_order or fetch_stoploss_order depending on + the stoploss_order parameter + :param stoploss_order: If true, uses fetch_stoploss_order, otherwise fetch_order. + """ + if stoploss_order: + return self.fetch_stoploss_order(order_id, pair) + return self.fetch_order(order_id, pair) + @retrier def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: """ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index d0e303f5f..e68629d3d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1936,6 +1936,31 @@ def test_fetch_stoploss_order(default_conf, mocker, exchange_name): order_id='_', pair='TKN/BTC') +def test_fetch_order_or_stoploss_order(default_conf, mocker): + exchange = get_patched_exchange(mocker, default_conf, id='binance') + fetch_order_mock = MagicMock() + fetch_stoploss_order_mock = MagicMock() + mocker.patch.multiple('freqtrade.exchange.Exchange', + fetch_order=fetch_order_mock, + fetch_stoploss_order=fetch_stoploss_order_mock, + ) + + exchange.fetch_order_or_stoploss_order('1234', 'ETH/BTC', False) + assert fetch_order_mock.call_count == 1 + assert fetch_order_mock.call_args_list[0][0][0] == '1234' + assert fetch_order_mock.call_args_list[0][0][1] == 'ETH/BTC' + assert fetch_stoploss_order_mock.call_count == 0 + + fetch_order_mock.reset_mock() + fetch_stoploss_order_mock.reset_mock() + + exchange.fetch_order_or_stoploss_order('1234', 'ETH/BTC', True) + assert fetch_order_mock.call_count == 0 + assert fetch_stoploss_order_mock.call_count == 1 + assert fetch_stoploss_order_mock.call_args_list[0][0][0] == '1234' + assert fetch_stoploss_order_mock.call_args_list[0][0][1] == 'ETH/BTC' + + @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_name(default_conf, mocker, exchange_name): exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) From 3b4446339e9df77044a99c55d5df9cb2a15ac227 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Aug 2020 09:30:25 +0200 Subject: [PATCH 052/224] Use fetch_order_or_stoploss order --- freqtrade/freqtradebot.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3783908a2..ea1cb5322 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -138,6 +138,7 @@ class FreqtradeBot: # This will update the database after the initial migration self.update_open_orders() + # TODO: remove next call once testing is done - this is called on every iteration. self.update_closed_trades_without_assigned_fees() def process(self) -> None: @@ -244,10 +245,8 @@ class FreqtradeBot: logger.info(f"Updating {len(orders)} open orders.") for order in orders: try: - if order.ft_order_side == 'stoploss': - fo = self.exchange.fetch_stoploss_order(order.order_id, order.ft_pair) - else: - fo = self.exchange.fetch_order(order.order_id, order.ft_pair) + fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, + order.ft_order_side == 'stoploss') self.update_trade_state(order.trade, order.order_id, fo) @@ -257,7 +256,7 @@ class FreqtradeBot: def update_closed_trades_without_assigned_fees(self): """ Update closed trades without close fees assigned. - Only works when Orders are in the database, otherwise the last orderid is unknown. + Only acts when Orders are in the database, otherwise the last orderid is unknown. """ trades: List[Trade] = Trade.get_sold_trades_without_assigned_fees() for trade in trades: @@ -267,7 +266,8 @@ class FreqtradeBot: order = trade.select_order('sell', 'closed') if order: logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.") - self.update_trade_state(trade, order.order_id) + self.update_trade_state(trade, order.order_id, + order.ft_order_side == 'stoploss') def refind_lost_order(self, trade): """ @@ -282,13 +282,13 @@ class FreqtradeBot: logger.info(f"Trying to refind {order}") fo = None try: + fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, + order.ft_order_side == 'stoploss') if order.ft_order_side == 'stoploss': - fo = self.exchange.fetch_stoploss_order(order.order_id, order.ft_pair) if fo and fo['status'] == 'open': # Assume this as the open stoploss order trade.stoploss_order_id = order.order_id elif order.ft_order_side == 'sell': - fo = self.exchange.fetch_order(order.order_id, order.ft_pair) if fo and fo['status'] == 'open': # Assume this as the open order trade.open_order_id = order.order_id @@ -296,7 +296,8 @@ class FreqtradeBot: # No action for buy orders ... continue if fo: - self.update_trade_state(trade, order.order_id, fo) + self.update_trade_state(trade, order.order_id, fo, + order.ft_order_side == 'stoploss') except ExchangeError: logger.warning(f"Error updating {order.order_id}") @@ -902,7 +903,8 @@ class FreqtradeBot: # We check if stoploss order is fulfilled if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value - self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order) + self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, + stoploss_order=True) # Lock pair for one candle to prevent immediate rebuys self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe'])) @@ -1330,7 +1332,7 @@ class FreqtradeBot: # def update_trade_state(self, trade: Trade, order_id: str, action_order: dict = None, - order_amount: float = None) -> bool: + order_amount: float = None, stoploss_order: bool = False) -> bool: """ Checks trades with open orders and updates the amount if necessary Handles closing both buy and sell orders. @@ -1348,7 +1350,9 @@ class FreqtradeBot: # Update trade with order values logger.info('Found open order for %s', trade) try: - order = action_order or self.exchange.fetch_order(order_id, trade.pair) + order = action_order or self.exchange.fetch_order_or_stoploss_order(order_id, + trade.pair, + stoploss_order) except InvalidOrderException as exception: logger.warning('Unable to fetch order %s: %s', order_id, exception) return False From fd33282eb1ecbd954c2d757b3e4efef9f174365a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Aug 2020 15:48:00 +0200 Subject: [PATCH 053/224] Add handle_insufficient exception --- freqtrade/freqtradebot.py | 31 +++++++++++++++++++++++++------ freqtrade/persistence/models.py | 7 ++++--- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ea1cb5322..2656d3d3a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -269,6 +269,27 @@ class FreqtradeBot: self.update_trade_state(trade, order.order_id, order.ft_order_side == 'stoploss') + def handle_insufficient_funds(self, trade: Trade): + """ + """ + sell_order = trade.select_order('sell', None) + if sell_order: + self.refind_lost_order(trade) + else: + self.reupdate_buy_order_fees(trade) + + # See if we ever opened a sell order for this + # If not, try update buy fees + + def reupdate_buy_order_fees(self, trade: Trade): + """ + """ + logger.info(f"Trying to reupdate buy fees for {trade}") + order = trade.select_order('buy', 'closed') + if order: + logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") + self.update_trade_state(trade, order.order_id) + def refind_lost_order(self, trade): """ Try refinding a lost trade. @@ -864,9 +885,8 @@ class FreqtradeBot: return True except InsufficientFundsError as e: logger.warning(f"Unable to place stoploss order {e}.") - # Try refinding stoploss order - # TODO: Currently disabled to allow testing without this first - # self.refind_lost_order(trade) + # Try to figure out what went wrong + self.handle_insufficient_funds(trade) except InvalidOrderException as e: trade.stoploss_order_id = None @@ -1221,9 +1241,8 @@ class FreqtradeBot: ) except InsufficientFundsError as e: logger.warning(f"Unable to place order {e}.") - # Try refinding "lost" orders - # TODO: Currently disabled to allow testing without this first - # self.refind_lost_order(trade) + # Try to figure out what went wrong + self.handle_insufficient_funds(trade) return False order_obj = Order.parse_from_ccxt_object(order, trade.pair, 'sell') diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a5c047cf4..d66108fce 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -510,12 +510,14 @@ class Trade(_DECL_BASE): profit_ratio = (close_trade_price / self.open_trade_price) - 1 return float(f"{profit_ratio:.8f}") - def select_order(self, order_side: str, status: str): + def select_order(self, order_side: str, status: Optional[str]): """ Returns latest order for this orderside and status Returns None if nothing is found """ - orders = [o for o in self.orders if o.side == order_side and o.status == status] + orders = [o for o in self.orders if o.side == order_side] + if status: + orders = [o for o in orders if o.status == status] if len(orders) > 0: return orders[-1] else: @@ -558,7 +560,6 @@ class Trade(_DECL_BASE): Returns all closed trades which don't have fees set correctly """ return Trade.get_trades([Trade.fee_close_currency.is_(None), - Trade.id == 100, Trade.orders.any(), Trade.is_open.is_(False), ]).all() From 11e69bdd65b5951fbf4bfb33678cb3c90e7649e5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Aug 2020 15:48:42 +0200 Subject: [PATCH 054/224] Update open trades too --- freqtrade/freqtradebot.py | 21 ++++++++++++++------- freqtrade/persistence/models.py | 12 +++++++++++- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2656d3d3a..5e782a353 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -267,10 +267,20 @@ class FreqtradeBot: if order: logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.") self.update_trade_state(trade, order.order_id, - order.ft_order_side == 'stoploss') + stoploss_order=order.ft_order_side == 'stoploss') + + trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() + for trade in trades: + if trade.is_open and not trade.fee_updated('buy'): + order = trade.select_order('buy', 'closed') + if order: + logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") + self.update_trade_state(trade, order.order_id) def handle_insufficient_funds(self, trade: Trade): """ + Determine if we ever opened a sell order for this trade. + If not, try update buy fees - otherwise "refind" the open order we obviously lost. """ sell_order = trade.select_order('sell', None) if sell_order: @@ -278,11 +288,10 @@ class FreqtradeBot: else: self.reupdate_buy_order_fees(trade) - # See if we ever opened a sell order for this - # If not, try update buy fees - def reupdate_buy_order_fees(self, trade: Trade): """ + Get buy order from database, and try to reupdate. + Handles trades where the initial fee-update did not work. """ logger.info(f"Trying to reupdate buy fees for {trade}") order = trade.select_order('buy', 'closed') @@ -295,8 +304,6 @@ class FreqtradeBot: Try refinding a lost trade. Only used when InsufficientFunds appears on sell orders (stoploss or sell). Tries to walk the stored orders and sell them off eventually. - - TODO: maybe remove this method again. """ logger.info(f"Trying to refind lost order for {trade}") for order in trade.orders: @@ -318,7 +325,7 @@ class FreqtradeBot: continue if fo: self.update_trade_state(trade, order.order_id, fo, - order.ft_order_side == 'stoploss') + stoploss_order=order.ft_order_side == 'stoploss') except ExchangeError: logger.warning(f"Error updating {order.order_id}") diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index d66108fce..f9eeedd4b 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional import arrow from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, - String, create_engine, desc, func, inspect, or_) + String, create_engine, desc, func, inspect) from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import Query, relationship @@ -554,6 +554,16 @@ class Trade(_DECL_BASE): """ return Trade.get_trades(Trade.open_order_id.isnot(None)).all() + @staticmethod + def get_open_trades_without_assigned_fees(): + """ + Returns all open trades which don't have open fees set correctly + """ + return Trade.get_trades([Trade.fee_open_currency.is_(None), + Trade.orders.any(), + Trade.is_open.is_(True), + ]).all() + @staticmethod def get_sold_trades_without_assigned_fees(): """ From 3d7e800ff2e00c2e8e580d2868b07d6cf21fbb88 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Aug 2020 16:08:54 +0200 Subject: [PATCH 055/224] Remove test code --- freqtrade/freqtradebot.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5e782a353..6d2f9ddcc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -138,9 +138,6 @@ class FreqtradeBot: # This will update the database after the initial migration self.update_open_orders() - # TODO: remove next call once testing is done - this is called on every iteration. - self.update_closed_trades_without_assigned_fees() - def process(self) -> None: """ Queries the persistence layer for open trades and handles them, From 674b510d2345ebcb7a8207588b44a1a6b25d9465 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Aug 2020 17:35:42 +0200 Subject: [PATCH 056/224] Parametrize fetch_order retry counts --- freqtrade/exchange/common.py | 4 ++++ freqtrade/exchange/exchange.py | 5 +++-- freqtrade/exchange/ftx.py | 4 ++-- tests/exchange/test_exchange.py | 22 ++++++++++++---------- tests/exchange/test_ftx.py | 5 ++--- tests/exchange/test_kraken.py | 2 -- 6 files changed, 23 insertions(+), 19 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 3bba9be72..539bcef22 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -9,7 +9,11 @@ from freqtrade.exceptions import (DDosProtection, RetryableOrderError, logger = logging.getLogger(__name__) +# Maximum default retry count. +# Functions are always called RETRY_COUNT + 1 times (for the original call) API_RETRY_COUNT = 4 +API_FETCH_ORDER_RETRY_COUNT = 3 + BAD_EXCHANGES = { "bitmex": "Various reasons.", "bitstamp": "Does not provide history. " diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 64d1a75de..3b2b56d13 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -23,7 +23,8 @@ from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, RetryableOrderError, TemporaryError) -from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async +from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, + BAD_EXCHANGES, retrier, retrier_async) from freqtrade.misc import deep_merge_dicts, safe_value_fallback2 CcxtModuleType = Any @@ -1010,7 +1011,7 @@ class Exchange: return order - @retrier(retries=5) + @retrier(retries=API_FETCH_ORDER_RETRY_COUNT) def fetch_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: try: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 27051a945..39100d0b7 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -8,7 +8,7 @@ from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange -from freqtrade.exchange.common import retrier +from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier logger = logging.getLogger(__name__) @@ -78,7 +78,7 @@ class Ftx(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e - @retrier(retries=5) + @retrier(retries=API_FETCH_ORDER_RETRY_COUNT) def fetch_stoploss_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: try: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index e68629d3d..a05377702 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1,5 +1,3 @@ -# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement -# pragma pylint: disable=protected-access import copy import logging from datetime import datetime, timezone @@ -11,10 +9,12 @@ import ccxt import pytest from pandas import DataFrame -from freqtrade.exceptions import (DependencyException, InvalidOrderException, DDosProtection, - OperationalException, TemporaryError) +from freqtrade.exceptions import (DDosProtection, DependencyException, + InvalidOrderException, OperationalException, + TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken -from freqtrade.exchange.common import API_RETRY_COUNT, calculate_backoff +from freqtrade.exchange.common import (API_RETRY_COUNT, API_FETCH_ORDER_RETRY_COUNT, + calculate_backoff) from freqtrade.exchange.exchange import (market_is_active, symbol_is_pair, timeframe_to_minutes, timeframe_to_msecs, @@ -1894,12 +1894,14 @@ def test_fetch_order(default_conf, mocker, exchange_name): # Ensure backoff is called assert tm.call_args_list[0][0][0] == 1 assert tm.call_args_list[1][0][0] == 2 - assert tm.call_args_list[2][0][0] == 5 - assert tm.call_args_list[3][0][0] == 10 - assert api_mock.fetch_order.call_count == 6 + if API_FETCH_ORDER_RETRY_COUNT > 2: + assert tm.call_args_list[2][0][0] == 5 + if API_FETCH_ORDER_RETRY_COUNT > 3: + assert tm.call_args_list[3][0][0] == 10 + assert api_mock.fetch_order.call_count == API_FETCH_ORDER_RETRY_COUNT + 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - 'fetch_order', 'fetch_order', retries=6, + 'fetch_order', 'fetch_order', retries=API_FETCH_ORDER_RETRY_COUNT + 1, order_id='_', pair='TKN/BTC') @@ -1932,7 +1934,7 @@ def test_fetch_stoploss_order(default_conf, mocker, exchange_name): ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, 'fetch_stoploss_order', 'fetch_order', - retries=6, + retries=API_FETCH_ORDER_RETRY_COUNT + 1, order_id='_', pair='TKN/BTC') diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index bed92d276..16789af2c 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -1,5 +1,3 @@ -# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement -# pragma pylint: disable=protected-access from random import randint from unittest.mock import MagicMock @@ -7,6 +5,7 @@ import ccxt import pytest from freqtrade.exceptions import DependencyException, InvalidOrderException +from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT from tests.conftest import get_patched_exchange from .test_exchange import ccxt_exceptionhandlers @@ -154,5 +153,5 @@ def test_fetch_stoploss_order(default_conf, mocker): ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'ftx', 'fetch_stoploss_order', 'fetch_orders', - retries=6, + retries=API_FETCH_ORDER_RETRY_COUNT + 1, order_id='_', pair='TKN/BTC') diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 9451c0b9e..8f774a7ec 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -1,5 +1,3 @@ -# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement -# pragma pylint: disable=protected-access from random import randint from unittest.mock import MagicMock From 9ba9f73706361e8f5d8cd93b778645d636dead15 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Aug 2020 16:04:32 +0200 Subject: [PATCH 057/224] Improve logging, don't search for buy orders in refind_lost_order --- freqtrade/freqtradebot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6d2f9ddcc..9db60be43 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -306,6 +306,9 @@ class FreqtradeBot: for order in trade.orders: logger.info(f"Trying to refind {order}") fo = None + if order.ft_order_side == 'buy': + # Skip buy side - this is handled by reupdate_buy_order_fees + continue try: fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, order.ft_order_side == 'stoploss') @@ -321,6 +324,7 @@ class FreqtradeBot: # No action for buy orders ... continue if fo: + logger.info(f"Found {order} for trade {trade}.jj") self.update_trade_state(trade, order.order_id, fo, stoploss_order=order.ft_order_side == 'stoploss') From 38809acde817c27262c119b7bab157b19ad9e34a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Aug 2020 06:50:43 +0200 Subject: [PATCH 058/224] Don't rerun for known closed orders --- freqtrade/freqtradebot.py | 4 ++++ freqtrade/persistence/models.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index cb44165f4..498b3eea6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -306,6 +306,10 @@ class FreqtradeBot: for order in trade.orders: logger.info(f"Trying to refind {order}") fo = None + if not order.ft_is_open: + # TODO: Does this need to be info level? + logger.info(f"Order {order} is no longer open.") + continue if order.ft_order_side == 'buy': # Skip buy side - this is handled by reupdate_buy_order_fees continue diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index c3a112828..e0b9624dd 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -132,7 +132,7 @@ class Order(_DECL_BASE): def __repr__(self): return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' - f'side={self.side}, status={self.status})') + f'side={self.side}, order_type={self.order_type}, status={self.status})') def update_from_ccxt_object(self, order): """ From 26f45c83234900dd8c2f6407138c2aa454d32f81 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Aug 2020 06:56:56 +0200 Subject: [PATCH 059/224] Improve logmessage for trailing stoploss --- freqtrade/freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 498b3eea6..faa67f504 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -988,8 +988,8 @@ class FreqtradeBot: update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: # cancelling the current stoploss on exchange first - logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s}) ' - 'in order to add another one ...', order['id']) + logger.info(f"Cancelling current stoploss on exchange for pair {trade.pair} " + f"(orderid:{order['id']}) in order to add another one ...") try: co = self.exchange.cancel_stoploss_order(order['id'], trade.pair) trade.update_order(co) From d161b94d7241e9f5c5bc873a8e8f1282aba1738e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Aug 2020 21:22:36 +0200 Subject: [PATCH 060/224] Allow simulating cancelled orders in dry-run --- freqtrade/exchange/exchange.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d84fe7b82..b89da14eb 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -973,7 +973,12 @@ class Exchange: @retrier def cancel_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: - return {} + order = self._dry_run_open_orders.get(order_id) + if order: + order.update({'status': 'canceled', 'filled': 0.0, 'remaining': order['amount']}) + return order + else: + return {} try: return self._api.cancel_order(order_id, pair) From add78414e4c9aa1d1b3a9847d3ae0a20fb332e1d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Aug 2020 21:24:47 +0200 Subject: [PATCH 061/224] Don't overwrite cancel_reason --- freqtrade/constants.py | 1 + freqtrade/freqtradebot.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 1f8cebd0d..f44be220e 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -341,6 +341,7 @@ CANCEL_REASON = { "PARTIALLY_FILLED": "partially filled - keeping order open", "ALL_CANCELLED": "cancelled (all unfilled and partially filled open orders cancelled)", "CANCELLED_ON_EXCHANGE": "cancelled on exchange", + "FORCE_SELL": "forcesold", } # List of pairs with their timeframes diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index eee60cc22..917bb356f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -974,7 +974,8 @@ class FreqtradeBot: # Cancelled orders may have the status of 'canceled' or 'closed' if order['status'] not in ('canceled', 'closed'): - reason = constants.CANCEL_REASON['TIMEOUT'] + # TODO: this reason will overwrite the input in all cases + # reason = constants.CANCEL_REASON['TIMEOUT'] corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, trade.amount) # Avoid race condition where the order could not be cancelled coz its already filled. From 85e71275d3c555b04a7ab8bb15269c0a6711830f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Aug 2020 21:27:09 +0200 Subject: [PATCH 062/224] Simplify forcesell method by using freqtrade methods --- freqtrade/rpc/rpc.py | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 12e79d35b..25a85ac02 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -11,6 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union import arrow from numpy import NAN, mean +from freqtrade.constants import CANCEL_REASON from freqtrade.exceptions import (ExchangeError, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs @@ -453,29 +454,22 @@ class RPC: """ def _exec_forcesell(trade: Trade) -> None: # Check if there is there is an open order + fully_canceled = False if trade.open_order_id: order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) - # Cancel open LIMIT_BUY orders and close trade - if order and order['status'] == 'open' \ - and order['type'] == 'limit' \ - and order['side'] == 'buy': - self._freqtrade.exchange.cancel_order(trade.open_order_id, trade.pair) - trade.close(order.get('price') or trade.open_rate) - # Do the best effort, if we don't know 'filled' amount, don't try selling - if order['filled'] is None: - return - trade.amount = order['filled'] + if order['side'] == 'buy': + fully_canceled = self._freqtrade.handle_cancel_buy( + trade, order, CANCEL_REASON['FORCE_SELL']) - # Ignore trades with an attached LIMIT_SELL order - if order and order['status'] == 'open' \ - and order['type'] == 'limit' \ - and order['side'] == 'sell': - return + if order['side'] == 'sell': + # Cancel order - so it is placed anew with a fresh price. + self._freqtrade.handle_cancel_sell(trade, order, CANCEL_REASON['FORCE_SELL']) - # Get current rate and execute sell - current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL) + if not fully_canceled: + # Get current rate and execute sell + current_rate = self._freqtrade.get_sell_rate(trade.pair, False) + self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL) # ---- EOF def _exec_forcesell ---- if self._freqtrade.state != State.RUNNING: From 5e75caa91778908abd14bbd8bcfc4c59e2d0c2ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Aug 2020 21:37:52 +0200 Subject: [PATCH 063/224] Adjust tests to new forcesell --- tests/rpc/test_rpc.py | 33 +++++++++++++++++++++++++++------ tests/rpc/test_rpc_telegram.py | 10 ++++++---- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index c370dce8f..102ed12fe 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -669,7 +669,8 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: return_value={ 'status': 'closed', 'type': 'limit', - 'side': 'buy' + 'side': 'buy', + 'filled': 0.0, } ), get_fee=fee, @@ -695,6 +696,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: msg = rpc._rpc_forcesell('all') assert msg == {'result': 'Created sell orders for all open trades.'} + freqtradebot.enter_positions() msg = rpc._rpc_forcesell('1') assert msg == {'result': 'Created sell order for trade 1.'} @@ -707,17 +709,24 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: freqtradebot.state = State.RUNNING assert cancel_order_mock.call_count == 0 + freqtradebot.enter_positions() # make an limit-buy open trade trade = Trade.query.filter(Trade.id == '1').first() filled_amount = trade.amount / 2 + # Fetch order - it's open first, and closed after cancel_order is called. mocker.patch( 'freqtrade.exchange.Exchange.fetch_order', - return_value={ + side_effect=[{ 'status': 'open', 'type': 'limit', 'side': 'buy', 'filled': filled_amount - } + }, { + 'status': 'closed', + 'type': 'limit', + 'side': 'buy', + 'filled': filled_amount + }] ) # check that the trade is called, which is done by ensuring exchange.cancel_order is called # and trade amount is updated @@ -725,6 +734,16 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: assert cancel_order_mock.call_count == 1 assert trade.amount == filled_amount + mocker.patch( + 'freqtrade.exchange.Exchange.fetch_order', + return_value={ + 'status': 'open', + 'type': 'limit', + 'side': 'buy', + 'filled': filled_amount + }) + + freqtradebot.config['max_open_trades'] = 3 freqtradebot.enter_positions() trade = Trade.query.filter(Trade.id == '2').first() amount = trade.amount @@ -744,20 +763,22 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: assert cancel_order_mock.call_count == 2 assert trade.amount == amount - freqtradebot.enter_positions() # make an limit-sell open trade mocker.patch( 'freqtrade.exchange.Exchange.fetch_order', return_value={ 'status': 'open', 'type': 'limit', - 'side': 'sell' + 'side': 'sell', + 'amount': amount, + 'remaining': amount, + 'filled': 0.0 } ) msg = rpc._rpc_forcesell('3') assert msg == {'result': 'Created sell order for trade 3.'} # status quo, no exchange calls - assert cancel_order_mock.call_count == 2 + assert cancel_order_mock.call_count == 3 def test_performance_handle(default_conf, ticker, limit_buy_order, fee, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 10738ada3..b11409767 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -724,7 +724,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, context.args = ["1"] telegram._forcesell(update=update, context=context) - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 4 last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, @@ -783,7 +783,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, context.args = ["1"] telegram._forcesell(update=update, context=context) - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 4 last_msg = rpc_mock.call_args_list[-1][0][0] assert { @@ -833,8 +833,10 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None context.args = ["all"] telegram._forcesell(update=update, context=context) - assert rpc_mock.call_count == 4 - msg = rpc_mock.call_args_list[0][0][0] + # Called for all trades 3 times + # cancel notification (wtf??), sell notification, buy_cancel + assert rpc_mock.call_count == 12 + msg = rpc_mock.call_args_list[2][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, From 9c0a3fffd733dc22f0498ea9cb72758d61ff0738 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Aug 2020 22:17:43 +0200 Subject: [PATCH 064/224] Avoid double notifications in case of partially filled buy orders --- freqtrade/constants.py | 4 +++- freqtrade/freqtradebot.py | 15 +++++++-------- freqtrade/rpc/telegram.py | 2 +- tests/rpc/test_rpc_telegram.py | 6 ++++-- tests/test_freqtradebot.py | 6 +++--- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index f44be220e..b92ab3eeb 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -338,7 +338,9 @@ SCHEMA_MINIMAL_REQUIRED = [ CANCEL_REASON = { "TIMEOUT": "cancelled due to timeout", - "PARTIALLY_FILLED": "partially filled - keeping order open", + "PARTIALLY_FILLED_KEEP_OPEN": "partially filled - keeping order open", + "PARTIALLY_FILLED": "partially filled", + "FULLY_CANCELLED": "fully cancelled", "ALL_CANCELLED": "cancelled (all unfilled and partially filled open orders cancelled)", "CANCELLED_ON_EXCHANGE": "cancelled on exchange", "FORCE_SELL": "forcesold", diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 917bb356f..d53902633 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -618,7 +618,7 @@ class FreqtradeBot: # Send the message self.rpc.send_msg(msg) - def _notify_buy_cancel(self, trade: Trade, order_type: str) -> None: + def _notify_buy_cancel(self, trade: Trade, order_type: str, reason: str) -> None: """ Sends rpc notification when a buy cancel occured. """ @@ -637,6 +637,7 @@ class FreqtradeBot: 'amount': trade.amount, 'open_date': trade.open_date, 'current_rate': current_rate, + 'reason': reason, } # Send the message @@ -993,13 +994,13 @@ class FreqtradeBot: # Using filled to determine the filled amount filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled') - if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC): logger.info('Buy order fully cancelled. Removing %s from database.', trade) # if trade is not partially completed, just delete the trade Trade.session.delete(trade) Trade.session.flush() was_trade_fully_canceled = True + reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}" else: # if trade is partially complete, edit the stake details for the trade # and close the order @@ -1012,13 +1013,11 @@ class FreqtradeBot: trade.open_order_id = None logger.info('Partial buy order timeout for %s.', trade) - self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'Remaining buy order for {trade.pair} cancelled due to timeout' - }) + reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" self.wallets.update() - self._notify_buy_cancel(trade, order_type=self.strategy.order_types['buy']) + self._notify_buy_cancel(trade, order_type=self.strategy.order_types['buy'], + reason=reason) return was_trade_fully_canceled def handle_cancel_sell(self, trade: Trade, order: Dict, reason: str) -> str: @@ -1049,7 +1048,7 @@ class FreqtradeBot: trade.open_order_id = None else: # TODO: figure out how to handle partially complete sell orders - reason = constants.CANCEL_REASON['PARTIALLY_FILLED'] + reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] self.wallets.update() self._notify_sell_cancel( diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 458007c04..ecf907f54 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -149,7 +149,7 @@ class Telegram(RPC): elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: message = ("\N{WARNING SIGN} *{exchange}:* " - "Cancelling Open Buy Order for {pair}".format(**msg)) + "Cancelling open buy Order for {pair}. Reason: {reason}.".format(**msg)) elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: msg['amount'] = round(msg['amount'], 8) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index b11409767..145df9ed7 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -14,6 +14,7 @@ from telegram import Chat, Message, Update from telegram.error import NetworkError from freqtrade import __version__ +from freqtrade.constants import CANCEL_REASON from freqtrade.edge import PairInfo from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade @@ -1310,9 +1311,10 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', + 'reason': CANCEL_REASON['TIMEOUT'] }) - assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Bittrex:* Cancelling Open Buy Order for ETH/BTC') + assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Bittrex:* ' + 'Cancelling open buy Order for ETH/BTC. Reason: cancelled due to timeout.') def test_send_msg_sell_notification(default_conf, mocker) -> None: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0d7968e26..7b4ed47f1 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2527,13 +2527,13 @@ def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None: send_msg_mock.reset_mock() order['amount'] = 2 - assert freqtrade.handle_cancel_sell(trade, order, reason) == CANCEL_REASON['PARTIALLY_FILLED'] + assert freqtrade.handle_cancel_sell(trade, order, reason) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] # Assert cancel_order was not called (callcount remains unchanged) assert cancel_order_mock.call_count == 1 assert send_msg_mock.call_count == 1 - assert freqtrade.handle_cancel_sell(trade, order, reason) == CANCEL_REASON['PARTIALLY_FILLED'] + assert freqtrade.handle_cancel_sell(trade, order, reason) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] # Message should not be iterated again - assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED'] + assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] assert send_msg_mock.call_count == 1 From b2373fccfd8a5441c4639f8934d344a40a921e2b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Aug 2020 22:24:45 +0200 Subject: [PATCH 065/224] Adjust tests as send_msg is only called once --- freqtrade/freqtradebot.py | 2 -- tests/rpc/test_rpc_telegram.py | 10 +++++----- tests/test_freqtradebot.py | 12 +++++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d53902633..66d687536 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -975,8 +975,6 @@ class FreqtradeBot: # Cancelled orders may have the status of 'canceled' or 'closed' if order['status'] not in ('canceled', 'closed'): - # TODO: this reason will overwrite the input in all cases - # reason = constants.CANCEL_REASON['TIMEOUT'] corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, trade.amount) # Avoid race condition where the order could not be cancelled coz its already filled. diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 145df9ed7..a5e501390 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -725,7 +725,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, context.args = ["1"] telegram._forcesell(update=update, context=context) - assert rpc_mock.call_count == 4 + assert rpc_mock.call_count == 3 last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, @@ -784,7 +784,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, context.args = ["1"] telegram._forcesell(update=update, context=context) - assert rpc_mock.call_count == 4 + assert rpc_mock.call_count == 3 last_msg = rpc_mock.call_args_list[-1][0][0] assert { @@ -836,8 +836,8 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None # Called for all trades 3 times # cancel notification (wtf??), sell notification, buy_cancel - assert rpc_mock.call_count == 12 - msg = rpc_mock.call_args_list[2][0][0] + assert rpc_mock.call_count == 8 + msg = rpc_mock.call_args_list[1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, @@ -1314,7 +1314,7 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: 'reason': CANCEL_REASON['TIMEOUT'] }) assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Bittrex:* ' - 'Cancelling open buy Order for ETH/BTC. Reason: cancelled due to timeout.') + 'Cancelling open buy Order for ETH/BTC. Reason: cancelled due to timeout.') def test_send_msg_sell_notification(default_conf, mocker) -> None: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 7b4ed47f1..ac6d3791a 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2289,7 +2289,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 == 2 + assert rpc_mock.call_count == 1 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 @@ -2324,7 +2324,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.*", caplog) assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 1 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() assert len(trades) == 1 # Verify that trade has been updated @@ -2364,7 +2364,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 == 2 + assert rpc_mock.call_count == 1 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() assert len(trades) == 1 # Verify that trade has been updated @@ -2527,11 +2527,13 @@ def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None: send_msg_mock.reset_mock() order['amount'] = 2 - assert freqtrade.handle_cancel_sell(trade, order, reason) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] + assert freqtrade.handle_cancel_sell(trade, order, reason + ) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] # Assert cancel_order was not called (callcount remains unchanged) assert cancel_order_mock.call_count == 1 assert send_msg_mock.call_count == 1 - assert freqtrade.handle_cancel_sell(trade, order, reason) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] + assert freqtrade.handle_cancel_sell(trade, order, reason + ) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] # Message should not be iterated again assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] assert send_msg_mock.call_count == 1 From 289425a434e5e7b3f010beca8639623a5e6982a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Aug 2020 10:07:02 +0200 Subject: [PATCH 066/224] Add test for dry-run-cancel order --- tests/exchange/test_exchange.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 571053b44..c254d6a09 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1761,6 +1761,14 @@ def test_cancel_order_dry_run(default_conf, mocker, exchange_name): assert exchange.cancel_order(order_id='123', pair='TKN/BTC') == {} assert exchange.cancel_stoploss_order(order_id='123', pair='TKN/BTC') == {} + order = exchange.buy('ETH/BTC', 'limit', 5, 0.55, 'gtc') + + cancel_order = exchange.cancel_order(order_id=order['id'], pair='ETH/BTC') + assert order['id'] == cancel_order['id'] + assert order['amount'] == cancel_order['amount'] + assert order['pair'] == cancel_order['pair'] + assert cancel_order['status'] == 'canceled' + @pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("order,result", [ From a595d23bf16b7488ac5c5c0e3d16ac7c2fa96411 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Aug 2020 10:14:49 +0200 Subject: [PATCH 067/224] Improve comment in test --- tests/rpc/test_rpc_telegram.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index bce20a043..51298d8f3 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -835,8 +835,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None context.args = ["all"] telegram._forcesell(update=update, context=context) - # Called for all trades 3 times - # cancel notification (wtf??), sell notification, buy_cancel + # Called for each trade 3 times assert rpc_mock.call_count == 8 msg = rpc_mock.call_args_list[1][0][0] assert { From 284d39930fc6b94912cdb4d45bc8e8df7307feb0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Aug 2020 10:07:28 +0200 Subject: [PATCH 068/224] Allow using pairlists through dataprovider in backtesting --- freqtrade/data/dataprovider.py | 6 ++++++ freqtrade/optimize/backtesting.py | 1 + 2 files changed, 7 insertions(+) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 3b4de823f..ccb6cbf56 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -39,6 +39,12 @@ class DataProvider: """ self.__cached_pairs[(pair, timeframe)] = (dataframe, Arrow.utcnow().datetime) + def add_pairlisthandler(self, pairlists) -> None: + """ + Allow adding pairlisthandler after initialization + """ + self._pairlists = pairlists + def refresh(self, pairlist: ListPairsWithTimeframes, helping_pairs: ListPairsWithTimeframes = None) -> None: diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3bd75f61a..005ec9fb8 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -96,6 +96,7 @@ class Backtesting: "PrecisionFilter not allowed for backtesting multiple strategies." ) + dataprovider.add_pairlisthandler(self.pairlists) self.pairlists.refresh_pairlist() if len(self.pairlists.whitelist) == 0: From 842eff95eba81ddaa34e71ac1d4082ed163d6630 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Aug 2020 10:07:58 +0200 Subject: [PATCH 069/224] Add simple verification to ensure pairlists is iitialized --- tests/optimize/test_backtesting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 52d8f217c..f5c313520 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -359,6 +359,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: ] for line in exists: assert log_has(line, caplog) + assert backtesting.strategy.dp._pairlists is not None def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None: From 3d39f05c8fabe2d237a9207aab026dcbc6878fbe Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Aug 2020 10:23:14 +0200 Subject: [PATCH 070/224] Improve release documetation --- docs/developer.md | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index f09ae2c76..8bee1fd8e 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -52,6 +52,7 @@ The fastest and easiest way to start up is to use docker-compose.develop which g * [docker-compose](https://docs.docker.com/compose/install/) #### Starting the bot + ##### Use the develop dockerfile ``` bash @@ -74,7 +75,7 @@ docker-compose up docker-compose build ``` -##### Execing (effectively SSH into the container) +##### Executing (effectively SSH into the container) The `exec` command requires that the container already be running, if you want to start it that can be effected by `docker-compose up` or `docker-compose run freqtrade_develop` @@ -127,7 +128,7 @@ First of all, have a look at the [VolumePairList](https://github.com/freqtrade/f This is a simple Handler, which however serves as a good example on how to start developing. -Next, modify the classname of the Handler (ideally align this with the module filename). +Next, modify the class-name of the Handler (ideally align this with the module filename). The base-class provides an instance of the exchange (`self._exchange`) the pairlist manager (`self._pairlistmanager`), as well as the main configuration (`self._config`), the pairlist dedicated configuration (`self._pairlistconfig`) and the absolute position within the list of pairlists. @@ -147,7 +148,7 @@ Configuration for the chain of Pairlist Handlers is done in the bot configuratio By convention, `"number_assets"` is used to specify the maximum number of pairs to keep in the pairlist. Please follow this to ensure a consistent user experience. -Additional parameters can be configured as needed. For instance, `VolumePairList` uses `"sort_key"` to specify the sorting value - however feel free to specify whatever is necessary for your great algorithm to be successfull and dynamic. +Additional parameters can be configured as needed. For instance, `VolumePairList` uses `"sort_key"` to specify the sorting value - however feel free to specify whatever is necessary for your great algorithm to be successful and dynamic. #### short_desc @@ -163,7 +164,7 @@ This is called with each iteration of the bot (only if the Pairlist Handler is a It must return the resulting pairlist (which may then be passed into the chain of Pairlist Handlers). -Validations are optional, the parent class exposes a `_verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filtering. Use this if you limit your result to a certain number of pairs - so the endresult is not shorter than expected. +Validations are optional, the parent class exposes a `_verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filtering. Use this if you limit your result to a certain number of pairs - so the end-result is not shorter than expected. #### filter_pairlist @@ -171,13 +172,13 @@ This method is called for each Pairlist Handler in the chain by the pairlist man This is called with each iteration of the bot - so consider implementing caching for compute/network heavy calculations. -It get's passed a pairlist (which can be the result of previous pairlists) as well as `tickers`, a pre-fetched version of `get_tickers()`. +It gets passed a pairlist (which can be the result of previous pairlists) as well as `tickers`, a pre-fetched version of `get_tickers()`. The default implementation in the base class simply calls the `_validate_pair()` method for each pair in the pairlist, but you may override it. So you should either implement the `_validate_pair()` in your Pairlist Handler or override `filter_pairlist()` to do something else. If overridden, it must return the resulting pairlist (which may then be passed into the next Pairlist Handler in the chain). -Validations are optional, the parent class exposes a `_verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filters. Use this if you limit your result to a certain number of pairs - so the endresult is not shorter than expected. +Validations are optional, the parent class exposes a `_verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filters. Use this if you limit your result to a certain number of pairs - so the end result is not shorter than expected. In `VolumePairList`, this implements different methods of sorting, does early validation so only the expected number of pairs is returned. @@ -201,7 +202,7 @@ Most exchanges supported by CCXT should work out of the box. Check if the new exchange supports Stoploss on Exchange orders through their API. -Since CCXT does not provide unification for Stoploss On Exchange yet, we'll need to implement the exchange-specific parameters ourselfs. Best look at `binance.py` for an example implementation of this. You'll need to dig through the documentation of the Exchange's API on how exactly this can be done. [CCXT Issues](https://github.com/ccxt/ccxt/issues) may also provide great help, since others may have implemented something similar for their projects. +Since CCXT does not provide unification for Stoploss On Exchange yet, we'll need to implement the exchange-specific parameters ourselves. Best look at `binance.py` for an example implementation of this. You'll need to dig through the documentation of the Exchange's API on how exactly this can be done. [CCXT Issues](https://github.com/ccxt/ccxt/issues) may also provide great help, since others may have implemented something similar for their projects. ### Incomplete candles @@ -274,6 +275,7 @@ git checkout -b new_release Determine if crucial bugfixes have been made between this commit and the current state, and eventually cherry-pick these. +* Merge the release branch (master) into this branch. * Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7.1` should we need to do a second release that month. Version numbers must follow allowed versions from PEP0440 to avoid failures pushing to pypi. * Commit this part * push that branch to the remote and create a PR against the master branch @@ -281,14 +283,14 @@ Determine if crucial bugfixes have been made between this commit and the current ### Create changelog from git commits !!! Note - Make sure that the master branch is uptodate! + Make sure that the master branch is up-to-date! ``` bash # Needs to be done before merging / pulling that branch. git log --oneline --no-decorate --no-merges master..new_release ``` -To keep the release-log short, best wrap the full git changelog into a collapsible details secction. +To keep the release-log short, best wrap the full git changelog into a collapsible details section. ```markdown

    @@ -312,6 +314,9 @@ Once the PR against master is merged (best right after merging): ### pypi +!!! Note + This process is now automated as part of Github Actions. + To create a pypi release, please run the following commands: Additional requirement: `wheel`, `twine` (for uploading), account on pypi with proper permissions. From 7f74ff53b12c5baff8c497227b8dd49ddcc4d499 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 31 Aug 2020 07:34:43 +0200 Subject: [PATCH 071/224] Move clock warning to installation pages --- docs/bot-usage.md | 3 +++ docs/docker.md | 3 +++ docs/index.md | 6 +----- docs/installation.md | 3 +++ 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 40ff3d82b..4a4496bbc 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -5,6 +5,9 @@ This page explains the different parameters of the bot and how to run it. !!! Note If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands. +!!! Warning "Up-to-date clock" + The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. + ## Bot commands ``` diff --git a/docs/docker.md b/docs/docker.md index 92478088a..b9508648b 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -12,6 +12,9 @@ Optionally, [docker-compose](https://docs.docker.com/compose/install/) should be Once you have Docker installed, simply prepare the config file (e.g. `config.json`) and run the image for `freqtrade` as explained below. +!!! Warning "Up-to-date clock" + The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. + ## Freqtrade with docker-compose Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) ready for usage. diff --git a/docs/index.md b/docs/index.md index adc661300..397c549aa 100644 --- a/docs/index.md +++ b/docs/index.md @@ -37,13 +37,9 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python ## Requirements -### Up to date clock - -The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. - ### Hardware requirements -To run this bot we recommend you a cloud instance with a minimum of: +To run this bot we recommend you a linux cloud instance with a minimum of: - 2GB RAM - 1GB disk space diff --git a/docs/installation.md b/docs/installation.md index c03be55d1..ec5e40965 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -18,6 +18,9 @@ Click each one for install guide: We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot), which is optional but recommended. +!!! Warning "Up-to-date clock" + The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. + ## Quick start Freqtrade provides the Linux/MacOS Easy Installation script to install all dependencies and help you configure the bot. From f83633ff4e9ded4689ccc70a0af9b7efe0ea26fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2020 06:38:24 +0000 Subject: [PATCH 072/224] Bump prompt-toolkit from 3.0.6 to 3.0.7 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.6 to 3.0.7. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.6...3.0.7) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index b6e2d329f..9e1dbf86f 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -32,4 +32,4 @@ flask-cors==3.0.8 colorama==0.4.3 # Building config files interactively questionary==1.5.2 -prompt-toolkit==3.0.6 +prompt-toolkit==3.0.7 From 4adf012ee68f200ab185a49a0b9c437e22ee2f8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2020 06:38:35 +0000 Subject: [PATCH 073/224] Bump flask-cors from 3.0.8 to 3.0.9 Bumps [flask-cors](https://github.com/corydolphin/flask-cors) from 3.0.8 to 3.0.9. - [Release notes](https://github.com/corydolphin/flask-cors/releases) - [Changelog](https://github.com/corydolphin/flask-cors/blob/master/CHANGELOG.md) - [Commits](https://github.com/corydolphin/flask-cors/compare/3.0.8...3.0.9) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index b6e2d329f..d543f206d 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -26,7 +26,7 @@ sdnotify==0.3.2 # Api server flask==1.1.2 flask-jwt-extended==3.24.1 -flask-cors==3.0.8 +flask-cors==3.0.9 # Support for colorized terminal output colorama==0.4.3 From 821af9be9e36ff5a5c4f632c309409034467c8a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2020 06:38:36 +0000 Subject: [PATCH 074/224] Bump mkdocs-material from 5.5.8 to 5.5.11 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.5.8 to 5.5.11. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/5.5.8...5.5.11) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 5226db750..c8f08d12a 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==5.5.8 +mkdocs-material==5.5.11 mdx_truly_sane_lists==1.2 From 55a49bfc5325364a0c5da7190e723b2a3c261089 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2020 06:38:38 +0000 Subject: [PATCH 075/224] Bump progressbar2 from 3.51.4 to 3.52.1 Bumps [progressbar2](https://github.com/WoLpH/python-progressbar) from 3.51.4 to 3.52.1. - [Release notes](https://github.com/WoLpH/python-progressbar/releases) - [Changelog](https://github.com/WoLpH/python-progressbar/blob/develop/CHANGES.rst) - [Commits](https://github.com/WoLpH/python-progressbar/compare/v3.51.4...v3.52.1) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index ce08f08e0..fbc679eaa 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -7,4 +7,4 @@ scikit-learn==0.23.1 scikit-optimize==0.7.4 filelock==3.0.12 joblib==0.16.0 -progressbar2==3.51.4 +progressbar2==3.52.1 From 8969ab4aa3026f3536980a34d6612bdb0a9dde38 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2020 06:38:55 +0000 Subject: [PATCH 076/224] Bump pytest-mock from 3.3.0 to 3.3.1 Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.3.0 to 3.3.1. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.3.0...v3.3.1) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1f5b68a73..44f0c7265 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ mypy==0.782 pytest==6.0.1 pytest-asyncio==0.14.0 pytest-cov==2.10.1 -pytest-mock==3.3.0 +pytest-mock==3.3.1 pytest-random-order==1.0.4 # Convert jupyter notebooks to markdown documents From c5b8993e9d061e7f839be9c40bc54769ec997991 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2020 06:38:56 +0000 Subject: [PATCH 077/224] Bump ccxt from 1.33.52 to 1.33.72 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.33.52 to 1.33.72. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.33.52...1.33.72) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index b6e2d329f..6f4ae45b3 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.33.52 +ccxt==1.33.72 SQLAlchemy==1.3.19 python-telegram-bot==12.8 arrow==0.16.0 From 24df8d6bf5c81c3bfc9127b0d3cd1c01b4a99780 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 31 Aug 2020 15:46:31 +0200 Subject: [PATCH 078/224] Sort imports --- tests/data/test_history.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index c89156f4c..2d6aed8f6 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -1,6 +1,5 @@ # pragma pylint: disable=missing-docstring, protected-access, C0103 -from freqtrade.data.history.hdf5datahandler import HDF5DataHandler import json import uuid from pathlib import Path @@ -15,6 +14,7 @@ from pandas.testing import assert_frame_equal from freqtrade.configuration import TimeRange from freqtrade.constants import AVAILABLE_DATAHANDLERS from freqtrade.data.converter import ohlcv_to_dataframe +from freqtrade.data.history.hdf5datahandler import HDF5DataHandler from freqtrade.data.history.history_utils import ( _download_pair_history, _download_trades_history, _load_cached_data_for_updating, convert_trades_to_ohlcv, get_timerange, From a4e3edbcc597fc05f9be1eec2aa827593e1d33e7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Sep 2020 07:10:48 +0200 Subject: [PATCH 079/224] Fix stoploss_last_update beein updated with date object in wrong timezone --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index eee60cc22..768f283ab 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -835,7 +835,7 @@ class FreqtradeBot: stop_price = trade.open_rate * (1 + stoploss) if self.create_stoploss_order(trade=trade, stop_price=stop_price): - trade.stoploss_last_update = datetime.now() + trade.stoploss_last_update = datetime.utcnow() return False # If stoploss order is canceled for some reason we add it From 3bc6cb36c60155a8de4e25cfd1879d1b260a4348 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Sep 2020 08:00:20 +0200 Subject: [PATCH 080/224] Remove deprectead volumepairlist options --- docs/deprecated.md | 2 +- freqtrade/pairlist/VolumePairList.py | 7 +------ tests/pairlist/test_pairlist.py | 13 ------------- 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/docs/deprecated.md b/docs/deprecated.md index 44f0b686a..312f2c74f 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -32,4 +32,4 @@ The old section of configuration parameters (`"pairlist"`) has been deprecated i ### deprecation of bidVolume and askVolume from volume-pairlist -Since only quoteVolume can be compared between assets, the other options (bidVolume, askVolume) have been deprecated in 2020.4. +Since only quoteVolume can be compared between assets, the other options (bidVolume, askVolume) have been deprecated in 2020.4, and have been removed in 2020.9. diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 35dce93eb..44e5c52d7 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -14,7 +14,7 @@ from freqtrade.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) -SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume'] +SORT_VALUES = ['quoteVolume'] class VolumePairList(IPairList): @@ -45,11 +45,6 @@ class VolumePairList(IPairList): raise OperationalException( f'key {self._sort_key} not in {SORT_VALUES}') - if self._sort_key != 'quoteVolume': - logger.warning( - "DEPRECATED: using any key other than quoteVolume for VolumePairList is deprecated." - ) - @property def needstickers(self) -> bool: """ diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 9217abc46..a5e479912 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -231,9 +231,6 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # VolumePairList only ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']), - # Different sorting depending on quote or bid volume - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}], - "BTC", ['HOT/BTC', 'FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']), ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']), # No pair for ETH, VolumePairList @@ -263,10 +260,6 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PrecisionFilter"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']), - # Precisionfilter bid - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}, - {"method": "PrecisionFilter"}], - "BTC", ['FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']), # PriceFilter and VolumePairList ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PriceFilter", "low_price_ratio": 0.03}], @@ -293,9 +286,6 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "StaticPairList"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), # Static Pairlist before VolumePairList - sorting changes - ([{"method": "StaticPairList"}, - {"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}], - "BTC", ['HOT/BTC', 'TKN/BTC', 'ETH/BTC']), # SpreadFilter ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}], @@ -344,9 +334,6 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "SpreadFilter", "max_spread_ratio": 0.005}], "BTC", 'filter_at_the_beginning'), # OperationalException expected # Static Pairlist after VolumePairList, on a non-first position - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}, - {"method": "StaticPairList"}], - "BTC", 'static_in_the_middle'), ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, {"method": "PriceFilter", "low_price_ratio": 0.02}], "USDT", ['ETH/USDT', 'NANO/USDT']), From d44418282935c97eb17181fa4e564bc2abf10d61 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Sep 2020 10:31:11 +0200 Subject: [PATCH 081/224] Reinstate wrongly removed pairlist test --- tests/pairlist/test_pairlist.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index a5e479912..1f05bef1e 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -334,6 +334,9 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "SpreadFilter", "max_spread_ratio": 0.005}], "BTC", 'filter_at_the_beginning'), # OperationalException expected # Static Pairlist after VolumePairList, on a non-first position + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "StaticPairList"}], + "BTC", 'static_in_the_middle'), ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, {"method": "PriceFilter", "low_price_ratio": 0.02}], "USDT", ['ETH/USDT', 'NANO/USDT']), From dff0ac276803e480c03ba6fb33620b7179307824 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Sep 2020 19:16:11 +0200 Subject: [PATCH 082/224] Remove trailing_stop from default config example - it'll be misleading --- config.json.example | 1 - config_binance.json.example | 1 - config_kraken.json.example | 1 - 3 files changed, 3 deletions(-) diff --git a/config.json.example b/config.json.example index 77a147d0c..ab517b77c 100644 --- a/config.json.example +++ b/config.json.example @@ -7,7 +7,6 @@ "timeframe": "5m", "dry_run": false, "cancel_open_orders_on_exit": false, - "trailing_stop": false, "unfilledtimeout": { "buy": 10, "sell": 30 diff --git a/config_binance.json.example b/config_binance.json.example index 82943749d..f3f8eb659 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -7,7 +7,6 @@ "timeframe": "5m", "dry_run": true, "cancel_open_orders_on_exit": false, - "trailing_stop": false, "unfilledtimeout": { "buy": 10, "sell": 30 diff --git a/config_kraken.json.example b/config_kraken.json.example index fb983a4a3..fd0b2b95d 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -7,7 +7,6 @@ "timeframe": "5m", "dry_run": true, "cancel_open_orders_on_exit": false, - "trailing_stop": false, "unfilledtimeout": { "buy": 10, "sell": 30 From f54fecaebaaa009dd4a5da2c83dcb85516be8a6a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Sep 2020 19:58:26 +0200 Subject: [PATCH 083/224] Expose helpermethods thorugh freqtrade.strategy --- freqtrade/strategy/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 40a4a0bea..91ea0e075 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -1 +1,4 @@ -from freqtrade.strategy.interface import IStrategy # noqa: F401 +# flake8: noqa: F401 +from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_prev_date, + timeframe_to_seconds, timeframe_to_next_date, timeframe_to_msecs) +from freqtrade.strategy.interface import IStrategy From e268bd192e85868cb4da9e74dceb652d249ffbe6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Sep 2020 19:59:04 +0200 Subject: [PATCH 084/224] Fix informative sample documentation --- docs/strategy-customization.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index be08faa2d..4362c251f 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -483,6 +483,10 @@ if self.dp: ### Complete Data-provider sample ```python +from freqtrade.strategy import IStrategy, timeframe_to_minutes +from pandas import DataFrame +import pandas as pd + class SampleStrategy(IStrategy): # strategy init stuff... @@ -518,9 +522,15 @@ class SampleStrategy(IStrategy): # Assuming inf_tf = '1d' - then the columns will now be: # date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d + # Shift date by 1 Frequency unit + # This is necessary since the data is always the "open date" + # and a 15m candle starting at 12:15 should not know the close of the 1h candle from 12:00 to 13:00 + minutes = timeframe_to_minutes(inf_tf) + informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes, 'm') + # Combine the 2 dataframes # all indicators on the informative sample MUST be calculated before this point - dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_{inf_tf}', how='left') + dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_merge_{inf_tf}', how='left') # FFill to have the 1d value available in every row throughout the day. # Without this, comparisons would only work once per day. dataframe = dataframe.ffill() From 79ea8cf7719879e942d4c49dddd63cafd2d38cfe Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Sep 2020 20:02:41 +0200 Subject: [PATCH 085/224] Improve wording --- docs/strategy-customization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 4362c251f..e2548e510 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -522,7 +522,7 @@ class SampleStrategy(IStrategy): # Assuming inf_tf = '1d' - then the columns will now be: # date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d - # Shift date by 1 Frequency unit + # Shift date by 1 candle # This is necessary since the data is always the "open date" # and a 15m candle starting at 12:15 should not know the close of the 1h candle from 12:00 to 13:00 minutes = timeframe_to_minutes(inf_tf) From 295ecaa9b20e688a4735a7a9fe1cd9bbbea2f7d0 Mon Sep 17 00:00:00 2001 From: silvavn <37382997+silvavn@users.noreply.github.com> Date: Wed, 2 Sep 2020 16:58:54 -0600 Subject: [PATCH 086/224] Updating Edge Positioning Doc. Integrated MathJax Included worked out examples Changed Language to achieve a middle ground. Minor formatting improvements --- docs/edge.md | 204 +++++++++++++++++++++++-------------- docs/javascripts/config.js | 12 +++ mkdocs.yml | 8 +- 3 files changed, 149 insertions(+), 75 deletions(-) create mode 100644 docs/javascripts/config.js diff --git a/docs/edge.md b/docs/edge.md index dcb559f96..182c47651 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -1,92 +1,141 @@ # Edge positioning -This page explains how to use Edge Positioning module in your bot in order to enter into a trade only if the trade has a reasonable win rate and risk reward ratio, and consequently adjust your position size and stoploss. +The `Edge Positioning` module uses probability to calculate your win rate and risk reward ration. It will use these statistics to control your strategy trade entry points, position side and, stoploss. !!! Warning - Edge positioning is not compatible with dynamic (volume-based) whitelist. + `Edge positioning` is not compatible with dynamic (volume-based) whitelist. !!! Note - Edge does not consider anything other than *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file. - Therefore, it is important to understand that Edge can improve the performance of some trading strategies but *decrease* the performance of others. + `Edge Positioning` only considers *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file. + `Edge Positioning` improves the performance of some trading strategies and *decreases* the performance of others. ## Introduction -Trading is all about probability. No one can claim that he has a strategy working all the time. You have to assume that sometimes you lose. +Trading strategies are not perfect. They are frameworks that are susceptible to the market and its indicators. Because the market is not at all predictable, sometimes a strategy will win and sometimes the same strategy will lose. -But it doesn't mean there is no rule, it only means rules should work "most of the time". Let's play a game: we toss a coin, heads: I give you 10$, tails: you give me 10$. Is it an interesting game? No, it's quite boring, isn't it? +To obtain an edge in the market, a strategy has to make more money than it loses. Marking money in trading is not only about *how often* the strategy makes or loses money. -But let's say the probability that we have heads is 80% (because our coin has the displaced distribution of mass or other defect), and the probability that we have tails is 20%. Now it is becoming interesting... +!!! tip "It doesn't matter how often, but how much!" + A bad strategy might make 1 penny in *ten* transactions but lose 1 dollar in *one* transaction. If one only checks the number of winning trades, it would be misleading to think that the strategy is actually making a profit. -That means 10$ X 80% versus 10$ X 20%. 8$ versus 2$. That means over time you will win 8$ risking only 2$ on each toss of coin. +The Edge Positioning module seeks to improve a strategy's winning probability and the money that the strategy will make *on the long run*. -Let's complicate it more: you win 80% of the time but only 2$, I win 20% of the time but 8$. The calculation is: 80% X 2$ versus 20% X 8$. It is becoming boring again because overtime you win $1.6$ (80% X 2$) and me $1.6 (20% X 8$) too. +We raise the following question[^1]: -The question is: How do you calculate that? How do you know if you wanna play? +!!! Question "Which trade is a better option?" + a) A trade with 80% of chance of losing $100 and 20% chance of winning $200
    + b) A trade with 100% of chance of losing $30 -The answer comes to two factors: +??? Info "Answer" + The expected value of *a)* is smaller than the expected value of *b)*.
    + Hence, *b*) represents a smaller loss in the long run.
    + However, the answer is: *it depends* -- Win Rate -- Risk Reward Ratio +Another way to look at it is to ask a similar question: -### Win Rate +!!! Question "Which trade is a better option?" + a) A trade with 80% of chance of winning 100 and 20% chance of losing $200
    + b) A trade with 100% of chance of winning $30 -Win Rate (*W*) is is the mean over some amount of trades (*N*) what is the percentage of winning trades to total number of trades (note that we don't consider how much you gained but only if you won or not). +Edge positioning tries to answer the hard questions about risk/reward and position size automatically, seeking to minimizes the chances of losing of a given strategy. -``` -W = (Number of winning trades) / (Total number of trades) = (Number of winning trades) / N -``` +### Trading, winning and losing -Complementary Loss Rate (*L*) is defined as +Let's call $o$ the return of a single transaction $o$ where $o \in \mathbb{R}$. The collection $O = \{o_1, o_2, ..., o_N\}$ is the set of all returns of transactions made during a trading session. We say that $N$ is the cardinality of $O$, or, in lay terms, it is the number of transactions made in a trading session. -``` -L = (Number of losing trades) / (Total number of trades) = (Number of losing trades) / N -``` +!!! Example + In a session where a strategy made three transactions we can say that $O = \{3.5, -1, 15\}$. That means that $N = 3$ and $o_1 = 3.5$, $o_2 = -1$, $o = 15$. -or, which is the same, as +A winning trade is a trade where a strategy *made* money. Making money means that the strategy closed the position in a value that returned a profit, after all deducted fees. Formally, a winning trade will have a return $o_i > 0$. Similarly, a losing trade will have a return $o_j \leq 0$. With that, we can discover the set of all winning trades, $T_{win}$, as follows: -``` -L = 1 – W -``` +$$ T_{win} = \{ o \in O | o > 0 \} $$ + +Similarly, we can discover the set of losing trades $T_{lose}$ as follows: + +$$ T_{lose} = \{o \in O | o \leq 0\} $$ + +!!! Example + In a section where a strategy made three transactions $O = \{3.5, -1, 15, 0\}$:
    + $T_{win} = \{3.5, 15\}$
    + $T_{lose} = \{-1, 0\}$
    + +### Win Rate and Lose Rate + +The win rate $W$ is the proportion of winning trades with respect to all the trades made by a strategy. We use the following function to compute the win rate: + +$$W = \frac{\sum^{o \in T_{win}} o}{N}$$ + +Where $W$ is the win rate, $N$ is the number of trades and, $T_{win}$ is the set of all trades where the strategy made money. + +Similarly, we can compute the rate of losing trades: + +$$ + L = \frac{\sum^{o \in T_{lose}} o}{N} +$$ + +Where $L$ is the lose rate, $N$ is the amount of trades made and, $T_{lose}$ is the set of all trades where the strategy lost money. Note that the above formula is the same as calculating $L = 1 – W$ or $W = 1 – L$ ### Risk Reward Ratio -Risk Reward Ratio (*R*) is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose: +Risk Reward Ratio (*R*) is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose. Formally: -``` -R = Profit / Loss -``` +$$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ -Over time, on many trades, you can calculate your risk reward by dividing your average profit on winning trades by your average loss on losing trades: +??? Example "Worked example of $R$ calculation" + Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100.
    + Your potential profit is calculated as:
    + $\begin{aligned} + \text{potential_profit} &= (\text{potential_price} - \text{cost_per_unit}) * \frac{\text{investment}}{\text{cost_per_unit}} \\ + &= (15 - 10) * \frac{100}{15}\\ + &= 33.33 + \end{aligned}$
    + Since the price might go to $0, the $100 dolars invested could turn into 0. We can compute the Risk Reward Ratio as follows:
    + $\begin{aligned} + R &= \frac{\text{potential_profit}}{\text{potential_loss}}\\ + &= \frac{33.33}{100}\\ + &= 0.333... + \end{aligned}$
    + What it effectivelly means is that the strategy have the potential to make $0.33 for each $1 invested. -``` -Average profit = (Sum of profits) / (Number of winning trades) +On a long horizonte, that is, on many trades, we can calculate the risk reward by dividing the strategy' average profit on winning trades by the strategy' average loss on losing trades. We can calculate the average profit, $\mu_{win}$, as follows: -Average loss = (Sum of losses) / (Number of losing trades) +$$ \text{average_profit} = \mu_{win} = \frac{\text{sum_of_profits}}{\text{count_winning_trades}} = \frac{\sum^{o \in T_{win}} o}{|T_{win}|} $$ -R = (Average profit) / (Average loss) -``` +Similarly, we can calculate the average loss, $\mu_{lose}$, as follows: + +$$ \text{average_loss} = \mu_{lose} = \frac{\text{sum_of_losses}}{\text{count_losing_trades}} = \frac{\sum^{o \in T_{lose}} o}{|T_{lose}|} $$ + +Finally, we can calculate the Risk Reward ratio as follows: + +$$ R = \frac{\text{average_profit}}{\text{average_loss}} = \frac{\mu_{win}}{\mu_{lose}}\\ $$ + + +??? Example "Worked example of $R$ calculation using mean profit/loss" + Let's say the strategy that we are using makes an average win $\mu_{win} = 2.06$ and an average loss $\mu_{loss} = 4.11$.
    + We calculate the risk reward ratio as follows:
    + $R = \frac{\mu_{win}}{\mu_{loss}} = \frac{2.06}{4.11} = 0.5012...$ + ### Expectancy -At this point we can combine *W* and *R* to create an expectancy ratio. This is a simple process of multiplying the risk reward ratio by the percentage of winning trades and subtracting the percentage of losing trades, which is calculated as follows: +By combining the Win Rate $W$ and and the Risk Reward ratio $R$ to create an expectancy ratio $E$. A expectance ratio is the expected return of the investment made in a trade. We can compute the value of $E$ as follows: -``` -Expectancy Ratio = (Risk Reward Ratio X Win Rate) – Loss Rate = (R X W) – L -``` +$$E = R * W - L$$ -So lets say your Win rate is 28% and your Risk Reward Ratio is 5: +!!! Example "Calculating $E$" + Let's say that a strategy has a win rate $W = 0.28$ and a risk reward ratio $R = 5$. What this means is that the strategy is expected to make 5 times the investment around on 28% of the trades it makes. Working out the example:
    + $E = R * W - L = 5 * 0.28 - 0.72 = 0.68$ +
    -``` -Expectancy = (5 X 0.28) – 0.72 = 0.68 -``` +The expectancy worked out in the example above means that, on average, this strategy' trades will return 1.68 times the size of its losses. Said another way, the strategy makes $1.68 for every $1 it loses, on average. -Superficially, this means that on average you expect this strategy’s trades to return 1.68 times the size of your loses. Said another way, you can expect to win $1.68 for every $1 you lose. This is important for two reasons: First, it may seem obvious, but you know right away that you have a positive return. Second, you now have a number you can compare to other candidate systems to make decisions about which ones you employ. +You canThis is important for two reasons: First, it may seem obvious, but you know right away that you have a positive return. Second, you now have a number you can compare to other candidate systems to make decisions about which ones you employ. It is important to remember that any system with an expectancy greater than 0 is profitable using past data. The key is finding one that will be profitable in the future. You can also use this value to evaluate the effectiveness of modifications to this system. -**NOTICE:** It's important to keep in mind that Edge is testing your expectancy using historical data, there's no guarantee that you will have a similar edge in the future. It's still vital to do this testing in order to build confidence in your methodology, but be wary of "curve-fitting" your approach to the historical data as things are unlikely to play out the exact same way for future trades. +**NOTICE:** It's important to keep in mind that Edge is testing your expectancy using historical data, there's no guarantee that you will have a similar edge in the future. It's still vital to do this testing in order to build confidence in your methodology but be wary of "curve-fitting" your approach to the historical data as things are unlikely to play out the exact same way for future trades. ## How does it work? @@ -99,13 +148,13 @@ Edge combines dynamic stoploss, dynamic positions, and whitelist generation into | XZC/ETH | -0.03 | 0.52 |1.359670 | 0.228 | | XZC/ETH | -0.04 | 0.51 |1.234539 | 0.117 | -The goal here is to find the best stoploss for the strategy in order to have the maximum expectancy. In the above example stoploss at 3% leads to the maximum expectancy according to historical data. +The goal here is to find the best stoploss for the strategy in order to have the maximum expectancy. In the above example stoploss at $3% $leads to the maximum expectancy according to historical data. Edge module then forces stoploss value it evaluated to your strategy dynamically. ### Position size -Edge also dictates the stake amount for each trade to the bot according to the following factors: +Edge dictates the amount at stake for each trade to the bot according to the following factors: - Allowed capital at risk - Stoploss @@ -116,9 +165,9 @@ Allowed capital at risk is calculated as follows: Allowed capital at risk = (Capital available_percentage) X (Allowed risk per trade) ``` -Stoploss is calculated as described above against historical data. +Stoploss is calculated as described above with respect to historical data. -Your position size then will be: +The position size is calculated as follows: ``` Position size = (Allowed capital at risk) / Stoploss @@ -126,19 +175,23 @@ Position size = (Allowed capital at risk) / Stoploss Example: -Let's say the stake currency is ETH and you have 10 ETH on the exchange, your capital available percentage is 50% and you would allow 1% of risk for each trade. thus your available capital for trading is **10 x 0.5 = 5 ETH** and allowed capital at risk would be **5 x 0.01 = 0.05 ETH**. +Let's say the stake currency is **ETH** and there is $10$ **ETH** on the wallet. The capital available percentage is $50%$ and the allowed risk per trade is $1\%$. Thus, the available capital for trading is $10 * 0.5 = 5$ **ETH** and the allowed capital at risk would be $5 * 0.01 = 0.05$ **ETH**. -Let's assume Edge has calculated that for **XLM/ETH** market your stoploss should be at 2%. So your position size will be **0.05 / 0.02 = 2.5 ETH**. +- **Trade 1:** The strategy detects a new buy signal in the **XLM/ETH** market. `Edge Positioning` calculates a stoploss of $2\%$ and a position of $0.05 / 0.02 = 2.5$ **ETH**. The bot takes a position of $2.5$ **ETH** in the **XLM/ETH** market. -Bot takes a position of 2.5 ETH on XLM/ETH (call it trade 1). Up next, you receive another buy signal while trade 1 is still open. This time on **BTC/ETH** market. Edge calculated stoploss for this market at 4%. So your position size would be 0.05 / 0.04 = 1.25 ETH (call it trade 2). +- **Trade 2:** The strategy detects a buy signal on the **BTC/ETH** market while **Trade 1** is still open. `Edge Positioning` calculates the stoploss of $4\%$ on this market. Thus, **Trade 2** position size is $0.05 / 0.04 = 1.25$ **ETH**. -Note that available capital for trading didn’t change for trade 2 even if you had already trade 1. The available capital doesn’t mean the free amount on your wallet. +!!! Tip "Available Capital $\neq$ Available in wallet" + The available capital for trading didn't change in **Trade 2** even with **Trade 1** still open. The available capital **is not** the free amount in the wallet. -Now you have two trades open. The bot receives yet another buy signal for another market: **ADA/ETH**. This time the stoploss is calculated at 1%. So your position size is **0.05 / 0.01 = 5 ETH**. But there are already 3.75 ETH blocked in two previous trades. So the position size for this third trade would be **5 – 3.75 = 1.25 ETH**. +- **Trade 3:** The strategy detects a buy signal in the **ADA/ETH** market. `Edge Positioning` calculates a stoploss of $1\%$ and a position of $0.05 / 0.01 = 5$ **ETH**. Since **Trade 1** has $2.5$ **ETH** blocked and **Trade 2** has $1.25$ **ETH** blocked, there is only $5 - 1.25 - 2.5 = 1.25$ **ETH** available. Hence, the position size of **Trade 3** is $1.25$ **ETH**. -Available capital doesn’t change before a position is sold. Let’s assume that trade 1 receives a sell signal and it is sold with a profit of 1 ETH. Your total capital on exchange would be 11 ETH and the available capital for trading becomes 5.5 ETH. +!!! Tip "Available Capital Updates" + The available capital does not change before a position is sold. After a trade is closed the Available Capital goes up if the trade was profitable or goes down if the trade was a loss. -So the Bot receives another buy signal for trade 4 with a stoploss at 2% then your position size would be **0.055 / 0.02 = 2.75 ETH**. +- The strategy detects a sell signal in the **XLM/ETH** market. The bot exits **Trade 1** for a profit of $1$ **ETH**. The total capital in the wallet becomes $11$ **ETH** and the available capital for trading becomes $5.5$ **ETH**. + +- **Trade 4** The strategy detects a new buy signal int the **XLM/ETH** market. `Edge Positioning` calculates the stoploss of $2%$, and the position size of $0.055 / 0.02 = 2.75$ **ETH**. ## Configurations @@ -169,23 +222,23 @@ freqtrade edge An example of its output: -| pair | stoploss | win rate | risk reward ratio | required risk reward | expectancy | total number of trades | average duration (min) | -|:----------|-----------:|-----------:|--------------------:|-----------------------:|-------------:|-------------------------:|-------------------------:| -| AGI/BTC | -0.02 | 0.64 | 5.86 | 0.56 | 3.41 | 14 | 54 | -| NXS/BTC | -0.03 | 0.64 | 2.99 | 0.57 | 1.54 | 11 | 26 | -| LEND/BTC | -0.02 | 0.82 | 2.05 | 0.22 | 1.50 | 11 | 36 | -| VIA/BTC | -0.01 | 0.55 | 3.01 | 0.83 | 1.19 | 11 | 48 | -| MTH/BTC | -0.09 | 0.56 | 2.82 | 0.80 | 1.12 | 18 | 52 | -| ARDR/BTC | -0.04 | 0.42 | 3.14 | 1.40 | 0.73 | 12 | 42 | -| BCPT/BTC | -0.01 | 0.71 | 1.34 | 0.40 | 0.67 | 14 | 30 | -| WINGS/BTC | -0.02 | 0.56 | 1.97 | 0.80 | 0.65 | 27 | 42 | -| VIBE/BTC | -0.02 | 0.83 | 0.91 | 0.20 | 0.59 | 12 | 35 | -| MCO/BTC | -0.02 | 0.79 | 0.97 | 0.27 | 0.55 | 14 | 31 | -| GNT/BTC | -0.02 | 0.50 | 2.06 | 1.00 | 0.53 | 18 | 24 | -| HOT/BTC | -0.01 | 0.17 | 7.72 | 4.81 | 0.50 | 209 | 7 | -| SNM/BTC | -0.03 | 0.71 | 1.06 | 0.42 | 0.45 | 17 | 38 | -| APPC/BTC | -0.02 | 0.44 | 2.28 | 1.27 | 0.44 | 25 | 43 | -| NEBL/BTC | -0.03 | 0.63 | 1.29 | 0.58 | 0.44 | 19 | 59 | +| **pair** | **stoploss** | **win rate** | **risk reward ratio** | **required risk reward** | **expectancy** | **total number of trades** | **average duration (min)** | +|:----------|-----------:|-----------:|--------------------:|-----------------------:|-------------:|-----------------:|---------------:| +| **AGI/BTC** | -0.02 | 0.64 | 5.86 | 0.56 | 3.41 | 14 | 54 | +| **NXS/BTC** | -0.03 | 0.64 | 2.99 | 0.57 | 1.54 | 11 | 26 | +| **LEND/BTC** | -0.02 | 0.82 | 2.05 | 0.22 | 1.50 | 11 | 36 | +| **VIA/BTC** | -0.01 | 0.55 | 3.01 | 0.83 | 1.19 | 11 | 48 | +| **MTH/BTC** | -0.09 | 0.56 | 2.82 | 0.80 | 1.12 | 18 | 52 | +| **ARDR/BTC** | -0.04 | 0.42 | 3.14 | 1.40 | 0.73 | 12 | 42 | +| **BCPT/BTC** | -0.01 | 0.71 | 1.34 | 0.40 | 0.67 | 14 | 30 | +| **WINGS/BTC** | -0.02 | 0.56 | 1.97 | 0.80 | 0.65 | 27 | 42 | +| **VIBE/BTC** | -0.02 | 0.83 | 0.91 | 0.20 | 0.59 | 12 | 35 | +| **MCO/BTC** | -0.02 | 0.79 | 0.97 | 0.27 | 0.55 | 14 | 31 | +| **GNT/BTC** | -0.02 | 0.50 | 2.06 | 1.00 | 0.53 | 18 | 24 | +| **HOT/BTC** | -0.01 | 0.17 | 7.72 | 4.81 | 0.50 | 209 | 7 | +| **SNM/BTC** | -0.03 | 0.71 | 1.06 | 0.42 | 0.45 | 17 | 38 | +| **APPC/BTC** | -0.02 | 0.44 | 2.28 | 1.27 | 0.44 | 25 | 43 | +| **NEBL/BTC** | -0.03 | 0.63 | 1.29 | 0.58 | 0.44 | 19 | 59 | Edge produced the above table by comparing `calculate_since_number_of_days` to `minimum_expectancy` to find `min_trade_number` historical information based on the config file. The timerange Edge uses for its comparisons can be further limited by using the `--timerange` switch. @@ -218,3 +271,6 @@ The full timerange specification: * Use tickframes since 2018/01/31: `--timerange=20180131-` * Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301` * Use tickframes between POSIX timestamps 1527595200 1527618600: `--timerange=1527595200-1527618600` + + +[^1]: Question extracted from MIT Opencourseware S096 - Mathematics with applications in Finance: https://ocw.mit.edu/courses/mathematics/18-s096-topics-in-mathematics-with-applications-in-finance-fall-2013/ diff --git a/docs/javascripts/config.js b/docs/javascripts/config.js new file mode 100644 index 000000000..95d619efc --- /dev/null +++ b/docs/javascripts/config.js @@ -0,0 +1,12 @@ +window.MathJax = { + tex: { + inlineMath: [["\\(", "\\)"]], + displayMath: [["\\[", "\\]"]], + processEscapes: true, + processEnvironments: true + }, + options: { + ignoreHtmlClass: ".*|", + processHtmlClass: "arithmatex" + } +}; \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index ebd32b3c1..324dc46db 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -39,13 +39,19 @@ theme: accent: 'tear' extra_css: - 'stylesheets/ft.extra.css' +extra_javascript: + - javascripts/config.js + - https://polyfill.io/v3/polyfill.min.js?features=es6 + - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js markdown_extensions: - admonition + - footnotes - codehilite: guess_lang: false - toc: permalink: true - - pymdownx.arithmatex + - pymdownx.arithmatex: + generic: true - pymdownx.caret - pymdownx.critic - pymdownx.details From 47352e17215731b02748ee05aedfd08fd7fc9c71 Mon Sep 17 00:00:00 2001 From: silvavn <37382997+silvavn@users.noreply.github.com> Date: Wed, 2 Sep 2020 20:37:45 -0600 Subject: [PATCH 087/224] Address issue #2487 Breakdown insllation instructions Make installation instructions shorter Separate Windows from the remainder Use tabs for better navigation Minor language improvements --- docs/docker.md | 349 ++++++--------------------------- docs/docker_compose.md | 44 +++++ docs/installation.md | 146 ++++---------- docs/windows_installation.md | 49 +++++ docs/without_docker_compose.md | 201 +++++++++++++++++++ mkdocs.yml | 10 +- 6 files changed, 405 insertions(+), 394 deletions(-) create mode 100644 docs/docker_compose.md create mode 100644 docs/windows_installation.md create mode 100644 docs/without_docker_compose.md diff --git a/docs/docker.md b/docs/docker.md index 92478088a..83f2d06e3 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -8,7 +8,7 @@ Start by downloading and installing Docker CE for your platform: * [Windows](https://docs.docker.com/docker-for-windows/install/) * [Linux](https://docs.docker.com/install/) -Optionally, [docker-compose](https://docs.docker.com/compose/install/) should be installed and available to follow the [docker quick start guide](#docker-quick-start). +Optionally, [`docker-compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the [docker quick start guide](#docker-quick-start). Once you have Docker installed, simply prepare the config file (e.g. `config.json`) and run the image for `freqtrade` as explained below. @@ -17,325 +17,96 @@ Once you have Docker installed, simply prepare the config file (e.g. `config.jso Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) ready for usage. !!! Note - The following section assumes that docker and docker-compose is installed and available to the logged in user. - -!!! Note - All below comands use relative directories and will have to be executed from the directory containing the `docker-compose.yml` file. - -!!! Note "Docker on Raspberry" - If you're running freqtrade on a Raspberry PI, you must change the image from `freqtradeorg/freqtrade:master` to `freqtradeorg/freqtrade:master_pi` or `freqtradeorg/freqtrade:develop_pi`, otherwise the image will not work. + - The following section assumes that `docker` and `docker-compose` are installed and available to the logged in user. + - All below comands use relative directories and will have to be executed from the directory containing the `docker-compose.yml` file. + ### Docker quick start Create a new directory and place the [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) in this directory. -``` bash -mkdir ft_userdata -cd ft_userdata/ -# Download the docker-compose file from the repository -curl https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docker-compose.yml -o docker-compose.yml +=== "PC/MAC/Linux" + ``` bash + mkdir ft_userdata + cd ft_userdata/ + # Download the docker-compose file from the repository + curl https://raw.githubusercontent.com/freqtrade/freqtrade/master/docker-compose.yml -o docker-compose.yml -# Pull the freqtrade image -docker-compose pull + # Pull the freqtrade image + docker-compose pull -# Create user directory structure -docker-compose run --rm freqtrade create-userdir --userdir user_data + # Create user directory structure + docker-compose run --rm freqtrade create-userdir --userdir user_data -# Create configuration - Requires answering interactive questions -docker-compose run --rm freqtrade new-config --config user_data/config.json -``` + # Create configuration - Requires answering interactive questions + docker-compose run --rm freqtrade new-config --config user_data/config.json + ``` -The above snippet creates a new directory called "ft_userdata", downloads the latest compose file and pulls the freqtrade image. -The last 2 steps in the snippet create the directory with user-data, as well as (interactively) the default configuration based on your selections. +=== "RaspberryPi" + ``` bash + mkdir ft_userdata + cd ft_userdata/ + # Download the docker-compose file from the repository + curl https://raw.githubusercontent.com/freqtrade/freqtrade/master_pi/docker-compose.yml -o docker-compose.yml -!!! Note + # Pull the freqtrade image + docker-compose pull + + # Create user directory structure + docker-compose run --rm freqtrade create-userdir --userdir user_data + + # Create configuration - Requires answering interactive questions + docker-compose run --rm freqtrade new-config --config user_data/config.json + ``` + +The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. +The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections. + +!!! Question "How to edit the bot configuration?" You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. -#### Adding your strategy +#### Adding a custom strategy -The configuration is now available as `user_data/config.json`. -You should now copy your strategy to `user_data/strategies/` - and add the Strategy class name to the `docker-compose.yml` file, replacing `SampleStrategy`. If you wish to run the bot with the SampleStrategy, just leave it as it is. +1. The configuration is now available as `user_data/config.json` +2. Copy a custom strategy to the directory `user_data/strategies/` +3. add the Strategy' class name to the `docker-compose.yml` file -!!! Warning +The `SampleStrategy` is run by default. + +!!! Warning "`SampleStrategy` is just a demo!" The `SampleStrategy` is there for your reference and give you ideas for your own strategy. Please always backtest the strategy and use dry-run for some time before risking real money! Once this is done, you're ready to launch the bot in trading mode (Dry-run or Live-trading, depending on your answer to the corresponding question you made above). -``` bash -docker-compose up -d -``` +=== "Docker Compose" + ``` bash + docker-compose up -d + ``` #### Docker-compose logs -Logs will be written to `user_data/logs/freqtrade.log`. -Alternatively, you can check the latest logs using `docker-compose logs -f`. +Logs will be located at: `user_data/logs/freqtrade.log`. +You can check the latest log with the command `docker-compose logs -f`. #### Database -The database will be in the user_data directory as well, and will be called `user_data/tradesv3.sqlite`. +The database will be at: `user_data/tradesv3.sqlite` #### Updating freqtrade with docker-compose -To update freqtrade when using docker-compose is as simple as running the following 2 commands: +To update freqtrade when using `docker-compose` is as simple as running the following 2 commands: -``` bash -# Download the latest image -docker-compose pull -# Restart the image -docker-compose up -d -``` +=== "Docker Compose" + ``` bash + # Download the latest image + docker-compose pull + # Restart the image + docker-compose up -d + ``` This will first pull the latest image, and will then restart the container with the just pulled version. -!!! Note +!!! Warning "Check the Changelog" You should always check the changelog for breaking changes / manual interventions required and make sure the bot starts correctly after the update. -#### Going from here - -Advanced users may edit the docker-compose file further to include all possible options or arguments. - -All possible freqtrade arguments will be available by running `docker-compose run --rm freqtrade `. - -!!! Note "`docker-compose run --rm`" - Including `--rm` will clean up the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). - -##### Example: Download data with docker-compose - -Download backtesting data for 5 days for the pair ETH/BTC and 1h timeframe from Binance. The data will be stored in the directory `user_data/data/` on the host. - -``` bash -docker-compose run --rm freqtrade download-data --pairs ETH/BTC --exchange binance --days 5 -t 1h -``` - -Head over to the [Data Downloading Documentation](data-download.md) for more details on downloading data. - -##### Example: Backtest with docker-compose - -Run backtesting in docker-containers for SampleStrategy and specified timerange of historical data, on 5m timeframe: - -``` bash -docker-compose run --rm freqtrade backtesting --config user_data/config.json --strategy SampleStrategy --timerange 20190801-20191001 -i 5m -``` - -Head over to the [Backtesting Documentation](backtesting.md) to learn more. - -#### Additional dependencies with docker-compose - -If your strategy requires dependencies not included in the default image (like [technical](https://github.com/freqtrade/technical)) - it will be necessary to build the image on your host. -For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) for an example). - -You'll then also need to modify the `docker-compose.yml` file and uncomment the build step, as well as rename the image to avoid naming collisions. - -``` yaml - image: freqtrade_custom - build: - context: . - dockerfile: "./Dockerfile." -``` - -You can then run `docker-compose build` to build the docker image, and run it using the commands described above. - -## Freqtrade with docker without docker-compose - -!!! Warning - The below documentation is provided for completeness and assumes that you are somewhat familiar with running docker containers. If you're just starting out with docker, we recommend to follow the [Freqtrade with docker-compose](#freqtrade-with-docker-compose) instructions. - -### Download the official Freqtrade docker image - -Pull the image from docker hub. - -Branches / tags available can be checked out on [Dockerhub tags page](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/). - -```bash -docker pull freqtradeorg/freqtrade:develop -# Optionally tag the repository so the run-commands remain shorter -docker tag freqtradeorg/freqtrade:develop freqtrade -``` - -To update the image, simply run the above commands again and restart your running container. - -Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image). - -!!! Note "Docker image update frequency" - The official docker images with tags `master`, `develop` and `latest` are automatically rebuild once a week to keep the base image uptodate. - In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`. - -### Prepare the configuration files - -Even though you will use docker, you'll still need some files from the github repository. - -#### Clone the git repository - -Linux/Mac/Windows with WSL - -```bash -git clone https://github.com/freqtrade/freqtrade.git -``` - -Windows with docker - -```bash -git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git -``` - -#### Copy `config.json.example` to `config.json` - -```bash -cd freqtrade -cp -n config.json.example config.json -``` - -> To understand the configuration options, please refer to the [Bot Configuration](configuration.md) page. - -#### Create your database file - -Production - -```bash -touch tradesv3.sqlite -```` - -Dry-Run - -```bash -touch tradesv3.dryrun.sqlite -``` - -!!! Note - Make sure to use the path to this file when starting the bot in docker. - -### Build your own Docker image - -Best start by pulling the official docker image from dockerhub as explained [here](#download-the-official-docker-image) to speed up building. - -To add additional libraries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image. - -```bash -docker build -t freqtrade -f Dockerfile.technical . -``` - -If you are developing using Docker, use `Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies: - -```bash -docker build -f Dockerfile.develop -t freqtrade-dev . -``` - -!!! Note - For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see the "5. Run a restartable docker image" section) to keep it between updates. - -#### Verify the Docker image - -After the build process you can verify that the image was created with: - -```bash -docker images -``` - -The output should contain the freqtrade image. - -### Run the Docker image - -You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory): - -```bash -docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade -``` - -!!! Warning - In this example, the database will be created inside the docker instance and will be lost when you will refresh your image. - -#### Adjust timezone - -By default, the container will use UTC timezone. -Should you find this irritating please add the following to your docker commands: - -##### Linux - -``` bash --v /etc/timezone:/etc/timezone:ro - -# Complete command: -docker run --rm -v /etc/timezone:/etc/timezone:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade -``` - -##### MacOS - -There is known issue in OSX Docker versions after 17.09.1, whereby `/etc/localtime` cannot be shared causing Docker to not start. A work-around for this is to start with the following cmd. - -```bash -docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade -``` - -More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396). - -### Run a restartable docker image - -To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem). - -#### Move your config file and database - -The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden directory in your home directory. Feel free to use a different directory and replace the directory in the upcomming commands. - -```bash -mkdir ~/.freqtrade -mv config.json ~/.freqtrade -mv tradesv3.sqlite ~/.freqtrade -``` - -#### Run the docker image - -```bash -docker run -d \ - --name freqtrade \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/user_data/:/freqtrade/user_data \ - -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy -``` - -!!! Note - When using docker, it's best to specify `--db-url` explicitly to ensure that the database URL and the mounted database file match. - -!!! Note - All available bot command line parameters can be added to the end of the `docker run` command. - -!!! Note - You can define a [restart policy](https://docs.docker.com/config/containers/start-containers-automatically/) in docker. It can be useful in some cases to use the `--restart unless-stopped` flag (crash of freqtrade or reboot of your system). - -### Monitor your Docker instance - -You can use the following commands to monitor and manage your container: - -```bash -docker logs freqtrade -docker logs -f freqtrade -docker restart freqtrade -docker stop freqtrade -docker start freqtrade -``` - -For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/). - -!!! Note - You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container. - -### Backtest with docker - -The following assumes that the download/setup of the docker image have been completed successfully. -Also, backtest-data should be available at `~/.freqtrade/user_data/`. - -```bash -docker run -d \ - --name freqtrade \ - -v /etc/localtime:/etc/localtime:ro \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - -v ~/.freqtrade/user_data/:/freqtrade/user_data/ \ - freqtrade backtesting --strategy AwsomelyProfitableStrategy -``` - -Head over to the [Backtesting Documentation](backtesting.md) for more details. - -!!! Note - Additional bot command line parameters can be appended after the image name (`freqtrade` in the above example). diff --git a/docs/docker_compose.md b/docs/docker_compose.md new file mode 100644 index 000000000..302d3b358 --- /dev/null +++ b/docs/docker_compose.md @@ -0,0 +1,44 @@ +#### Editing the docker-compose file + +Advanced users may edit the docker-compose file further to include all possible options or arguments. + +All possible freqtrade arguments will be available by running `docker-compose run --rm freqtrade `. + +!!! Note "`docker-compose run --rm`" + Including `--rm` will clean up the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). + +##### Example: Download data with docker-compose + +Download backtesting data for 5 days for the pair ETH/BTC and 1h timeframe from Binance. The data will be stored in the directory `user_data/data/` on the host. + +``` bash +docker-compose run --rm freqtrade download-data --pairs ETH/BTC --exchange binance --days 5 -t 1h +``` + +Head over to the [Data Downloading Documentation](data-download.md) for more details on downloading data. + +##### Example: Backtest with docker-compose + +Run backtesting in docker-containers for SampleStrategy and specified timerange of historical data, on 5m timeframe: + +``` bash +docker-compose run --rm freqtrade backtesting --config user_data/config.json --strategy SampleStrategy --timerange 20190801-20191001 -i 5m +``` + +Head over to the [Backtesting Documentation](backtesting.md) to learn more. + +#### Additional dependencies with docker-compose + +If your strategy requires dependencies not included in the default image (like [technical](https://github.com/freqtrade/technical)) - it will be necessary to build the image on your host. +For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) for an example). + +You'll then also need to modify the `docker-compose.yml` file and uncomment the build step, as well as rename the image to avoid naming collisions. + +``` yaml + image: freqtrade_custom + build: + context: . + dockerfile: "./Dockerfile." +``` + +You can then run `docker-compose build` to build the docker image, and run it using the commands described above. \ No newline at end of file diff --git a/docs/installation.md b/docs/installation.md index c03be55d1..979679c9f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -79,6 +79,20 @@ This option will hard reset your branch (only if you are on either `master` or ` DEPRECATED - use `freqtrade new-config -c config.json` instead. +### MacOS installation error + +Newer versions of MacOS may have installation failed with errors like `error: command 'g++' failed with exit status 1`. + +This error will require explicit installation of the SDK Headers, which are not installed by default in this version of MacOS. +For MacOS 10.14, this can be accomplished with the below command. + +``` bash +open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg +``` + +If this file is inexistant, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details. + + ------ ## Custom Installation @@ -89,36 +103,44 @@ OS Specific steps are listed first, the [Common](#common) section below is neces !!! Note Python3.6 or higher and the corresponding pip are assumed to be available. -### Linux - Ubuntu 16.04 +=== "Ubuntu 16.04" + #### Install necessary dependencies -#### Install necessary dependencies + ```bash + sudo apt-get update + sudo apt-get install build-essential git + ``` -```bash -sudo apt-get update -sudo apt-get install build-essential git -``` +=== "RaspberryPi/Raspbian" + The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/) from at least September 2019. + This image comes with python3.7 preinstalled, making it easy to get freqtrade up and running. -### Raspberry Pi / Raspbian + Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied. -The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/) from at least September 2019. -This image comes with python3.7 preinstalled, making it easy to get freqtrade up and running. + ``` bash + sudo apt-get install python3-venv libatlas-base-dev + git clone https://github.com/freqtrade/freqtrade.git + cd freqtrade -Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied. + bash setup.sh -i + ``` -``` bash -sudo apt-get install python3-venv libatlas-base-dev -git clone https://github.com/freqtrade/freqtrade.git -cd freqtrade + !!! Note "Installation duration" + Depending on your internet speed and the Raspberry Pi version, installation can take multiple hours to complete. -bash setup.sh -i -``` + !!! Note + The above does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`. + We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine. -!!! Note "Installation duration" - Depending on your internet speed and the Raspberry Pi version, installation can take multiple hours to complete. +=== "Anaconda" + Freqtrade can also be installed using Anaconda (or Miniconda). -!!! Note - The above does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`. - We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine. + !!! Note + This requires the [ta-lib](#1-install-ta-lib) C-library to be installed first. See below. + + ``` bash + conda env create -f environment.yml + ``` ### Common @@ -169,11 +191,6 @@ Clone the git repository: ```bash git clone https://github.com/freqtrade/freqtrade.git cd freqtrade -``` - -Optionally checkout the master branch to get the latest stable release: - -```bash git checkout master ``` @@ -212,83 +229,6 @@ On Linux, as an optional post-installation task, you may wish to setup the bot t ------ -## Using Conda - -Freqtrade can also be installed using Anaconda (or Miniconda). - -``` bash -conda env create -f environment.yml -``` - -!!! Note - This requires the [ta-lib](#1-install-ta-lib) C-library to be installed first. - -## Windows - -We recommend that Windows users use [Docker](docker.md) as this will work much easier and smoother (also more secure). - -If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work. -If that is not available on your system, feel free to try the instructions below, which led to success for some. - -### Install freqtrade manually - -!!! Note - Make sure to use 64bit Windows and 64bit Python to avoid problems with backtesting or hyperopt due to the memory constraints 32bit applications have under Windows. - -!!! Hint - Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Conda section](#using-conda) in this document for more information. - -#### Clone the git repository - -```bash -git clone https://github.com/freqtrade/freqtrade.git -``` - -#### Install ta-lib - -Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows). - -As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial precompiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib‑0.4.18‑cp38‑cp38‑win_amd64.whl` (make sure to use the version matching your python version) - -```cmd ->cd \path\freqtrade-develop ->python -m venv .env ->.env\Scripts\activate.bat -REM optionally install ta-lib from wheel -REM >pip install TA_Lib‑0.4.18‑cp38‑cp38‑win_amd64.whl ->pip install -r requirements.txt ->pip install -e . ->freqtrade -``` - -> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222) - -#### Error during installation under Windows - -``` bash -error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools -``` - -Unfortunately, many packages requiring compilation don't provide a pre-build wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. - -The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first. - ---- - Now you have an environment ready, the next step is [Bot Configuration](configuration.md). -## Troubleshooting - -### MacOS installation error - -Newer versions of MacOS may have installation failed with errors like `error: command 'g++' failed with exit status 1`. - -This error will require explicit installation of the SDK Headers, which are not installed by default in this version of MacOS. -For MacOS 10.14, this can be accomplished with the below command. - -``` bash -open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg -``` - -If this file is inexistant, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details. diff --git a/docs/windows_installation.md b/docs/windows_installation.md new file mode 100644 index 000000000..1cdb3d613 --- /dev/null +++ b/docs/windows_installation.md @@ -0,0 +1,49 @@ +We **strongly** recommend that Windows users use [Docker](docker.md) as this will work much easier and smoother (also more secure). + +If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work. +Otherwise, try the instructions below. + +## Install freqtrade manually + +!!! Note + Make sure to use 64bit Windows and 64bit Python to avoid problems with backtesting or hyperopt due to the memory constraints 32bit applications have under Windows. + +!!! Hint + Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Conda section](#using-conda) in this document for more information. + +### 1. Clone the git repository + +```bash +git clone https://github.com/freqtrade/freqtrade.git +``` + +### 2. Install ta-lib + +Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows). + +As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial precompiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib‑0.4.18‑cp38‑cp38‑win_amd64.whl` (make sure to use the version matching your python version) + +```cmd +>cd \path\freqtrade-develop +>python -m venv .env +>.env\Scripts\activate.bat +REM optionally install ta-lib from wheel +REM >pip install TA_Lib‑0.4.18‑cp38‑cp38‑win_amd64.whl +>pip install -r requirements.txt +>pip install -e . +>freqtrade +``` + +> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222) + +### Error during installation on Windows + +``` bash +error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools +``` + +Unfortunately, many packages requiring compilation don't provide a pre-build wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. + +The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first. + +--- diff --git a/docs/without_docker_compose.md b/docs/without_docker_compose.md new file mode 100644 index 000000000..23994f38f --- /dev/null +++ b/docs/without_docker_compose.md @@ -0,0 +1,201 @@ +## Freqtrade with docker without docker-compose + +!!! Warning + The below documentation is provided for completeness and assumes that you are familiar with running docker containers. If you're just starting out with Docker, we recommend to follow the [Quickstart](docker.md) instructions. + +### Download the official Freqtrade docker image + +Pull the image from docker hub. + +Branches / tags available can be checked out on [Dockerhub tags page](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/). + +```bash +docker pull freqtradeorg/freqtrade:master +# Optionally tag the repository so the run-commands remain shorter +docker tag freqtradeorg/freqtrade:master freqtrade +``` + +To update the image, simply run the above commands again and restart your running container. + +Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image). + +!!! Note "Docker image update frequency" + The official docker images with tags `master`, `develop` and `latest` are automatically rebuild once a week to keep the base image uptodate. + In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`. + +### Prepare the configuration files + +Even though you will use docker, you'll still need some files from the github repository. + +#### Clone the git repository + +Linux/Mac/Windows with WSL + +```bash +git clone https://github.com/freqtrade/freqtrade.git +``` + +Windows with docker + +```bash +git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git +``` + +#### Copy `config.json.example` to `config.json` + +```bash +cd freqtrade +cp -n config.json.example config.json +``` + +> To understand the configuration options, please refer to the [Bot Configuration](configuration.md) page. + +#### Create your database file + +=== "Dry-Run" + ``` bash + touch tradesv3.dryrun.sqlite + ``` + +=== "Production" + ``` bash + touch tradesv3.sqlite + ``` + + +!!! Warning Database File Path + Make sure to use the path to the correct database file when starting the bot in Docker. + +### Build your own Docker image + +Best start by pulling the official docker image from dockerhub as explained [here](#download-the-official-docker-image) to speed up building. + +To add additional libraries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image. + +```bash +docker build -t freqtrade -f Dockerfile.technical . +``` + +If you are developing using Docker, use `Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies: + +```bash +docker build -f Dockerfile.develop -t freqtrade-dev . +``` + +!!! Warning Include your config file manually + For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see [5. Run a restartable docker image](#run-a-restartable-docker-image)") to keep it between updates. + +#### Verify the Docker image + +After the build process you can verify that the image was created with: + +```bash +docker images +``` + +The output should contain the freqtrade image. + +### Run the Docker image + +You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory): + +```bash +docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade +``` + +!!! Warning + In this example, the database will be created inside the docker instance and will be lost when you refresh your image. + +#### Adjust timezone + +By default, the container will use UTC timezone. +If you would like to change the timezone use the following commands: + +=== "Linux" + ``` bash + -v /etc/timezone:/etc/timezone:ro + + # Complete command: + docker run --rm -v /etc/timezone:/etc/timezone:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade + ``` + +=== "MacOS" + ```bash + docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade + ``` + +!!! Note MacOS Issues + The OSX Docker versions after 17.09.1 have a known issue whereby `/etc/localtime` cannot be shared causing Docker to not start.
    + A work-around for this is to start with the MacOS command above + More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396). + +### Run a restartable docker image + +To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem). + +#### 1. Move your config file and database + +The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden directory in your home directory. Feel free to use a different directory and replace the directory in the upcomming commands. + +```bash +mkdir ~/.freqtrade +mv config.json ~/.freqtrade +mv tradesv3.sqlite ~/.freqtrade +``` + +#### 2. Run the docker image + +```bash +docker run -d \ + --name freqtrade \ + -v ~/.freqtrade/config.json:/freqtrade/config.json \ + -v ~/.freqtrade/user_data/:/freqtrade/user_data \ + -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ + freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy +``` + +!!! Note + When using docker, it's best to specify `--db-url` explicitly to ensure that the database URL and the mounted database file match. + +!!! Note + All available bot command line parameters can be added to the end of the `docker run` command. + +!!! Note + You can define a [restart policy](https://docs.docker.com/config/containers/start-containers-automatically/) in docker. It can be useful in some cases to use the `--restart unless-stopped` flag (crash of freqtrade or reboot of your system). + +### Monitor your Docker instance + +You can use the following commands to monitor and manage your container: + +```bash +docker logs freqtrade +docker logs -f freqtrade +docker restart freqtrade +docker stop freqtrade +docker start freqtrade +``` + +For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/). + +!!! Note + You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container. + +### Backtest with docker + +The following assumes that the download/setup of the docker image have been completed successfully. +Also, backtest-data should be available at `~/.freqtrade/user_data/`. + +```bash +docker run -d \ + --name freqtrade \ + -v /etc/localtime:/etc/localtime:ro \ + -v ~/.freqtrade/config.json:/freqtrade/config.json \ + -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ + -v ~/.freqtrade/user_data/:/freqtrade/user_data/ \ + freqtrade backtesting --strategy AwsomelyProfitableStrategy +``` + +Head over to the [Backtesting Documentation](backtesting.md) for more details. + +!!! Note + Additional bot command line parameters can be appended after the image name (`freqtrade` in the above example). diff --git a/mkdocs.yml b/mkdocs.yml index 324dc46db..2750ed3a5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,8 +1,13 @@ site_name: Freqtrade nav: - Home: index.md - - Installation Docker: docker.md - - Installation: installation.md + - Installation Docker: + - Quickstart: docker.md + - Freqtrade with Docker Compose (Advanced): docker_compose.md + - Freqtrade without docker-compose: without_docker_compose.md + - Installation: + - Linux/MacOS/Raspberry: installation.md + - Windows: windows_installation.md - Freqtrade Basics: bot-basics.md - Configuration: configuration.md - Strategy Customization: strategy-customization.md @@ -59,6 +64,7 @@ markdown_extensions: - pymdownx.magiclink - pymdownx.mark - pymdownx.smartsymbols + - pymdownx.tabbed - pymdownx.superfences - pymdownx.tasklist: custom_checkbox: true From 5c5cf782f53597f27bf0c361f86268d1c07a8ebf Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 3 Sep 2020 19:29:48 +0200 Subject: [PATCH 088/224] Fix small bug with /daily if close_profit_abs is not yet filled --- freqtrade/rpc/rpc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 802c8372b..b89a95ee8 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -224,7 +224,8 @@ class RPC: Trade.close_date >= profitday, Trade.close_date < (profitday + timedelta(days=1)) ]).order_by(Trade.close_date).all() - curdayprofit = sum(trade.close_profit_abs for trade in trades) + curdayprofit = sum( + trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None) profit_days[profitday] = { 'amount': curdayprofit, 'trades': len(trades) From 27362046d474744da563d74bd370da70eb82cd3b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 3 Sep 2020 19:33:34 +0200 Subject: [PATCH 089/224] Add documentation section about running docs locally --- docs/developer.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/developer.md b/docs/developer.md index 8bee1fd8e..111c7a96f 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -10,6 +10,15 @@ Documentation is available at [https://freqtrade.io](https://www.freqtrade.io/) Special fields for the documentation (like Note boxes, ...) can be found [here](https://squidfunk.github.io/mkdocs-material/extensions/admonition/). +To test the documentation locally use the following commands. + +``` bash +pip install -r docs/requirements-docs.txt +mkdocs serve +``` + +This will spin up a local server (usually on port 8000) so you can see if everything looks as you'd like it to. + ## Developer setup To configure a development environment, best use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ". From ec9b51d60a49feda0ab4d8dbb477780ffe149bca Mon Sep 17 00:00:00 2001 From: Victor Silva <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 12:49:32 -0600 Subject: [PATCH 090/224] Update docs/edge.md Co-authored-by: Matthias --- docs/edge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index 182c47651..6d43b0ea9 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -13,7 +13,7 @@ The `Edge Positioning` module uses probability to calculate your win rate and ri Trading strategies are not perfect. They are frameworks that are susceptible to the market and its indicators. Because the market is not at all predictable, sometimes a strategy will win and sometimes the same strategy will lose. -To obtain an edge in the market, a strategy has to make more money than it loses. Marking money in trading is not only about *how often* the strategy makes or loses money. +To obtain an edge in the market, a strategy has to make more money than it loses. Making money in trading is not only about *how often* the strategy makes or loses money. !!! tip "It doesn't matter how often, but how much!" A bad strategy might make 1 penny in *ten* transactions but lose 1 dollar in *one* transaction. If one only checks the number of winning trades, it would be misleading to think that the strategy is actually making a profit. From 69349a9d8d047c762e567723637a68259a478bd2 Mon Sep 17 00:00:00 2001 From: Victor Silva <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 12:49:54 -0600 Subject: [PATCH 091/224] Update docs/edge.md Co-authored-by: Matthias --- docs/edge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index 6d43b0ea9..8fae683a5 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -82,7 +82,7 @@ Risk Reward Ratio (*R*) is a formula used to measure the expected gains of a giv $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ ??? Example "Worked example of $R$ calculation" - Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100.
    + Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin, it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100.
    Your potential profit is calculated as:
    $\begin{aligned} \text{potential_profit} &= (\text{potential_price} - \text{cost_per_unit}) * \frac{\text{investment}}{\text{cost_per_unit}} \\ From 70eaf971cd55c45c9e27c8420f23e5c41a875ecc Mon Sep 17 00:00:00 2001 From: Victor Silva <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 12:50:23 -0600 Subject: [PATCH 092/224] Update docs/edge.md Co-authored-by: Matthias --- docs/edge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index 8fae683a5..7634718ae 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -77,7 +77,7 @@ Where $L$ is the lose rate, $N$ is the amount of trades made and, $T_{lose}$ is ### Risk Reward Ratio -Risk Reward Ratio (*R*) is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose. Formally: +Risk Reward Ratio ($R$) is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose. Formally: $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ From 5f9c449d8eecd8a1af79c2a1e7a3f5b897ea5eb6 Mon Sep 17 00:00:00 2001 From: Victor Silva <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 12:53:33 -0600 Subject: [PATCH 093/224] Update docs/edge.md Co-authored-by: Matthias --- docs/edge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index 7634718ae..e6b27a340 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -97,7 +97,7 @@ $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ \end{aligned}$
    What it effectivelly means is that the strategy have the potential to make $0.33 for each $1 invested. -On a long horizonte, that is, on many trades, we can calculate the risk reward by dividing the strategy' average profit on winning trades by the strategy' average loss on losing trades. We can calculate the average profit, $\mu_{win}$, as follows: +On a long horizon, that is, on many trades, we can calculate the risk reward by dividing the strategy' average profit on winning trades by the strategy' average loss on losing trades. We can calculate the average profit, $\mu_{win}$, as follows: $$ \text{average_profit} = \mu_{win} = \frac{\text{sum_of_profits}}{\text{count_winning_trades}} = \frac{\sum^{o \in T_{win}} o}{|T_{win}|} $$ From 08e35461209d47ab0b57b838da55add33d8a5575 Mon Sep 17 00:00:00 2001 From: Victor Silva <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 12:55:07 -0600 Subject: [PATCH 094/224] Update docs/edge.md Co-authored-by: Matthias --- docs/edge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index e6b27a340..8c8b230c9 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -105,7 +105,7 @@ Similarly, we can calculate the average loss, $\mu_{lose}$, as follows: $$ \text{average_loss} = \mu_{lose} = \frac{\text{sum_of_losses}}{\text{count_losing_trades}} = \frac{\sum^{o \in T_{lose}} o}{|T_{lose}|} $$ -Finally, we can calculate the Risk Reward ratio as follows: +Finally, we can calculate the Risk Reward ratio, $R$, as follows: $$ R = \frac{\text{average_profit}}{\text{average_loss}} = \frac{\mu_{win}}{\mu_{lose}}\\ $$ From 1f13a8b91d28d5f25579b2010e9b4246bece4a90 Mon Sep 17 00:00:00 2001 From: Victor Silva <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 12:55:49 -0600 Subject: [PATCH 095/224] Update docs/edge.md Co-authored-by: Matthias --- docs/edge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index 8c8b230c9..a5df45901 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -129,7 +129,7 @@ $$E = R * W - L$$ The expectancy worked out in the example above means that, on average, this strategy' trades will return 1.68 times the size of its losses. Said another way, the strategy makes $1.68 for every $1 it loses, on average. -You canThis is important for two reasons: First, it may seem obvious, but you know right away that you have a positive return. Second, you now have a number you can compare to other candidate systems to make decisions about which ones you employ. +This is important for two reasons: First, it may seem obvious, but you know right away that you have a positive return. Second, you now have a number you can compare to other candidate systems to make decisions about which ones you employ. It is important to remember that any system with an expectancy greater than 0 is profitable using past data. The key is finding one that will be profitable in the future. From 93d1ad5ed9569072a51678e3a3b2634be41bc8fd Mon Sep 17 00:00:00 2001 From: Victor Silva <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 12:56:54 -0600 Subject: [PATCH 096/224] Update docs/edge.md Co-authored-by: Matthias --- docs/edge.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index a5df45901..f7870ac1a 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -135,7 +135,8 @@ It is important to remember that any system with an expectancy greater than 0 is You can also use this value to evaluate the effectiveness of modifications to this system. -**NOTICE:** It's important to keep in mind that Edge is testing your expectancy using historical data, there's no guarantee that you will have a similar edge in the future. It's still vital to do this testing in order to build confidence in your methodology but be wary of "curve-fitting" your approach to the historical data as things are unlikely to play out the exact same way for future trades. +!!! Note + It's important to keep in mind that Edge is testing your expectancy using historical data, there's no guarantee that you will have a similar edge in the future. It's still vital to do this testing in order to build confidence in your methodology but be wary of "curve-fitting" your approach to the historical data as things are unlikely to play out the exact same way for future trades. ## How does it work? From 47f0e69072975469acabe808304f1a9491086db2 Mon Sep 17 00:00:00 2001 From: Victor Silva <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 12:57:15 -0600 Subject: [PATCH 097/224] Update docs/edge.md Co-authored-by: Matthias --- docs/edge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index f7870ac1a..593137c8d 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -149,7 +149,7 @@ Edge combines dynamic stoploss, dynamic positions, and whitelist generation into | XZC/ETH | -0.03 | 0.52 |1.359670 | 0.228 | | XZC/ETH | -0.04 | 0.51 |1.234539 | 0.117 | -The goal here is to find the best stoploss for the strategy in order to have the maximum expectancy. In the above example stoploss at $3% $leads to the maximum expectancy according to historical data. +The goal here is to find the best stoploss for the strategy in order to have the maximum expectancy. In the above example stoploss at $3%$ leads to the maximum expectancy according to historical data. Edge module then forces stoploss value it evaluated to your strategy dynamically. From 714264701c5bcc338ebee9b1dd8286e3d7cb90cd Mon Sep 17 00:00:00 2001 From: silvavn <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 13:11:04 -0600 Subject: [PATCH 098/224] Fixes typos --- docs/without_docker_compose.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/without_docker_compose.md b/docs/without_docker_compose.md index 23994f38f..3fe335cf0 100644 --- a/docs/without_docker_compose.md +++ b/docs/without_docker_compose.md @@ -63,7 +63,7 @@ cp -n config.json.example config.json ``` -!!! Warning Database File Path +!!! Warning "Database File Path" Make sure to use the path to the correct database file when starting the bot in Docker. ### Build your own Docker image @@ -82,7 +82,7 @@ If you are developing using Docker, use `Dockerfile.develop` to build a dev Dock docker build -f Dockerfile.develop -t freqtrade-dev . ``` -!!! Warning Include your config file manually +!!! Warning "Include your config file manually" For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see [5. Run a restartable docker image](#run-a-restartable-docker-image)") to keep it between updates. #### Verify the Docker image @@ -124,7 +124,7 @@ If you would like to change the timezone use the following commands: docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade ``` -!!! Note MacOS Issues +!!! Note "MacOS Issues" The OSX Docker versions after 17.09.1 have a known issue whereby `/etc/localtime` cannot be shared causing Docker to not start.
    A work-around for this is to start with the MacOS command above More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396). From f6a8dda8e5600ca8a95108013cb1903240085bec Mon Sep 17 00:00:00 2001 From: silvavn <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 13:12:43 -0600 Subject: [PATCH 099/224] Reorganize structure - Quickstart moved out of installation - Installation now contains only advanced modes. - Joined quickstart with Docker --- mkdocs.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 2750ed3a5..5d936687f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,11 +1,9 @@ site_name: Freqtrade nav: - Home: index.md - - Installation Docker: - - Quickstart: docker.md - - Freqtrade with Docker Compose (Advanced): docker_compose.md - - Freqtrade without docker-compose: without_docker_compose.md + - Quickstart with Docker: docker.md - Installation: + - Freqtrade without docker-compose: without_docker_compose.md - Linux/MacOS/Raspberry: installation.md - Windows: windows_installation.md - Freqtrade Basics: bot-basics.md From 66505bd9bf2f281417044b5259db345b4621b6c8 Mon Sep 17 00:00:00 2001 From: silvavn <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 13:18:15 -0600 Subject: [PATCH 100/224] Fixes Raspberri Pi Image config --- docs/docker.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/docker.md b/docs/docker.md index 83f2d06e3..9c8cc1683 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -47,7 +47,7 @@ Create a new directory and place the [docker-compose file](https://github.com/fr mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository - curl https://raw.githubusercontent.com/freqtrade/freqtrade/master_pi/docker-compose.yml -o docker-compose.yml + curl https://raw.githubusercontent.com/freqtrade/freqtrade/master/docker-compose.yml -o docker-compose.yml # Pull the freqtrade image docker-compose pull @@ -59,6 +59,13 @@ Create a new directory and place the [docker-compose file](https://github.com/fr docker-compose run --rm freqtrade new-config --config user_data/config.json ``` + !!! Note "Change your docker Image" + You should change the docker image in your config file for your Raspeberry build to work properly. + ``` bash + image: freqtradeorg/freqtrade:master_pi + # image: freqtradeorg/freqtrade:develop_pi + ``` + The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections. From e6058b716b1be8711d54d9274b0509700fdda5dd Mon Sep 17 00:00:00 2001 From: silvavn <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 13:19:05 -0600 Subject: [PATCH 101/224] removes prolixity docker-compose --- docs/docker.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/docker.md b/docs/docker.md index 9c8cc1683..9f90502bf 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -104,13 +104,13 @@ The database will be at: `user_data/tradesv3.sqlite` To update freqtrade when using `docker-compose` is as simple as running the following 2 commands: -=== "Docker Compose" - ``` bash - # Download the latest image - docker-compose pull - # Restart the image - docker-compose up -d - ``` + +``` bash +# Download the latest image +docker-compose pull +# Restart the image +docker-compose up -d +``` This will first pull the latest image, and will then restart the container with the just pulled version. From 29fe2ffff74de8f1ee2f3ce1f53f1da4f6cbf960 Mon Sep 17 00:00:00 2001 From: silvavn <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 13:22:22 -0600 Subject: [PATCH 102/224] Added that the user can edit docker-compose.yml --- docs/docker.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/docker.md b/docs/docker.md index 9f90502bf..ae16fe922 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -60,7 +60,7 @@ Create a new directory and place the [docker-compose file](https://github.com/fr ``` !!! Note "Change your docker Image" - You should change the docker image in your config file for your Raspeberry build to work properly. + You should change the docker image in your config file for your Raspberry build to work properly. ``` bash image: freqtradeorg/freqtrade:master_pi # image: freqtradeorg/freqtrade:develop_pi @@ -72,6 +72,8 @@ The last 2 steps in the snippet create the directory with `user_data`, as well a !!! Question "How to edit the bot configuration?" You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. + You can also change the both Strategy and commands by editing the `docker-compose.yml` file. + #### Adding a custom strategy 1. The configuration is now available as `user_data/config.json` From 34b27d2f96808846e64430a9c19724519cfb5309 Mon Sep 17 00:00:00 2001 From: silvavn <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 13:32:07 -0600 Subject: [PATCH 103/224] Moving stuff around - Mac troubleshooting to the end - optional master checkout - Anaconda moved to the end --- docs/installation.md | 50 +++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 979679c9f..83dd2938c 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -38,7 +38,7 @@ This can be achieved with the following commands: ```bash git clone https://github.com/freqtrade/freqtrade.git cd freqtrade -git checkout master # Optional, see (1) +# git checkout master # Optional, see (1) ./setup.sh --install ``` @@ -79,18 +79,7 @@ This option will hard reset your branch (only if you are on either `master` or ` DEPRECATED - use `freqtrade new-config -c config.json` instead. -### MacOS installation error -Newer versions of MacOS may have installation failed with errors like `error: command 'g++' failed with exit status 1`. - -This error will require explicit installation of the SDK Headers, which are not installed by default in this version of MacOS. -For MacOS 10.14, this can be accomplished with the below command. - -``` bash -open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg -``` - -If this file is inexistant, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details. ------ @@ -132,16 +121,6 @@ OS Specific steps are listed first, the [Common](#common) section below is neces The above does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`. We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine. -=== "Anaconda" - Freqtrade can also be installed using Anaconda (or Miniconda). - - !!! Note - This requires the [ta-lib](#1-install-ta-lib) C-library to be installed first. See below. - - ``` bash - conda env create -f environment.yml - ``` - ### Common #### 1. Install TA-Lib @@ -229,6 +208,33 @@ On Linux, as an optional post-installation task, you may wish to setup the bot t ------ +### Anaconda + +Freqtrade can also be installed using Anaconda (or Miniconda). + +!!! Note + This requires the [ta-lib](#1-install-ta-lib) C-library to be installed first. See below. + +``` bash +conda env create -f environment.yml +``` + +----- + Now you have an environment ready, the next step is [Bot Configuration](configuration.md). +----- + +### MacOS installation error + +Newer versions of MacOS may have installation failed with errors like `error: command 'g++' failed with exit status 1`. + +This error will require explicit installation of the SDK Headers, which are not installed by default in this version of MacOS. +For MacOS 10.14, this can be accomplished with the below command. + +``` bash +open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg +``` + +If this file is inexistant, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details. \ No newline at end of file From 275d8534323129124a1a5ed369fe7a38576e7251 Mon Sep 17 00:00:00 2001 From: silvavn <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 13:38:46 -0600 Subject: [PATCH 104/224] Updated W, L Formulas --- docs/edge.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/edge.md b/docs/edge.md index 593137c8d..e147cc15e 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -26,7 +26,7 @@ We raise the following question[^1]: a) A trade with 80% of chance of losing $100 and 20% chance of winning $200
    b) A trade with 100% of chance of losing $30 -??? Info "Answer" +???+ Info "Answer" The expected value of *a)* is smaller than the expected value of *b)*.
    Hence, *b*) represents a smaller loss in the long run.
    However, the answer is: *it depends* @@ -63,14 +63,14 @@ $$ T_{lose} = \{o \in O | o \leq 0\} $$ The win rate $W$ is the proportion of winning trades with respect to all the trades made by a strategy. We use the following function to compute the win rate: -$$W = \frac{\sum^{o \in T_{win}} o}{N}$$ +$$W = \frac{|T_{win}|}{N}$$ Where $W$ is the win rate, $N$ is the number of trades and, $T_{win}$ is the set of all trades where the strategy made money. Similarly, we can compute the rate of losing trades: $$ - L = \frac{\sum^{o \in T_{lose}} o}{N} + L = \frac{|T_{lose}|}{N} $$ Where $L$ is the lose rate, $N$ is the amount of trades made and, $T_{lose}$ is the set of all trades where the strategy lost money. Note that the above formula is the same as calculating $L = 1 – W$ or $W = 1 – L$ @@ -81,7 +81,7 @@ Risk Reward Ratio ($R$) is a formula used to measure the expected gains of a giv $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ -??? Example "Worked example of $R$ calculation" +???+ Example "Worked example of $R$ calculation" Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin, it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100.
    Your potential profit is calculated as:
    $\begin{aligned} @@ -110,7 +110,7 @@ Finally, we can calculate the Risk Reward ratio, $R$, as follows: $$ R = \frac{\text{average_profit}}{\text{average_loss}} = \frac{\mu_{win}}{\mu_{lose}}\\ $$ -??? Example "Worked example of $R$ calculation using mean profit/loss" +???+ Example "Worked example of $R$ calculation using mean profit/loss" Let's say the strategy that we are using makes an average win $\mu_{win} = 2.06$ and an average loss $\mu_{loss} = 4.11$.
    We calculate the risk reward ratio as follows:
    $R = \frac{\mu_{win}}{\mu_{loss}} = \frac{2.06}{4.11} = 0.5012...$ From 32005b886a9cc5916e015cb1308ce3e78572db7b Mon Sep 17 00:00:00 2001 From: silvavn <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 13:39:38 -0600 Subject: [PATCH 105/224] small typo --- docs/edge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index e147cc15e..500c3c833 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -44,7 +44,7 @@ Edge positioning tries to answer the hard questions about risk/reward and positi Let's call $o$ the return of a single transaction $o$ where $o \in \mathbb{R}$. The collection $O = \{o_1, o_2, ..., o_N\}$ is the set of all returns of transactions made during a trading session. We say that $N$ is the cardinality of $O$, or, in lay terms, it is the number of transactions made in a trading session. !!! Example - In a session where a strategy made three transactions we can say that $O = \{3.5, -1, 15\}$. That means that $N = 3$ and $o_1 = 3.5$, $o_2 = -1$, $o = 15$. + In a session where a strategy made three transactions we can say that $O = \{3.5, -1, 15\}$. That means that $N = 3$ and $o_1 = 3.5$, $o_2 = -1$, $o_3 = 15$. A winning trade is a trade where a strategy *made* money. Making money means that the strategy closed the position in a value that returned a profit, after all deducted fees. Formally, a winning trade will have a return $o_i > 0$. Similarly, a losing trade will have a return $o_j \leq 0$. With that, we can discover the set of all winning trades, $T_{win}$, as follows: From 1406691945511db6c1de8f69b89f03fd1a09af75 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 4 Sep 2020 07:12:08 +0200 Subject: [PATCH 106/224] Rename files to have clearer paths --- docs/docker.md | 304 +++++++++++++++++++++------------ docs/docker_compose.md | 44 ----- docs/docker_quickstart.md | 162 ++++++++++++++++++ docs/installation.md | 13 +- docs/without_docker_compose.md | 201 ---------------------- mkdocs.yml | 6 +- 6 files changed, 364 insertions(+), 366 deletions(-) delete mode 100644 docs/docker_compose.md create mode 100644 docs/docker_quickstart.md delete mode 100644 docs/without_docker_compose.md diff --git a/docs/docker.md b/docs/docker.md index ae16fe922..3fe335cf0 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -1,121 +1,201 @@ -# Using Freqtrade with Docker +## Freqtrade with docker without docker-compose -## Install Docker +!!! Warning + The below documentation is provided for completeness and assumes that you are familiar with running docker containers. If you're just starting out with Docker, we recommend to follow the [Quickstart](docker.md) instructions. -Start by downloading and installing Docker CE for your platform: +### Download the official Freqtrade docker image -* [Mac](https://docs.docker.com/docker-for-mac/install/) -* [Windows](https://docs.docker.com/docker-for-windows/install/) -* [Linux](https://docs.docker.com/install/) +Pull the image from docker hub. -Optionally, [`docker-compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the [docker quick start guide](#docker-quick-start). +Branches / tags available can be checked out on [Dockerhub tags page](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/). -Once you have Docker installed, simply prepare the config file (e.g. `config.json`) and run the image for `freqtrade` as explained below. - -## Freqtrade with docker-compose - -Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) ready for usage. - -!!! Note - - The following section assumes that `docker` and `docker-compose` are installed and available to the logged in user. - - All below comands use relative directories and will have to be executed from the directory containing the `docker-compose.yml` file. - - -### Docker quick start - -Create a new directory and place the [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) in this directory. - -=== "PC/MAC/Linux" - ``` bash - mkdir ft_userdata - cd ft_userdata/ - # Download the docker-compose file from the repository - curl https://raw.githubusercontent.com/freqtrade/freqtrade/master/docker-compose.yml -o docker-compose.yml - - # Pull the freqtrade image - docker-compose pull - - # Create user directory structure - docker-compose run --rm freqtrade create-userdir --userdir user_data - - # Create configuration - Requires answering interactive questions - docker-compose run --rm freqtrade new-config --config user_data/config.json - ``` - -=== "RaspberryPi" - ``` bash - mkdir ft_userdata - cd ft_userdata/ - # Download the docker-compose file from the repository - curl https://raw.githubusercontent.com/freqtrade/freqtrade/master/docker-compose.yml -o docker-compose.yml - - # Pull the freqtrade image - docker-compose pull - - # Create user directory structure - docker-compose run --rm freqtrade create-userdir --userdir user_data - - # Create configuration - Requires answering interactive questions - docker-compose run --rm freqtrade new-config --config user_data/config.json - ``` - - !!! Note "Change your docker Image" - You should change the docker image in your config file for your Raspberry build to work properly. - ``` bash - image: freqtradeorg/freqtrade:master_pi - # image: freqtradeorg/freqtrade:develop_pi - ``` - -The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. -The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections. - -!!! Question "How to edit the bot configuration?" - You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. - - You can also change the both Strategy and commands by editing the `docker-compose.yml` file. - -#### Adding a custom strategy - -1. The configuration is now available as `user_data/config.json` -2. Copy a custom strategy to the directory `user_data/strategies/` -3. add the Strategy' class name to the `docker-compose.yml` file - -The `SampleStrategy` is run by default. - -!!! Warning "`SampleStrategy` is just a demo!" - The `SampleStrategy` is there for your reference and give you ideas for your own strategy. - Please always backtest the strategy and use dry-run for some time before risking real money! - -Once this is done, you're ready to launch the bot in trading mode (Dry-run or Live-trading, depending on your answer to the corresponding question you made above). - -=== "Docker Compose" - ``` bash - docker-compose up -d - ``` - -#### Docker-compose logs - -Logs will be located at: `user_data/logs/freqtrade.log`. -You can check the latest log with the command `docker-compose logs -f`. - -#### Database - -The database will be at: `user_data/tradesv3.sqlite` - -#### Updating freqtrade with docker-compose - -To update freqtrade when using `docker-compose` is as simple as running the following 2 commands: - - -``` bash -# Download the latest image -docker-compose pull -# Restart the image -docker-compose up -d +```bash +docker pull freqtradeorg/freqtrade:master +# Optionally tag the repository so the run-commands remain shorter +docker tag freqtradeorg/freqtrade:master freqtrade ``` -This will first pull the latest image, and will then restart the container with the just pulled version. +To update the image, simply run the above commands again and restart your running container. -!!! Warning "Check the Changelog" - You should always check the changelog for breaking changes / manual interventions required and make sure the bot starts correctly after the update. +Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image). +!!! Note "Docker image update frequency" + The official docker images with tags `master`, `develop` and `latest` are automatically rebuild once a week to keep the base image uptodate. + In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`. + +### Prepare the configuration files + +Even though you will use docker, you'll still need some files from the github repository. + +#### Clone the git repository + +Linux/Mac/Windows with WSL + +```bash +git clone https://github.com/freqtrade/freqtrade.git +``` + +Windows with docker + +```bash +git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git +``` + +#### Copy `config.json.example` to `config.json` + +```bash +cd freqtrade +cp -n config.json.example config.json +``` + +> To understand the configuration options, please refer to the [Bot Configuration](configuration.md) page. + +#### Create your database file + +=== "Dry-Run" + ``` bash + touch tradesv3.dryrun.sqlite + ``` + +=== "Production" + ``` bash + touch tradesv3.sqlite + ``` + + +!!! Warning "Database File Path" + Make sure to use the path to the correct database file when starting the bot in Docker. + +### Build your own Docker image + +Best start by pulling the official docker image from dockerhub as explained [here](#download-the-official-docker-image) to speed up building. + +To add additional libraries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image. + +```bash +docker build -t freqtrade -f Dockerfile.technical . +``` + +If you are developing using Docker, use `Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies: + +```bash +docker build -f Dockerfile.develop -t freqtrade-dev . +``` + +!!! Warning "Include your config file manually" + For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see [5. Run a restartable docker image](#run-a-restartable-docker-image)") to keep it between updates. + +#### Verify the Docker image + +After the build process you can verify that the image was created with: + +```bash +docker images +``` + +The output should contain the freqtrade image. + +### Run the Docker image + +You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory): + +```bash +docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade +``` + +!!! Warning + In this example, the database will be created inside the docker instance and will be lost when you refresh your image. + +#### Adjust timezone + +By default, the container will use UTC timezone. +If you would like to change the timezone use the following commands: + +=== "Linux" + ``` bash + -v /etc/timezone:/etc/timezone:ro + + # Complete command: + docker run --rm -v /etc/timezone:/etc/timezone:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade + ``` + +=== "MacOS" + ```bash + docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade + ``` + +!!! Note "MacOS Issues" + The OSX Docker versions after 17.09.1 have a known issue whereby `/etc/localtime` cannot be shared causing Docker to not start.
    + A work-around for this is to start with the MacOS command above + More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396). + +### Run a restartable docker image + +To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem). + +#### 1. Move your config file and database + +The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden directory in your home directory. Feel free to use a different directory and replace the directory in the upcomming commands. + +```bash +mkdir ~/.freqtrade +mv config.json ~/.freqtrade +mv tradesv3.sqlite ~/.freqtrade +``` + +#### 2. Run the docker image + +```bash +docker run -d \ + --name freqtrade \ + -v ~/.freqtrade/config.json:/freqtrade/config.json \ + -v ~/.freqtrade/user_data/:/freqtrade/user_data \ + -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ + freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy +``` + +!!! Note + When using docker, it's best to specify `--db-url` explicitly to ensure that the database URL and the mounted database file match. + +!!! Note + All available bot command line parameters can be added to the end of the `docker run` command. + +!!! Note + You can define a [restart policy](https://docs.docker.com/config/containers/start-containers-automatically/) in docker. It can be useful in some cases to use the `--restart unless-stopped` flag (crash of freqtrade or reboot of your system). + +### Monitor your Docker instance + +You can use the following commands to monitor and manage your container: + +```bash +docker logs freqtrade +docker logs -f freqtrade +docker restart freqtrade +docker stop freqtrade +docker start freqtrade +``` + +For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/). + +!!! Note + You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container. + +### Backtest with docker + +The following assumes that the download/setup of the docker image have been completed successfully. +Also, backtest-data should be available at `~/.freqtrade/user_data/`. + +```bash +docker run -d \ + --name freqtrade \ + -v /etc/localtime:/etc/localtime:ro \ + -v ~/.freqtrade/config.json:/freqtrade/config.json \ + -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ + -v ~/.freqtrade/user_data/:/freqtrade/user_data/ \ + freqtrade backtesting --strategy AwsomelyProfitableStrategy +``` + +Head over to the [Backtesting Documentation](backtesting.md) for more details. + +!!! Note + Additional bot command line parameters can be appended after the image name (`freqtrade` in the above example). diff --git a/docs/docker_compose.md b/docs/docker_compose.md deleted file mode 100644 index 302d3b358..000000000 --- a/docs/docker_compose.md +++ /dev/null @@ -1,44 +0,0 @@ -#### Editing the docker-compose file - -Advanced users may edit the docker-compose file further to include all possible options or arguments. - -All possible freqtrade arguments will be available by running `docker-compose run --rm freqtrade `. - -!!! Note "`docker-compose run --rm`" - Including `--rm` will clean up the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). - -##### Example: Download data with docker-compose - -Download backtesting data for 5 days for the pair ETH/BTC and 1h timeframe from Binance. The data will be stored in the directory `user_data/data/` on the host. - -``` bash -docker-compose run --rm freqtrade download-data --pairs ETH/BTC --exchange binance --days 5 -t 1h -``` - -Head over to the [Data Downloading Documentation](data-download.md) for more details on downloading data. - -##### Example: Backtest with docker-compose - -Run backtesting in docker-containers for SampleStrategy and specified timerange of historical data, on 5m timeframe: - -``` bash -docker-compose run --rm freqtrade backtesting --config user_data/config.json --strategy SampleStrategy --timerange 20190801-20191001 -i 5m -``` - -Head over to the [Backtesting Documentation](backtesting.md) to learn more. - -#### Additional dependencies with docker-compose - -If your strategy requires dependencies not included in the default image (like [technical](https://github.com/freqtrade/technical)) - it will be necessary to build the image on your host. -For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) for an example). - -You'll then also need to modify the `docker-compose.yml` file and uncomment the build step, as well as rename the image to avoid naming collisions. - -``` yaml - image: freqtrade_custom - build: - context: . - dockerfile: "./Dockerfile." -``` - -You can then run `docker-compose build` to build the docker image, and run it using the commands described above. \ No newline at end of file diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md new file mode 100644 index 000000000..c033e827b --- /dev/null +++ b/docs/docker_quickstart.md @@ -0,0 +1,162 @@ +# Using Freqtrade with Docker + +## Install Docker + +Start by downloading and installing Docker CE for your platform: + +* [Mac](https://docs.docker.com/docker-for-mac/install/) +* [Windows](https://docs.docker.com/docker-for-windows/install/) +* [Linux](https://docs.docker.com/install/) + +Optionally, [`docker-compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the [docker quick start guide](#docker-quick-start). + +Once you have Docker installed, simply prepare the config file (e.g. `config.json`) and run the image for `freqtrade` as explained below. + +## Freqtrade with docker-compose + +Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) ready for usage. + +!!! Note + - The following section assumes that `docker` and `docker-compose` are installed and available to the logged in user. + - All below commands use relative directories and will have to be executed from the directory containing the `docker-compose.yml` file. + +### Docker quick start + +Create a new directory and place the [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) in this directory. + +=== "PC/MAC/Linux" + ``` bash + mkdir ft_userdata + cd ft_userdata/ + # Download the docker-compose file from the repository + curl https://raw.githubusercontent.com/freqtrade/freqtrade/master/docker-compose.yml -o docker-compose.yml + + # Pull the freqtrade image + docker-compose pull + + # Create user directory structure + docker-compose run --rm freqtrade create-userdir --userdir user_data + + # Create configuration - Requires answering interactive questions + docker-compose run --rm freqtrade new-config --config user_data/config.json + ``` + +=== "RaspberryPi" + ``` bash + mkdir ft_userdata + cd ft_userdata/ + # Download the docker-compose file from the repository + curl https://raw.githubusercontent.com/freqtrade/freqtrade/master/docker-compose.yml -o docker-compose.yml + + # Pull the freqtrade image + docker-compose pull + + # Create user directory structure + docker-compose run --rm freqtrade create-userdir --userdir user_data + + # Create configuration - Requires answering interactive questions + docker-compose run --rm freqtrade new-config --config user_data/config.json + ``` + + !!! Note "Change your docker Image" + You have to change the docker image in the docker-compose file for your Raspberry build to work properly. + ``` yml + image: freqtradeorg/freqtrade:master_pi + # image: freqtradeorg/freqtrade:develop_pi + ``` + +The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. +The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections. + +!!! Question "How to edit the bot configuration?" + You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. + + You can also change the both Strategy and commands by editing the `docker-compose.yml` file. + +#### Adding a custom strategy + +1. The configuration is now available as `user_data/config.json` +2. Copy a custom strategy to the directory `user_data/strategies/` +3. add the Strategy' class name to the `docker-compose.yml` file + +The `SampleStrategy` is run by default. + +!!! Warning "`SampleStrategy` is just a demo!" + The `SampleStrategy` is there for your reference and give you ideas for your own strategy. + Please always backtest the strategy and use dry-run for some time before risking real money! + +Once this is done, you're ready to launch the bot in trading mode (Dry-run or Live-trading, depending on your answer to the corresponding question you made above). + +``` bash +docker-compose up -d +``` + +#### Docker-compose logs + +Logs will be located at: `user_data/logs/freqtrade.log`. +You can check the latest log with the command `docker-compose logs -f`. + +#### Database + +The database will be at: `user_data/tradesv3.sqlite` + +#### Updating freqtrade with docker-compose + +To update freqtrade when using `docker-compose` is as simple as running the following 2 commands: + +``` bash +# Download the latest image +docker-compose pull +# Restart the image +docker-compose up -d +``` + +This will first pull the latest image, and will then restart the container with the just pulled version. + +!!! Warning "Check the Changelog" + You should always check the changelog for breaking changes / manual interventions required and make sure the bot starts correctly after the update. + +### Editing the docker-compose file + +Advanced users may edit the docker-compose file further to include all possible options or arguments. + +All possible freqtrade arguments will be available by running `docker-compose run --rm freqtrade `. + +!!! Note "`docker-compose run --rm`" + Including `--rm` will clean up the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). + +#### Example: Download data with docker-compose + +Download backtesting data for 5 days for the pair ETH/BTC and 1h timeframe from Binance. The data will be stored in the directory `user_data/data/` on the host. + +``` bash +docker-compose run --rm freqtrade download-data --pairs ETH/BTC --exchange binance --days 5 -t 1h +``` + +Head over to the [Data Downloading Documentation](data-download.md) for more details on downloading data. + +#### Example: Backtest with docker-compose + +Run backtesting in docker-containers for SampleStrategy and specified timerange of historical data, on 5m timeframe: + +``` bash +docker-compose run --rm freqtrade backtesting --config user_data/config.json --strategy SampleStrategy --timerange 20190801-20191001 -i 5m +``` + +Head over to the [Backtesting Documentation](backtesting.md) to learn more. + +### Additional dependencies with docker-compose + +If your strategy requires dependencies not included in the default image (like [technical](https://github.com/freqtrade/technical)) - it will be necessary to build the image on your host. +For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) for an example). + +You'll then also need to modify the `docker-compose.yml` file and uncomment the build step, as well as rename the image to avoid naming collisions. + +``` yaml + image: freqtrade_custom + build: + context: . + dockerfile: "./Dockerfile." +``` + +You can then run `docker-compose build` to build the docker image, and run it using the commands described above. diff --git a/docs/installation.md b/docs/installation.md index 83dd2938c..baa4a64d7 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -220,11 +220,7 @@ conda env create -f environment.yml ``` ----- - -Now you have an environment ready, the next step is -[Bot Configuration](configuration.md). - ------ +## Troubleshooting ### MacOS installation error @@ -237,4 +233,9 @@ For MacOS 10.14, this can be accomplished with the below command. open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg ``` -If this file is inexistant, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details. \ No newline at end of file +If this file is inexistent, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details. + +----- + +Now you have an environment ready, the next step is +[Bot Configuration](configuration.md). diff --git a/docs/without_docker_compose.md b/docs/without_docker_compose.md deleted file mode 100644 index 3fe335cf0..000000000 --- a/docs/without_docker_compose.md +++ /dev/null @@ -1,201 +0,0 @@ -## Freqtrade with docker without docker-compose - -!!! Warning - The below documentation is provided for completeness and assumes that you are familiar with running docker containers. If you're just starting out with Docker, we recommend to follow the [Quickstart](docker.md) instructions. - -### Download the official Freqtrade docker image - -Pull the image from docker hub. - -Branches / tags available can be checked out on [Dockerhub tags page](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/). - -```bash -docker pull freqtradeorg/freqtrade:master -# Optionally tag the repository so the run-commands remain shorter -docker tag freqtradeorg/freqtrade:master freqtrade -``` - -To update the image, simply run the above commands again and restart your running container. - -Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image). - -!!! Note "Docker image update frequency" - The official docker images with tags `master`, `develop` and `latest` are automatically rebuild once a week to keep the base image uptodate. - In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`. - -### Prepare the configuration files - -Even though you will use docker, you'll still need some files from the github repository. - -#### Clone the git repository - -Linux/Mac/Windows with WSL - -```bash -git clone https://github.com/freqtrade/freqtrade.git -``` - -Windows with docker - -```bash -git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git -``` - -#### Copy `config.json.example` to `config.json` - -```bash -cd freqtrade -cp -n config.json.example config.json -``` - -> To understand the configuration options, please refer to the [Bot Configuration](configuration.md) page. - -#### Create your database file - -=== "Dry-Run" - ``` bash - touch tradesv3.dryrun.sqlite - ``` - -=== "Production" - ``` bash - touch tradesv3.sqlite - ``` - - -!!! Warning "Database File Path" - Make sure to use the path to the correct database file when starting the bot in Docker. - -### Build your own Docker image - -Best start by pulling the official docker image from dockerhub as explained [here](#download-the-official-docker-image) to speed up building. - -To add additional libraries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image. - -```bash -docker build -t freqtrade -f Dockerfile.technical . -``` - -If you are developing using Docker, use `Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies: - -```bash -docker build -f Dockerfile.develop -t freqtrade-dev . -``` - -!!! Warning "Include your config file manually" - For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see [5. Run a restartable docker image](#run-a-restartable-docker-image)") to keep it between updates. - -#### Verify the Docker image - -After the build process you can verify that the image was created with: - -```bash -docker images -``` - -The output should contain the freqtrade image. - -### Run the Docker image - -You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory): - -```bash -docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade -``` - -!!! Warning - In this example, the database will be created inside the docker instance and will be lost when you refresh your image. - -#### Adjust timezone - -By default, the container will use UTC timezone. -If you would like to change the timezone use the following commands: - -=== "Linux" - ``` bash - -v /etc/timezone:/etc/timezone:ro - - # Complete command: - docker run --rm -v /etc/timezone:/etc/timezone:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade - ``` - -=== "MacOS" - ```bash - docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade - ``` - -!!! Note "MacOS Issues" - The OSX Docker versions after 17.09.1 have a known issue whereby `/etc/localtime` cannot be shared causing Docker to not start.
    - A work-around for this is to start with the MacOS command above - More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396). - -### Run a restartable docker image - -To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem). - -#### 1. Move your config file and database - -The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden directory in your home directory. Feel free to use a different directory and replace the directory in the upcomming commands. - -```bash -mkdir ~/.freqtrade -mv config.json ~/.freqtrade -mv tradesv3.sqlite ~/.freqtrade -``` - -#### 2. Run the docker image - -```bash -docker run -d \ - --name freqtrade \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/user_data/:/freqtrade/user_data \ - -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy -``` - -!!! Note - When using docker, it's best to specify `--db-url` explicitly to ensure that the database URL and the mounted database file match. - -!!! Note - All available bot command line parameters can be added to the end of the `docker run` command. - -!!! Note - You can define a [restart policy](https://docs.docker.com/config/containers/start-containers-automatically/) in docker. It can be useful in some cases to use the `--restart unless-stopped` flag (crash of freqtrade or reboot of your system). - -### Monitor your Docker instance - -You can use the following commands to monitor and manage your container: - -```bash -docker logs freqtrade -docker logs -f freqtrade -docker restart freqtrade -docker stop freqtrade -docker start freqtrade -``` - -For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/). - -!!! Note - You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container. - -### Backtest with docker - -The following assumes that the download/setup of the docker image have been completed successfully. -Also, backtest-data should be available at `~/.freqtrade/user_data/`. - -```bash -docker run -d \ - --name freqtrade \ - -v /etc/localtime:/etc/localtime:ro \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - -v ~/.freqtrade/user_data/:/freqtrade/user_data/ \ - freqtrade backtesting --strategy AwsomelyProfitableStrategy -``` - -Head over to the [Backtesting Documentation](backtesting.md) for more details. - -!!! Note - Additional bot command line parameters can be appended after the image name (`freqtrade` in the above example). diff --git a/mkdocs.yml b/mkdocs.yml index 5d936687f..26494ae45 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,9 +1,9 @@ site_name: Freqtrade nav: - Home: index.md - - Quickstart with Docker: docker.md - - Installation: - - Freqtrade without docker-compose: without_docker_compose.md + - Quickstart with Docker: docker_quickstart.md + - Installation: + - Docker without docker-compose: docker.md - Linux/MacOS/Raspberry: installation.md - Windows: windows_installation.md - Freqtrade Basics: bot-basics.md From bc5cc48f67ddb494ad320e93817e7f8f7e44606b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 4 Sep 2020 07:28:21 +0200 Subject: [PATCH 107/224] Adjust windows docs, fix failing doc-test --- docs/windows_installation.md | 28 ++++++++++++++++++---------- tests/test_docs.sh | 3 +-- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 1cdb3d613..f7900d85a 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -9,7 +9,7 @@ Otherwise, try the instructions below. Make sure to use 64bit Windows and 64bit Python to avoid problems with backtesting or hyperopt due to the memory constraints 32bit applications have under Windows. !!! Hint - Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Conda section](#using-conda) in this document for more information. + Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Anaconda installation section](installation.md#Anaconda) in this document for more information. ### 1. Clone the git repository @@ -23,17 +23,25 @@ Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7 As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial precompiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib‑0.4.18‑cp38‑cp38‑win_amd64.whl` (make sure to use the version matching your python version) -```cmd ->cd \path\freqtrade-develop ->python -m venv .env ->.env\Scripts\activate.bat -REM optionally install ta-lib from wheel -REM >pip install TA_Lib‑0.4.18‑cp38‑cp38‑win_amd64.whl ->pip install -r requirements.txt ->pip install -e . ->freqtrade +Freqtrade provides these dependencies for the latest 2 Python versions (3.7 and 3.8) and for 64bit Windows. +Other versions must be downloaded from the above link. + +``` powershell +cd \path\freqtrade +python -m venv .env +.env\Scripts\activate.ps1 +# optionally install ta-lib from wheel +# Eventually adjust the below filename to match the downloaded wheel +pip install build_helpes/TA_Lib‑0.4.18‑cp38‑cp38‑win_amd64.whl +pip install -r requirements.txt +pip install -e . +freqtrade ``` +!!! Note "Use Powershell" + The above installation script assumes you're using powershell on a 64bit windows. + Commands for the legacy CMD windows console may differ. + > Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222) ### Error during installation on Windows diff --git a/tests/test_docs.sh b/tests/test_docs.sh index 8a354daad..09e142b99 100755 --- a/tests/test_docs.sh +++ b/tests/test_docs.sh @@ -2,8 +2,7 @@ # Test Documentation boxes - # !!! : is not allowed! # !!! "title" - Title needs to be quoted! -# !!! Spaces at the beginning are not allowed -grep -Er '^!{3}\s\S+:|^!{3}\s\S+\s[^"]|^\s+!{3}\s\S+' docs/* +grep -Er '^!{3}\s\S+:|^!{3}\s\S+\s[^"]' docs/* if [ $? -ne 0 ]; then echo "Docs test success." From bd4f3d838ab959c3c8ff9f0e39daaeb62c3bddce Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 4 Sep 2020 19:44:35 +0200 Subject: [PATCH 108/224] Implement merge_informative_pairs helper --- freqtrade/strategy/__init__.py | 1 + freqtrade/strategy/strategy_helper.py | 39 ++++++++++++++++ tests/strategy/test_strategy_helpers.py | 61 +++++++++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 freqtrade/strategy/strategy_helper.py create mode 100644 tests/strategy/test_strategy_helpers.py diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 91ea0e075..5758bbbcc 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -2,3 +2,4 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_prev_date, timeframe_to_seconds, timeframe_to_next_date, timeframe_to_msecs) from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy.strategy_helper import merge_informative_pairs diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py new file mode 100644 index 000000000..ce98cccba --- /dev/null +++ b/freqtrade/strategy/strategy_helper.py @@ -0,0 +1,39 @@ +import pandas as pd +from freqtrade.exchange import timeframe_to_minutes + + +def merge_informative_pairs(dataframe: pd.DataFrame, informative: pd.DataFrame, + timeframe_inf: str, ffill: bool = True) -> pd.DataFrame: + """ + Correctly merge informative samples to the original dataframe, avoiding lookahead bias. + + Since dates are candle open dates, merging a 15m candle that starts at 15:00, and a + 1h candle that starts at 15:00 will result in all candles to know the close at 16:00 + which they should not know. + + Moves the date of the informative pair by 1 time interval forward. + This way, the 14:00 1h candle is merged to 15:00 15m candle, since the 14:00 1h candle is the + last candle that's closed at 15:00, 15:15, 15:30 or 15:45. + + :param dataframe: Original dataframe + :param informative: Informative pair, most likely loaded via dp.get_pair_dataframe + :param timeframe_inf: Timeframe of the informative pair sample. + :param ffill: Forwardfill missing values - optional but usually required + """ + # Rename columns to be unique + + minutes = timeframe_to_minutes(timeframe_inf) + informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes, 'm') + + informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] + + # Combine the 2 dataframes + # all indicators on the informative sample MUST be calculated before this point + dataframe = pd.merge(dataframe, informative, left_on='date', + right_on=f'date_merge_{timeframe_inf}', how='left') + dataframe = dataframe.drop(f'date_merge_{timeframe_inf}', axis=1) + + if ffill: + dataframe = dataframe.ffill() + + return dataframe diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py new file mode 100644 index 000000000..89bbba2c1 --- /dev/null +++ b/tests/strategy/test_strategy_helpers.py @@ -0,0 +1,61 @@ +import pandas as pd +import numpy as np + +from freqtrade.strategy import merge_informative_pairs, timeframe_to_minutes + + +def generate_test_data(timeframe: str, size: int): + np.random.seed(42) + tf_mins = timeframe_to_minutes(timeframe) + + base = np.random.normal(20, 2, size=size) + + date = pd.period_range('2020-07-05', periods=size, freq=f'{tf_mins}min').to_timestamp() + df = pd.DataFrame({ + 'date': date, + 'open': base, + 'high': base + np.random.normal(2, 1, size=size), + 'low': base - np.random.normal(2, 1, size=size), + 'close': base + np.random.normal(0, 1, size=size), + 'volume': np.random.normal(200, size=size) + } + ) + df = df.dropna() + return df + + +def test_merge_informative_pairs(): + data = generate_test_data('15m', 40) + informative = generate_test_data('1h', 40) + + result = merge_informative_pairs(data, informative, '1h', ffill=True) + assert isinstance(result, pd.DataFrame) + assert len(result) == len(data) + assert 'date' in result.columns + assert result['date'].equals(data['date']) + assert 'date_1h' in result.columns + + assert 'open' in result.columns + assert 'open_1h' in result.columns + assert result['open'].equals(data['open']) + + assert 'close' in result.columns + assert 'close_1h' in result.columns + assert result['close'].equals(data['close']) + + assert 'volume' in result.columns + assert 'volume_1h' in result.columns + assert result['volume'].equals(data['volume']) + + # First 4 rows are empty + assert result.iloc[0]['date_1h'] is pd.NaT + assert result.iloc[1]['date_1h'] is pd.NaT + assert result.iloc[2]['date_1h'] is pd.NaT + assert result.iloc[3]['date_1h'] is pd.NaT + # Next 4 rows contain the starting date (0:00) + assert result.iloc[4]['date_1h'] == result.iloc[0]['date'] + assert result.iloc[5]['date_1h'] == result.iloc[0]['date'] + assert result.iloc[6]['date_1h'] == result.iloc[0]['date'] + assert result.iloc[7]['date_1h'] == result.iloc[0]['date'] + # Next 4 rows contain the next Hourly date original date row 4 + assert result.iloc[8]['date_1h'] == result.iloc[4]['date'] From 7bc89279148872e43d4f6f6233ffbc864bc1f436 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 4 Sep 2020 20:02:31 +0200 Subject: [PATCH 109/224] Add documentation for merge_informative_pair helper --- docs/strategy-customization.md | 79 +++++++++++++++++++------ freqtrade/strategy/__init__.py | 2 +- freqtrade/strategy/strategy_helper.py | 7 ++- tests/strategy/test_strategy_helpers.py | 6 +- 4 files changed, 71 insertions(+), 23 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index e2548e510..c791be615 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -483,9 +483,8 @@ if self.dp: ### Complete Data-provider sample ```python -from freqtrade.strategy import IStrategy, timeframe_to_minutes +from freqtrade.strategy import IStrategy, merge_informative_pairs from pandas import DataFrame -import pandas as pd class SampleStrategy(IStrategy): # strategy init stuff... @@ -517,23 +516,12 @@ class SampleStrategy(IStrategy): # Get the 14 day rsi informative['rsi'] = ta.RSI(informative, timeperiod=14) - # Rename columns to be unique - informative.columns = [f"{col}_{inf_tf}" for col in informative.columns] - # Assuming inf_tf = '1d' - then the columns will now be: - # date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d - - # Shift date by 1 candle - # This is necessary since the data is always the "open date" - # and a 15m candle starting at 12:15 should not know the close of the 1h candle from 12:00 to 13:00 - minutes = timeframe_to_minutes(inf_tf) - informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes, 'm') - - # Combine the 2 dataframes - # all indicators on the informative sample MUST be calculated before this point - dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_merge_{inf_tf}', how='left') + # Use the helper function merge_informative_pair to safely merge the pair + # Automatically renames the columns and merges a shorter timeframe dataframe and a longer timeframe informative pair # FFill to have the 1d value available in every row throughout the day. # Without this, comparisons would only work once per day. - dataframe = dataframe.ffill() + # Full documentation of this method, see below + dataframe = merge_informative_pair(dataframe, informative_pairs, inf_tf, ffill=True) # Calculate rsi of the original dataframe (5m timeframe) dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) @@ -557,6 +545,63 @@ class SampleStrategy(IStrategy): *** +## Helper functions + +### *merge_informative_pair()* + +This method helps you merge an informative pair to a regular dataframe without lookahead bias. +It's there to help you merge the dataframe in a safe and consistent way. + +Options: + +- Rename the columns for you to create unique columns +- Merge the dataframe without lookahead bias +- Forward-fill (optional) + +All columns of the informative dataframe will be available on the returning dataframe in a renamed fashion: + +!!! Example "Column renaming" + Assuming `inf_tf = '1d'` the resulting columns will be: + + ``` python + 'date', 'open', 'high', 'low', 'close', 'rsi' # from the original dataframe + 'date_1d', 'open_1d', 'high_1d', 'low_1d', 'close_1d', 'rsi_1d' # from the informative dataframe + ``` + +??? Example "Column renaming - 1h" + Assuming `inf_tf = '1h'` the resulting columns will be: + + ``` python + 'date', 'open', 'high', 'low', 'close', 'rsi' # from the original dataframe + 'date_1h', 'open_1h', 'high_1h', 'low_1h', 'close_1h', 'rsi_1h' # from the informative dataframe + ``` + +??? Example "Custom implementation" + A custom implementation for this is possible, and can be done as follows: + + ``` python + # Rename columns to be unique + informative.columns = [f"{col}_{inf_tf}" for col in informative.columns] + # Assuming inf_tf = '1d' - then the columns will now be: + # date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d + + # Shift date by 1 candle + # This is necessary since the data is always the "open date" + # and a 15m candle starting at 12:15 should not know the close of the 1h candle from 12:00 to 13:00 + minutes = timeframe_to_minutes(inf_tf) + informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes, 'm') + + # Combine the 2 dataframes + # all indicators on the informative sample MUST be calculated before this point + dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_merge_{inf_tf}', how='left') + # FFill to have the 1d value available in every row throughout the day. + # Without this, comparisons would only work once per day. + dataframe = dataframe.ffill() + + ``` + +*** + ## Additional data (Wallets) The strategy provides access to the `Wallets` object. This contains the current balances on the exchange. diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 5758bbbcc..d1510489e 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -2,4 +2,4 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_prev_date, timeframe_to_seconds, timeframe_to_next_date, timeframe_to_msecs) from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.strategy_helper import merge_informative_pairs +from freqtrade.strategy.strategy_helper import merge_informative_pair diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index ce98cccba..2684e7b03 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -2,8 +2,8 @@ import pandas as pd from freqtrade.exchange import timeframe_to_minutes -def merge_informative_pairs(dataframe: pd.DataFrame, informative: pd.DataFrame, - timeframe_inf: str, ffill: bool = True) -> pd.DataFrame: +def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, + timeframe_inf: str, ffill: bool = True) -> pd.DataFrame: """ Correctly merge informative samples to the original dataframe, avoiding lookahead bias. @@ -15,6 +15,9 @@ def merge_informative_pairs(dataframe: pd.DataFrame, informative: pd.DataFrame, This way, the 14:00 1h candle is merged to 15:00 15m candle, since the 14:00 1h candle is the last candle that's closed at 15:00, 15:15, 15:30 or 15:45. + Assuming inf_tf = '1d' - then the resulting columns will be: + date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d + :param dataframe: Original dataframe :param informative: Informative pair, most likely loaded via dp.get_pair_dataframe :param timeframe_inf: Timeframe of the informative pair sample. diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 89bbba2c1..9201d91e1 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -1,7 +1,7 @@ import pandas as pd import numpy as np -from freqtrade.strategy import merge_informative_pairs, timeframe_to_minutes +from freqtrade.strategy import merge_informative_pair, timeframe_to_minutes def generate_test_data(timeframe: str, size: int): @@ -24,11 +24,11 @@ def generate_test_data(timeframe: str, size: int): return df -def test_merge_informative_pairs(): +def test_merge_informative_pair(): data = generate_test_data('15m', 40) informative = generate_test_data('1h', 40) - result = merge_informative_pairs(data, informative, '1h', ffill=True) + result = merge_informative_pair(data, informative, '1h', ffill=True) assert isinstance(result, pd.DataFrame) assert len(result) == len(data) assert 'date' in result.columns From cc684c51415afd7e1b682242ecd8c76f594e8c1d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 4 Sep 2020 20:09:02 +0200 Subject: [PATCH 110/224] Correctly handle identical timerame merges --- docs/strategy-customization.md | 7 +++--- freqtrade/strategy/strategy_helper.py | 11 +++++++--- tests/strategy/test_strategy_helpers.py | 29 ++++++++++++++++++++++++- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index c791be615..7396f2a89 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -518,10 +518,10 @@ class SampleStrategy(IStrategy): # Use the helper function merge_informative_pair to safely merge the pair # Automatically renames the columns and merges a shorter timeframe dataframe and a longer timeframe informative pair - # FFill to have the 1d value available in every row throughout the day. - # Without this, comparisons would only work once per day. + # use ffill to have the 1d value available in every row throughout the day. + # Without this, comparisons between columns of the original and the informative pair would only work once per day. # Full documentation of this method, see below - dataframe = merge_informative_pair(dataframe, informative_pairs, inf_tf, ffill=True) + dataframe = merge_informative_pair(dataframe, informative_pairs, self.timeframe, inf_tf, ffill=True) # Calculate rsi of the original dataframe (5m timeframe) dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) @@ -589,6 +589,7 @@ All columns of the informative dataframe will be available on the returning data # This is necessary since the data is always the "open date" # and a 15m candle starting at 12:15 should not know the close of the 1h candle from 12:00 to 13:00 minutes = timeframe_to_minutes(inf_tf) + # Only do this if the timeframes are different: informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes, 'm') # Combine the 2 dataframes diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 2684e7b03..0fa7f4258 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -3,7 +3,7 @@ from freqtrade.exchange import timeframe_to_minutes def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, - timeframe_inf: str, ffill: bool = True) -> pd.DataFrame: + timeframe: str, timeframe_inf: str, ffill: bool = True) -> pd.DataFrame: """ Correctly merge informative samples to the original dataframe, avoiding lookahead bias. @@ -20,13 +20,18 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, :param dataframe: Original dataframe :param informative: Informative pair, most likely loaded via dp.get_pair_dataframe + :param timeframe: Timeframe of the original pair sample. :param timeframe_inf: Timeframe of the informative pair sample. :param ffill: Forwardfill missing values - optional but usually required """ # Rename columns to be unique - minutes = timeframe_to_minutes(timeframe_inf) - informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes, 'm') + minutes_inf = timeframe_to_minutes(timeframe_inf) + if timeframe == timeframe_inf: + # No need to forwardshift if the timeframes are identical + informative['date_merge'] = informative["date"] + else: + informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes_inf, 'm') informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 9201d91e1..4b29bf304 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -28,7 +28,7 @@ def test_merge_informative_pair(): data = generate_test_data('15m', 40) informative = generate_test_data('1h', 40) - result = merge_informative_pair(data, informative, '1h', ffill=True) + result = merge_informative_pair(data, informative, '15m', '1h', ffill=True) assert isinstance(result, pd.DataFrame) assert len(result) == len(data) assert 'date' in result.columns @@ -59,3 +59,30 @@ def test_merge_informative_pair(): assert result.iloc[7]['date_1h'] == result.iloc[0]['date'] # Next 4 rows contain the next Hourly date original date row 4 assert result.iloc[8]['date_1h'] == result.iloc[4]['date'] + + +def test_merge_informative_pair_same(): + data = generate_test_data('15m', 40) + informative = generate_test_data('15m', 40) + + result = merge_informative_pair(data, informative, '15m', '15m', ffill=True) + assert isinstance(result, pd.DataFrame) + assert len(result) == len(data) + assert 'date' in result.columns + assert result['date'].equals(data['date']) + assert 'date_15m' in result.columns + + assert 'open' in result.columns + assert 'open_15m' in result.columns + assert result['open'].equals(data['open']) + + assert 'close' in result.columns + assert 'close_15m' in result.columns + assert result['close'].equals(data['close']) + + assert 'volume' in result.columns + assert 'volume_15m' in result.columns + assert result['volume'].equals(data['volume']) + + # Dates match 1:1 + assert result['date_15m'].equals(result['date']) From 71af64af94ba07a19181618e58f0dbfa8480709f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 4 Sep 2020 20:10:43 +0200 Subject: [PATCH 111/224] Move comment to the right place --- freqtrade/strategy/strategy_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 0fa7f4258..1fbf618bd 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -24,7 +24,6 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, :param timeframe_inf: Timeframe of the informative pair sample. :param ffill: Forwardfill missing values - optional but usually required """ - # Rename columns to be unique minutes_inf = timeframe_to_minutes(timeframe_inf) if timeframe == timeframe_inf: @@ -33,6 +32,7 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, else: informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes_inf, 'm') + # Rename columns to be unique informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] # Combine the 2 dataframes From c18441f36fe5fa25012f0b363153d41df93c4955 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Sep 2020 16:44:23 +0200 Subject: [PATCH 112/224] Fix typo in reloading_conf --- freqtrade/rpc/rpc.py | 2 +- tests/rpc/test_rpc_apiserver.py | 2 +- tests/rpc/test_rpc_telegram.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b89a95ee8..0b9196f2e 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -436,7 +436,7 @@ class RPC: def _rpc_reload_config(self) -> Dict[str, str]: """ Handler for reload_config. """ self._freqtrade.state = State.RELOAD_CONFIG - return {'status': 'reloading config ...'} + return {'status': 'Reloading config ...'} def _rpc_stopbuy(self) -> Dict[str, str]: """ diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d2b69ee4f..d9f5bf781 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -266,7 +266,7 @@ def test_api_reloadconf(botclient): rc = client_post(client, f"{BASE_URI}/reload_config") assert_response(rc) - assert rc.json == {'status': 'reloading config ...'} + assert rc.json == {'status': 'Reloading config ...'} assert ftbot.state == State.RELOAD_CONFIG diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 71898db8c..762780111 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -692,7 +692,7 @@ def test_reload_config_handle(default_conf, update, mocker) -> None: telegram._reload_config(update=update, context=MagicMock()) assert freqtradebot.state == State.RELOAD_CONFIG assert msg_mock.call_count == 1 - assert 'reloading config' in msg_mock.call_args_list[0][0][0] + assert 'Reloading config' in msg_mock.call_args_list[0][0][0] def test_telegram_forcesell_handle(default_conf, update, ticker, fee, From 8c9297e1f0effeccd5490652f83b91a0a3105507 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Sep 2020 16:51:19 +0200 Subject: [PATCH 113/224] Don't crash if a strategy imports something wrongly --- freqtrade/resolvers/iresolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 52d944f2c..b7d25ef2c 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -59,7 +59,7 @@ class IResolver: module = importlib.util.module_from_spec(spec) try: spec.loader.exec_module(module) # type: ignore # importlib does not use typehints - except (ModuleNotFoundError, SyntaxError) as err: + except (ModuleNotFoundError, SyntaxError, ImportError) as err: # Catch errors in case a specific module is not installed logger.warning(f"Could not import {module_path} due to '{err}'") if enum_failed: From b4c35291358fa33613b1eeae72f3a6bbdabef15d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Sep 2020 14:05:15 +0200 Subject: [PATCH 114/224] Add orders to mock_trades fixture --- tests/conftest.py | 67 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 08061f647..591fd47bb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring +from freqtrade.persistence.models import Order import json import logging import re @@ -184,6 +185,17 @@ def create_mock_trades(fee): open_order_id='dry_run_buy_12345', strategy='DefaultStrategy', ) + o = Order.parse_from_ccxt_object({ + 'id': '1234', + 'symbol': 'ETH/BTC', + 'status': 'closed', + 'side': 'buy', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'buy') + trade.orders.append(o) Trade.session.add(trade) trade = Trade( @@ -201,6 +213,28 @@ def create_mock_trades(fee): open_order_id='dry_run_sell_12345', strategy='DefaultStrategy', ) + o = Order.parse_from_ccxt_object({ + 'id': '1235', + 'symbol': 'ETC/BTC', + 'status': 'closed', + 'side': 'buy', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'buy') + trade.orders.append(o) + o = Order.parse_from_ccxt_object({ + 'id': '12366', + 'symbol': 'ETC/BTC', + 'status': 'closed', + 'side': 'sell', + 'price': 0.128, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'sell') + trade.orders.append(o) Trade.session.add(trade) trade = Trade( @@ -215,6 +249,28 @@ def create_mock_trades(fee): exchange='bittrex', is_open=False, ) + o = Order.parse_from_ccxt_object({ + 'id': '41231a12a', + 'symbol': 'XRP/BTC', + 'status': 'closed', + 'side': 'buy', + 'price': 0.05, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'buy') + trade.orders.append(o) + o = Order.parse_from_ccxt_object({ + 'id': '41231a666a', + 'symbol': 'XRP/BTC', + 'status': 'closed', + 'side': 'stop_loss', + 'price': 0.06, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'sell') + trade.orders.append(o) Trade.session.add(trade) # Simulate prod entry @@ -230,6 +286,17 @@ def create_mock_trades(fee): open_order_id='prod_buy_12345', strategy='DefaultStrategy', ) + o = Order.parse_from_ccxt_object({ + 'id': 'prod_buy_12345', + 'symbol': 'ETC/BTC', + 'status': 'open', + 'side': 'buy', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'buy') + trade.orders.append(o) Trade.session.add(trade) From b7662722bacb8635c4733abe450214c8d5297bb5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Sep 2020 14:17:45 +0200 Subject: [PATCH 115/224] Add tests for Order object parsing --- freqtrade/persistence/models.py | 1 + tests/test_persistence.py | 47 +++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index e0b9624dd..891822064 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -154,6 +154,7 @@ class Order(_DECL_BASE): if 'timestamp' in order and order['timestamp'] is not None: self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc) + self.ft_is_open = True if self.status in ('closed', 'canceled', 'cancelled'): self.ft_is_open = False if order.get('filled', 0) > 0: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index c812c496f..b6623d461 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1030,3 +1030,50 @@ def test_get_best_pair(fee): assert len(res) == 2 assert res[0] == 'XRP/BTC' assert res[1] == 0.01 + + +@pytest.mark.usefixtures("init_persistence") +def test_update_order_from_ccxt(): + # Most basic order return (only has orderid) + o = Order.parse_from_ccxt_object({'id': '1234'}, 'ETH/BTC', 'buy') + assert isinstance(o, Order) + assert o.ft_pair == 'ETH/BTC' + assert o.ft_order_side == 'buy' + assert o.order_id == '1234' + assert o.ft_is_open + ccxt_order = { + 'id': '1234', + 'side': 'buy', + 'symbol': 'ETH/BTC', + 'type': 'limit', + 'price': 1234.5, + 'amount': 20.0, + 'filled': 9, + 'remaining': 11, + 'status': 'open', + 'timestamp': 1599394315123 + } + o = Order.parse_from_ccxt_object(ccxt_order, 'ETH/BTC', 'buy') + assert isinstance(o, Order) + assert o.ft_pair == 'ETH/BTC' + assert o.ft_order_side == 'buy' + assert o.order_id == '1234' + assert o.order_type == 'limit' + assert o.price == 1234.5 + assert o.filled == 9 + assert o.remaining == 11 + assert o.order_date is not None + assert o.ft_is_open + assert o.order_filled_date is None + + # Order has been closed + ccxt_order.update({'filled': 20.0, 'remaining': 0.0, 'status': 'closed'}) + o.update_from_ccxt_object(ccxt_order) + + assert o.filled == 20.0 + assert o.remaining == 0.0 + assert not o.ft_is_open + assert o.order_filled_date is not None + + + From a78d61150c5ab7346fe9a3557de0cc3b282d2e67 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Sep 2020 14:27:36 +0200 Subject: [PATCH 116/224] Deleting must delete orders first --- freqtrade/freqtradebot.py | 3 +-- freqtrade/persistence/models.py | 8 ++++++++ freqtrade/rpc/rpc.py | 3 +-- tests/rpc/test_rpc.py | 1 - tests/test_persistence.py | 3 --- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3e9f8c75c..f2cd4c5dd 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1118,8 +1118,7 @@ class FreqtradeBot: if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC): logger.info('Buy order fully cancelled. Removing %s from database.', trade) # if trade is not partially completed, just delete the trade - Trade.session.delete(trade) - Trade.session.flush() + trade.delete() was_trade_fully_canceled = True else: # if trade is partially complete, edit the stake details for the trade diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 891822064..9759ac830 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -446,6 +446,14 @@ class Trade(_DECL_BASE): def update_order(self, order: Dict) -> None: Order.update_orders(self.orders, order) + def delete(self) -> None: + + for order in self.orders: + Order.session.delete(order) + + Trade.session.delete(self) + Trade.session.flush() + def _calc_open_trade_price(self) -> float: """ Calculate the open_rate including open_fee. diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 9b5d79267..6ace0bb88 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -567,8 +567,7 @@ class RPC: except (ExchangeError): pass - Trade.session.delete(trade) - Trade.session.flush() + trade.delete() self._freqtrade.wallets.update() return { 'result': 'success', diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 42025b3a3..8c13bc00f 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -313,7 +313,6 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog): with pytest.raises(RPCException, match='invalid argument'): rpc._rpc_delete('200') - create_mock_trades(fee) trades = Trade.query.all() trades[1].stoploss_order_id = '1234' trades[2].stoploss_order_id = '1234' diff --git a/tests/test_persistence.py b/tests/test_persistence.py index b6623d461..707247e99 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1074,6 +1074,3 @@ def test_update_order_from_ccxt(): assert o.remaining == 0.0 assert not o.ft_is_open assert o.order_filled_date is not None - - - From 68d51a97878e8b31e8fe49013d3cced2136ffa51 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Sep 2020 14:33:45 +0200 Subject: [PATCH 117/224] Don't raise OperationalException when orderid's dont' match --- freqtrade/persistence/models.py | 4 ++-- tests/test_persistence.py | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 9759ac830..096cf6209 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -17,7 +17,7 @@ from sqlalchemy.orm.session import sessionmaker from sqlalchemy.pool import StaticPool from sqlalchemy.sql.schema import UniqueConstraint -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.misc import safe_value_fallback from freqtrade.persistence.migrations import check_migrate @@ -140,7 +140,7 @@ class Order(_DECL_BASE): Only updates if fields are available from ccxt - """ if self.order_id != str(order['id']): - return OperationalException("Order-id's don't match") + raise DependencyException("Order-id's don't match") self.status = order.get('status', self.status) self.symbol = order.get('symbol', self.symbol) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 707247e99..788debace 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -7,9 +7,9 @@ import pytest from sqlalchemy import create_engine from freqtrade import constants -from freqtrade.exceptions import OperationalException -from freqtrade.persistence import Trade, Order, clean_dry_run_db, init -from tests.conftest import log_has, create_mock_trades +from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.persistence import Order, Trade, clean_dry_run_db, init +from tests.conftest import create_mock_trades, log_has def test_init_create_session(default_conf): @@ -1074,3 +1074,7 @@ def test_update_order_from_ccxt(): assert o.remaining == 0.0 assert not o.ft_is_open assert o.order_filled_date is not None + + ccxt_order.update({'id': 'somethingelse'}) + with pytest.raises(DependencyException, match=r"Order-id's don't match"): + o.update_from_ccxt_object(ccxt_order) From cec98ad407b2814b7266ae9d21a2ee4b1da066ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Sep 2020 14:51:11 +0200 Subject: [PATCH 118/224] Test stoploss insufficient funds handling --- tests/test_freqtradebot.py | 40 +++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index f89befc55..850ddaeeb 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -13,7 +13,7 @@ import pytest from freqtrade.constants import (CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT) -from freqtrade.exceptions import (DependencyException, ExchangeError, +from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot @@ -1327,6 +1327,44 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, assert rpc_mock.call_args_list[1][0][0]['order_type'] == 'market' +def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, fee, + limit_buy_order_open, limit_sell_order): + sell_mock = MagicMock(return_value={'id': limit_sell_order['id']}) + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=MagicMock(return_value={ + 'bid': 0.00001172, + 'ask': 0.00001173, + 'last': 0.00001172 + }), + buy=MagicMock(return_value=limit_buy_order_open), + sell=sell_mock, + get_fee=fee, + fetch_order=MagicMock(return_value={'status': 'canceled'}), + stoploss=MagicMock(side_effect=InsufficientFundsError()), + ) + patch_get_signal(freqtrade) + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + + freqtrade.enter_positions() + trade = Trade.query.first() + caplog.clear() + freqtrade.create_stoploss_order(trade, 200) + # stoploss_orderid was empty before + assert trade.stoploss_order_id is None + assert mock_insuf.call_count == 1 + mock_insuf.reset_mock() + + trade.stoploss_order_id = 'stoploss_orderid' + freqtrade.create_stoploss_order(trade, 200) + # No change to stoploss-orderid + assert trade.stoploss_order_id == 'stoploss_orderid' + assert mock_insuf.call_count == 1 + + @pytest.mark.usefixtures("init_persistence") def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, limit_buy_order, limit_sell_order) -> None: From 7c1f111ddfd50eeb4cc770aaa8564497cf12ea64 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Sep 2020 14:59:30 +0200 Subject: [PATCH 119/224] Add insufficient_funds_test --- tests/test_freqtradebot.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 850ddaeeb..2f8e5aef6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2981,6 +2981,35 @@ def test_execute_sell_market_order(default_conf, ticker, fee, } == last_msg +def test_execute_sell_insufficient_funds_error(default_conf, ticker, fee, + ticker_sell_up, mocker) -> None: + freqtrade = get_patched_freqtradebot(mocker, default_conf) + mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + get_fee=fee, + sell=MagicMock(side_effect=InsufficientFundsError()) + ) + patch_get_signal(freqtrade) + + # Create some test data + freqtrade.enter_positions() + + trade = Trade.query.first() + assert trade + + # Increase the price and sell it + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker_sell_up + ) + + assert not freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], + sell_reason=SellType.ROI) + assert mock_insuf.call_count == 1 + + def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy_order_open, fee, mocker) -> None: patch_RPCManager(mocker) From b4da36d6e9a94bd81b9da90a5e0350b931793f4c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Sep 2020 15:05:47 +0200 Subject: [PATCH 120/224] Fix small typo and add small testcase --- freqtrade/freqtradebot.py | 2 +- tests/test_freqtradebot.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f2cd4c5dd..44d1c31eb 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1376,7 +1376,7 @@ class FreqtradeBot: """ if not order_id: logger.warning(f'Orderid for trade {trade} is empty.') - False + return False # Update trade with order values logger.info('Found open order for %s', trade) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 2f8e5aef6..fe5b64d5b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1737,6 +1737,8 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No open_date=arrow.utcnow().datetime, amount=11, ) + assert not freqtrade.update_trade_state(trade, None) + assert log_has_re(r'Orderid for trade .* is empty.', caplog) # Add datetime explicitly since sqlalchemy defaults apply only once written to database freqtrade.update_trade_state(trade, '123') # Test amount not modified by fee-logic From a0fd7f46445e32c7f92d339fd398de3a2151c528 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Sep 2020 15:27:16 +0200 Subject: [PATCH 121/224] Update tests to merged version --- tests/exchange/test_exchange.py | 2 +- tests/rpc/test_rpc.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 64de1f171..e0b97d157 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1765,7 +1765,7 @@ def test_cancel_order_dry_run(default_conf, mocker, exchange_name): cancel_order = exchange.cancel_order(order_id=order['id'], pair='ETH/BTC') assert order['id'] == cancel_order['id'] assert order['amount'] == cancel_order['amount'] - assert order['pair'] == cancel_order['pair'] + assert order['symbol'] == cancel_order['symbol'] assert cancel_order['status'] == 'canceled' diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 5f11532b0..c2dee6439 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -716,11 +716,13 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: mocker.patch( 'freqtrade.exchange.Exchange.fetch_order', side_effect=[{ + 'id': '1234', 'status': 'open', 'type': 'limit', 'side': 'buy', 'filled': filled_amount }, { + 'id': '1234', 'status': 'closed', 'type': 'limit', 'side': 'buy', From f6ebe51314875b42596b4e21ea4e9acf6761822f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Sep 2020 19:32:00 +0200 Subject: [PATCH 122/224] Add test for update_open_orders --- freqtrade/freqtradebot.py | 4 ++-- tests/test_freqtradebot.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4f68a1112..1af0e85b7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -247,8 +247,8 @@ class FreqtradeBot: self.update_trade_state(order.trade, order.order_id, fo) - except ExchangeError: - logger.warning(f"Error updating {order.order_id}") + except ExchangeError as e: + logger.warning(f"Error updating Order {order.order_id} due to {e}") def update_closed_trades_without_assigned_fees(self): """ diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c366b6777..0f340ae8f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4216,7 +4216,7 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi @pytest.mark.usefixtures("init_persistence") -def test_check_for_open_trades(mocker, default_conf, fee, limit_buy_order, limit_sell_order): +def test_check_for_open_trades(mocker, default_conf, fee): freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade.check_for_open_trades() @@ -4229,3 +4229,30 @@ def test_check_for_open_trades(mocker, default_conf, fee, limit_buy_order, limit freqtrade.check_for_open_trades() assert freqtrade.rpc.send_msg.call_count == 1 assert 'Handle these trades manually' in freqtrade.rpc.send_msg.call_args[0][0]['status'] + + +@pytest.mark.usefixtures("init_persistence") +def test_update_open_orders(mocker, default_conf, fee, caplog): + freqtrade = get_patched_freqtradebot(mocker, default_conf) + create_mock_trades(fee) + + freqtrade.update_open_orders() + assert log_has_re(r"Error updating Order .*", caplog) + caplog.clear() + + assert len(Order.get_open_orders()) == 1 + + matching_buy_order = { + 'id': 'prod_buy_12345', + 'symbol': 'ETC/BTC', + 'status': 'closed', + 'side': 'buy', + 'type': 'limit', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + } + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=matching_buy_order) + freqtrade.update_open_orders() + assert len(Order.get_open_orders()) == 0 From cad0275b3282b30f50bd62d034a79add1a575f87 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Sep 2020 06:39:48 +0200 Subject: [PATCH 123/224] Extract mock_trade generation to sepearate file --- tests/conftest.py | 134 +------------------------------- tests/conftest_trades.py | 162 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+), 130 deletions(-) create mode 100644 tests/conftest_trades.py diff --git a/tests/conftest.py b/tests/conftest.py index 591fd47bb..2181388fa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring +from tests.conftest_trades import create_mock_trades from freqtrade.persistence.models import Order import json import logging @@ -168,136 +169,9 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: freqtrade.exchange.refresh_latest_ohlcv = lambda p: None -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, - amount_requested=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', - strategy='DefaultStrategy', - ) - o = Order.parse_from_ccxt_object({ - 'id': '1234', - 'symbol': 'ETH/BTC', - 'status': 'closed', - 'side': 'buy', - 'price': 0.123, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'buy') - trade.orders.append(o) - Trade.session.add(trade) - - trade = Trade( - pair='ETC/BTC', - stake_amount=0.001, - amount=123.0, - amount_requested=123.0, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_rate=0.123, - close_rate=0.128, - close_profit=0.005, - exchange='bittrex', - is_open=False, - open_order_id='dry_run_sell_12345', - strategy='DefaultStrategy', - ) - o = Order.parse_from_ccxt_object({ - 'id': '1235', - 'symbol': 'ETC/BTC', - 'status': 'closed', - 'side': 'buy', - 'price': 0.123, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'buy') - trade.orders.append(o) - o = Order.parse_from_ccxt_object({ - 'id': '12366', - 'symbol': 'ETC/BTC', - 'status': 'closed', - 'side': 'sell', - 'price': 0.128, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'sell') - trade.orders.append(o) - Trade.session.add(trade) - - trade = Trade( - pair='XRP/BTC', - stake_amount=0.001, - amount=123.0, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_rate=0.05, - close_rate=0.06, - close_profit=0.01, - exchange='bittrex', - is_open=False, - ) - o = Order.parse_from_ccxt_object({ - 'id': '41231a12a', - 'symbol': 'XRP/BTC', - 'status': 'closed', - 'side': 'buy', - 'price': 0.05, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'buy') - trade.orders.append(o) - o = Order.parse_from_ccxt_object({ - 'id': '41231a666a', - 'symbol': 'XRP/BTC', - 'status': 'closed', - 'side': 'stop_loss', - 'price': 0.06, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'sell') - trade.orders.append(o) - Trade.session.add(trade) - - # Simulate prod entry - trade = Trade( - pair='ETC/BTC', - stake_amount=0.001, - amount=123.0, - amount_requested=124.0, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_rate=0.123, - exchange='bittrex', - open_order_id='prod_buy_12345', - strategy='DefaultStrategy', - ) - o = Order.parse_from_ccxt_object({ - 'id': 'prod_buy_12345', - 'symbol': 'ETC/BTC', - 'status': 'open', - 'side': 'buy', - 'price': 0.123, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'buy') - trade.orders.append(o) - Trade.session.add(trade) +@pytest.fixture(scope='function') +def mock_trades(fee): + return create_mock_trades(fee) @pytest.fixture(autouse=True) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py new file mode 100644 index 000000000..6966eb68f --- /dev/null +++ b/tests/conftest_trades.py @@ -0,0 +1,162 @@ +import pytest + +from freqtrade.persistence.models import Order, Trade + + +def mock_trade_1(fee): + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=123.0, + amount_requested=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', + strategy='DefaultStrategy', + ) + o = Order.parse_from_ccxt_object({ + 'id': '1234', + 'symbol': 'ETH/BTC', + 'status': 'closed', + 'side': 'buy', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'buy') + trade.orders.append(o) + return trade + + +def mock_trade_2(fee): + """ + Closed trade... + """ + trade = Trade( + pair='ETC/BTC', + stake_amount=0.001, + amount=123.0, + amount_requested=123.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.123, + close_rate=0.128, + close_profit=0.005, + exchange='bittrex', + is_open=False, + open_order_id='dry_run_sell_12345', + strategy='DefaultStrategy', + ) + o = Order.parse_from_ccxt_object({ + 'id': '1235', + 'symbol': 'ETC/BTC', + 'status': 'closed', + 'side': 'buy', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'buy') + trade.orders.append(o) + o = Order.parse_from_ccxt_object({ + 'id': '12366', + 'symbol': 'ETC/BTC', + 'status': 'closed', + 'side': 'sell', + 'price': 0.128, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'sell') + trade.orders.append(o) + return trade + + +def mock_trade_3(fee): + """ + Closed trade + """ + trade = Trade( + pair='XRP/BTC', + stake_amount=0.001, + amount=123.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.05, + close_rate=0.06, + close_profit=0.01, + exchange='bittrex', + is_open=False, + ) + o = Order.parse_from_ccxt_object({ + 'id': '41231a12a', + 'symbol': 'XRP/BTC', + 'status': 'closed', + 'side': 'buy', + 'price': 0.05, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'buy') + trade.orders.append(o) + o = Order.parse_from_ccxt_object({ + 'id': '41231a666a', + 'symbol': 'XRP/BTC', + 'status': 'closed', + 'side': 'stop_loss', + 'price': 0.06, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'sell') + trade.orders.append(o) + return trade + + +def mock_trade_4(fee): + """ + Simulate prod entry + """ + trade = Trade( + pair='ETC/BTC', + stake_amount=0.001, + amount=123.0, + amount_requested=124.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.123, + exchange='bittrex', + open_order_id='prod_buy_12345', + strategy='DefaultStrategy', + ) + o = Order.parse_from_ccxt_object({ + 'id': 'prod_buy_12345', + 'symbol': 'ETC/BTC', + 'status': 'open', + 'side': 'buy', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'buy') + trade.orders.append(o) + + +def create_mock_trades(fee): + """ + Create some fake trades ... + """ + # Simulate dry_run entries + trade = mock_trade_1(fee) + Trade.session.add(trade) + + trade = mock_trade_2(fee) + Trade.session.add(trade) + + trade = mock_trade_3(fee) + Trade.session.add(trade) + + trade = mock_trade_4(fee) + Trade.session.add(trade) From da0ceb7d87314242b73edc400e4ea30d2e894874 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Sep 2020 06:48:34 +0200 Subject: [PATCH 124/224] Extract orders for mock trades --- tests/conftest.py | 2 +- tests/conftest_trades.py | 143 ++++++++++++++++++++++++--------------- 2 files changed, 88 insertions(+), 57 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2181388fa..b966cde5b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -170,7 +170,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: @pytest.fixture(scope='function') -def mock_trades(fee): +def mock_trades(init_persistence, fee): return create_mock_trades(fee) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 6966eb68f..b2aff2ee7 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -3,6 +3,20 @@ import pytest from freqtrade.persistence.models import Order, Trade +def mock_order_1(): + return { + 'id': '1234', + 'symbol': 'ETH/BTC', + 'status': 'closed', + 'side': 'buy', + 'type': 'limit', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + } + + def mock_trade_1(fee): trade = Trade( pair='ETH/BTC', @@ -16,18 +30,37 @@ def mock_trade_1(fee): open_order_id='dry_run_buy_12345', strategy='DefaultStrategy', ) - o = Order.parse_from_ccxt_object({ - 'id': '1234', - 'symbol': 'ETH/BTC', + o = Order.parse_from_ccxt_object(mock_order_1(), 'ETH/BTC', 'buy') + trade.orders.append(o) + return trade + + +def mock_order_2(): + return { + 'id': '1235', + 'symbol': 'ETC/BTC', 'status': 'closed', 'side': 'buy', + 'type': 'limit', 'price': 0.123, 'amount': 123.0, 'filled': 123.0, 'remaining': 0.0, - }, 'ETH/BTC', 'buy') - trade.orders.append(o) - return trade + } + + +def mock_order_2_sell(): + return { + 'id': '12366', + 'symbol': 'ETC/BTC', + 'status': 'closed', + 'side': 'sell', + 'type': 'limit', + 'price': 0.128, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + } def mock_trade_2(fee): @@ -49,31 +82,41 @@ def mock_trade_2(fee): open_order_id='dry_run_sell_12345', strategy='DefaultStrategy', ) - o = Order.parse_from_ccxt_object({ - 'id': '1235', - 'symbol': 'ETC/BTC', - 'status': 'closed', - 'side': 'buy', - 'price': 0.123, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_2(), 'ETH/BTC', 'buy') trade.orders.append(o) - o = Order.parse_from_ccxt_object({ - 'id': '12366', - 'symbol': 'ETC/BTC', - 'status': 'closed', - 'side': 'sell', - 'price': 0.128, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'sell') + o = Order.parse_from_ccxt_object(mock_order_2_sell(), 'ETH/BTC', 'sell') trade.orders.append(o) return trade +def mock_order_3(): + return { + 'id': '41231a12a', + 'symbol': 'XRP/BTC', + 'status': 'closed', + 'side': 'buy', + 'type': 'limit', + 'price': 0.05, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + } + + +def mock_order_3_sell(): + return { + 'id': '41231a666a', + 'symbol': 'XRP/BTC', + 'status': 'closed', + 'side': 'stop_loss', + 'type': 'limit', + 'price': 0.06, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + } + + def mock_trade_3(fee): """ Closed trade @@ -90,31 +133,27 @@ def mock_trade_3(fee): exchange='bittrex', is_open=False, ) - o = Order.parse_from_ccxt_object({ - 'id': '41231a12a', - 'symbol': 'XRP/BTC', - 'status': 'closed', - 'side': 'buy', - 'price': 0.05, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_3(), 'ETH/BTC', 'buy') trade.orders.append(o) - o = Order.parse_from_ccxt_object({ - 'id': '41231a666a', - 'symbol': 'XRP/BTC', - 'status': 'closed', - 'side': 'stop_loss', - 'price': 0.06, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'sell') + o = Order.parse_from_ccxt_object(mock_order_3_sell(), 'ETH/BTC', 'sell') trade.orders.append(o) return trade +def mock_order_4(): + return { + 'id': 'prod_buy_12345', + 'symbol': 'ETC/BTC', + 'status': 'open', + 'side': 'buy', + 'type': 'limit', + 'price': 0.123, + 'amount': 123.0, + 'filled': 0.0, + 'remaining': 123.0, + } + + def mock_trade_4(fee): """ Simulate prod entry @@ -131,17 +170,9 @@ def mock_trade_4(fee): open_order_id='prod_buy_12345', strategy='DefaultStrategy', ) - o = Order.parse_from_ccxt_object({ - 'id': 'prod_buy_12345', - 'symbol': 'ETC/BTC', - 'status': 'open', - 'side': 'buy', - 'price': 0.123, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_4(), 'ETH/BTC', 'buy') trade.orders.append(o) + return trade def create_mock_trades(fee): From f113b45036c39d216c6cfdca7702d816901b4f3b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Sep 2020 06:49:30 +0200 Subject: [PATCH 125/224] Refactor test to not duplicate order info --- tests/conftest.py | 23 ++++++++++++++++++----- tests/conftest_trades.py | 18 ------------------ tests/test_freqtradebot.py | 20 +++++++------------- 3 files changed, 25 insertions(+), 36 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b966cde5b..a63b4e314 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,4 @@ # pragma pylint: disable=missing-docstring -from tests.conftest_trades import create_mock_trades -from freqtrade.persistence.models import Order import json import logging import re @@ -24,6 +22,8 @@ from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker +from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, + mock_trade_4) logging.getLogger('').setLevel(logging.INFO) @@ -169,9 +169,22 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: freqtrade.exchange.refresh_latest_ohlcv = lambda p: None -@pytest.fixture(scope='function') -def mock_trades(init_persistence, fee): - return create_mock_trades(fee) +def create_mock_trades(fee): + """ + Create some fake trades ... + """ + # Simulate dry_run entries + trade = mock_trade_1(fee) + Trade.session.add(trade) + + trade = mock_trade_2(fee) + Trade.session.add(trade) + + trade = mock_trade_3(fee) + Trade.session.add(trade) + + trade = mock_trade_4(fee) + Trade.session.add(trade) @pytest.fixture(autouse=True) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index b2aff2ee7..1990725ee 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -173,21 +173,3 @@ def mock_trade_4(fee): o = Order.parse_from_ccxt_object(mock_order_4(), 'ETH/BTC', 'buy') trade.orders.append(o) return trade - - -def create_mock_trades(fee): - """ - Create some fake trades ... - """ - # Simulate dry_run entries - trade = mock_trade_1(fee) - Trade.session.add(trade) - - trade = mock_trade_2(fee) - Trade.session.add(trade) - - trade = mock_trade_3(fee) - Trade.session.add(trade) - - trade = mock_trade_4(fee) - Trade.session.add(trade) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0f340ae8f..0d2e70f1e 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1,7 +1,6 @@ # pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments -from freqtrade.persistence.models import Order import logging import time from copy import deepcopy @@ -13,11 +12,13 @@ import pytest from freqtrade.constants import (CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT) -from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, +from freqtrade.exceptions import (DependencyException, ExchangeError, + InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade +from freqtrade.persistence.models import Order from freqtrade.rpc import RPCMessageType from freqtrade.state import RunMode, State from freqtrade.strategy.interface import SellCheckTuple, SellType @@ -26,6 +27,7 @@ from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, patch_wallet, patch_whitelist) +from tests.conftest_trades import mock_order_4 def patch_RPCManager(mocker) -> MagicMock: @@ -4241,18 +4243,10 @@ def test_update_open_orders(mocker, default_conf, fee, caplog): caplog.clear() assert len(Order.get_open_orders()) == 1 - - matching_buy_order = { - 'id': 'prod_buy_12345', - 'symbol': 'ETC/BTC', + matching_buy_order = mock_order_4() + matching_buy_order.update({ 'status': 'closed', - 'side': 'buy', - 'type': 'limit', - 'price': 0.123, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - } + }) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=matching_buy_order) freqtrade.update_open_orders() assert len(Order.get_open_orders()) == 0 From 26a5cc5959de01f1a3c410409ddf12db461f0f9d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Sep 2020 07:41:58 +0200 Subject: [PATCH 126/224] Add return-type for select_order --- freqtrade/persistence/models.py | 2 +- tests/conftest_trades.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 096cf6209..79b3d491b 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -520,7 +520,7 @@ class Trade(_DECL_BASE): profit_ratio = (close_trade_price / self.open_trade_price) - 1 return float(f"{profit_ratio:.8f}") - def select_order(self, order_side: str, status: Optional[str]): + def select_order(self, order_side: str, status: Optional[str]) -> Optional[Order]: """ Returns latest order for this orderside and status Returns None if nothing is found diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 1990725ee..723a3fe58 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -108,9 +108,10 @@ def mock_order_3_sell(): 'id': '41231a666a', 'symbol': 'XRP/BTC', 'status': 'closed', - 'side': 'stop_loss', - 'type': 'limit', + 'side': 'sell', + 'type': 'stop_loss_limit', 'price': 0.06, + 'average': 0.06, 'amount': 123.0, 'filled': 123.0, 'remaining': 0.0, From 6518e7a7892e4b71771fe540e5a18ef8a5438fb4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Sep 2020 07:47:38 +0200 Subject: [PATCH 127/224] Add test for update_closed_trades_without_fees --- tests/test_freqtradebot.py | 51 +++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0d2e70f1e..b6e5c80c8 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -27,7 +27,7 @@ from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, patch_wallet, patch_whitelist) -from tests.conftest_trades import mock_order_4 +from tests.conftest_trades import mock_order_1, mock_order_2, mock_order_2_sell, mock_order_3, mock_order_3_sell, mock_order_4 def patch_RPCManager(mocker) -> MagicMock: @@ -4250,3 +4250,52 @@ def test_update_open_orders(mocker, default_conf, fee, caplog): mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=matching_buy_order) freqtrade.update_open_orders() assert len(Order.get_open_orders()) == 0 + + +@pytest.mark.usefixtures("init_persistence") +def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + def patch_with_fee(order): + order.update({'fee': {'cost': 0.1, 'rate': 0.2, + 'currency': order['symbol'].split('/')[0]}}) + return order + + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + side_effect=[ + patch_with_fee(mock_order_2_sell()), + patch_with_fee(mock_order_3_sell()), + patch_with_fee(mock_order_1()), + patch_with_fee(mock_order_2()), + patch_with_fee(mock_order_3()), + patch_with_fee(mock_order_4()), + ] + ) + + create_mock_trades(fee) + trades = Trade.get_trades().all() + assert len(trades) == 4 + for trade in trades: + assert trade.fee_open_cost is None + assert trade.fee_open_currency is None + assert trade.fee_close_cost is None + assert trade.fee_close_currency is None + + freqtrade.update_closed_trades_without_assigned_fees() + + # trades = Trade.get_trades().all() + assert len(trades) == 4 + + for trade in trades: + if trade.is_open: + # Exclude Trade 4 - as the order is still open. + if trade.select_order('buy', 'closed'): + assert trade.fee_open_cost is not None + assert trade.fee_open_currency is not None + else: + assert trade.fee_open_cost is None + assert trade.fee_open_currency is None + + else: + assert trade.fee_close_cost is not None + assert trade.fee_close_currency is not None From f3e0370d4da308e40ecf5cf21b2844c4d1166de0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Sep 2020 07:54:55 +0200 Subject: [PATCH 128/224] Stylistic fixes --- tests/conftest_trades.py | 2 -- tests/test_freqtradebot.py | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 723a3fe58..c990f6cdc 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -1,5 +1,3 @@ -import pytest - from freqtrade.persistence.models import Order, Trade diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index b6e5c80c8..f9bf37938 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -27,7 +27,9 @@ from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, patch_wallet, patch_whitelist) -from tests.conftest_trades import mock_order_1, mock_order_2, mock_order_2_sell, mock_order_3, mock_order_3_sell, mock_order_4 +from tests.conftest_trades import (mock_order_1, mock_order_2, + mock_order_2_sell, mock_order_3, + mock_order_3_sell, mock_order_4) def patch_RPCManager(mocker) -> MagicMock: From 7852feab0566ecdda19e705cd88c7c7bd61cd52b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Sep 2020 09:06:43 +0200 Subject: [PATCH 129/224] support smaller timeframes --- docs/strategy-customization.md | 6 +++++- freqtrade/strategy/strategy_helper.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 7396f2a89..a1de1044c 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -521,7 +521,7 @@ class SampleStrategy(IStrategy): # use ffill to have the 1d value available in every row throughout the day. # Without this, comparisons between columns of the original and the informative pair would only work once per day. # Full documentation of this method, see below - dataframe = merge_informative_pair(dataframe, informative_pairs, self.timeframe, inf_tf, ffill=True) + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, inf_tf, ffill=True) # Calculate rsi of the original dataframe (5m timeframe) dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) @@ -601,6 +601,10 @@ All columns of the informative dataframe will be available on the returning data ``` +!!! Warning "Informative timeframe < timeframe" + Using informative timeframes smaller than the dataframe timeframe is not recommended with this method, as it will not use any of the additional information this would provide. + To use the more detailed information properly, more advanced methods should be applied (which are out of scope for freqtrade documentation, as it'll depend on the respective need). + *** ## Additional data (Wallets) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 1fbf618bd..1a5b2d0f8 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -26,7 +26,8 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, """ minutes_inf = timeframe_to_minutes(timeframe_inf) - if timeframe == timeframe_inf: + minutes = timeframe_to_minutes(timeframe) + if minutes >= minutes_inf: # No need to forwardshift if the timeframes are identical informative['date_merge'] = informative["date"] else: From 014fcb36f4e6b3da535dcc7ebaa604ea47f9e2d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2020 07:09:07 +0000 Subject: [PATCH 130/224] Bump mkdocs-material from 5.5.11 to 5.5.12 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.5.11 to 5.5.12. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/5.5.11...5.5.12) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index c8f08d12a..6408616a0 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==5.5.11 +mkdocs-material==5.5.12 mdx_truly_sane_lists==1.2 From 534404c284a3de1b8bc9b6cb4f8e9603c32f81c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2020 07:09:22 +0000 Subject: [PATCH 131/224] Bump ccxt from 1.33.72 to 1.34.3 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.33.72 to 1.34.3. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.33.72...1.34.3) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index f305d8793..58912bc0c 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.33.72 +ccxt==1.34.3 SQLAlchemy==1.3.19 python-telegram-bot==12.8 arrow==0.16.0 From ff0e73a9e592e4c61eca8fce97b0c84d5b886844 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2020 07:09:23 +0000 Subject: [PATCH 132/224] Bump scikit-optimize from 0.7.4 to 0.8.1 Bumps [scikit-optimize](https://github.com/scikit-optimize/scikit-optimize) from 0.7.4 to 0.8.1. - [Release notes](https://github.com/scikit-optimize/scikit-optimize/releases) - [Changelog](https://github.com/scikit-optimize/scikit-optimize/blob/master/CHANGELOG.md) - [Commits](https://github.com/scikit-optimize/scikit-optimize/compare/v0.7.4...v0.8.1) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index fbc679eaa..ea24196b9 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -4,7 +4,7 @@ # Required for hyperopt scipy==1.5.2 scikit-learn==0.23.1 -scikit-optimize==0.7.4 +scikit-optimize==0.8.1 filelock==3.0.12 joblib==0.16.0 progressbar2==3.52.1 From f20318fad1b27039c8269984f4ea78cecb1aab90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2020 07:37:12 +0000 Subject: [PATCH 133/224] Bump scikit-learn from 0.23.1 to 0.23.2 Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 0.23.1 to 0.23.2. - [Release notes](https://github.com/scikit-learn/scikit-learn/releases) - [Commits](https://github.com/scikit-learn/scikit-learn/compare/0.23.1...0.23.2) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index ea24196b9..4d884b4fe 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -3,7 +3,7 @@ # Required for hyperopt scipy==1.5.2 -scikit-learn==0.23.1 +scikit-learn==0.23.2 scikit-optimize==0.8.1 filelock==3.0.12 joblib==0.16.0 From f63a3789672b7a5e58ce264688e26f895f26d747 Mon Sep 17 00:00:00 2001 From: Allen Day Date: Mon, 7 Sep 2020 23:26:55 +0800 Subject: [PATCH 134/224] Update hyperopt.py zero pad wins/draws/losses (W/D/L) column to preserve alignment in console pretty print --- freqtrade/optimize/hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b9db3c09a..6d29be08e 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -574,7 +574,7 @@ class Hyperopt: 'wins': wins, 'draws': draws, 'losses': losses, - 'winsdrawslosses': f"{wins}/{draws}/{losses}", + 'winsdrawslosses': f"{wins:04}/{draws:04}/{losses:04}", 'avg_profit': backtesting_results.profit_percent.mean() * 100.0, 'median_profit': backtesting_results.profit_percent.median() * 100.0, 'total_profit': backtesting_results.profit_abs.sum(), From 3fe2ed0e189a2843c570e84395aea3c99e5b8af8 Mon Sep 17 00:00:00 2001 From: Allen Day Date: Mon, 7 Sep 2020 23:38:51 +0800 Subject: [PATCH 135/224] zero pad in test --- tests/optimize/test_hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index bb6f043e7..e7a26cd5f 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -813,7 +813,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'draws': 0, 'duration': 100.0, 'losses': 0, - 'winsdrawslosses': '1/0/0', + 'winsdrawslosses': '0001/0000/0000', 'median_profit': 2.3117, 'profit': 2.3117, 'total_profit': 0.000233, From 8af610b543a82c8ebef54aeeaf834d092dceeea1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Sep 2020 06:40:40 +0200 Subject: [PATCH 136/224] Add Test for reupdate_buy_order_fees --- tests/test_freqtradebot.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index f9bf37938..aef9b5f29 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4301,3 +4301,40 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): else: assert trade.fee_close_cost is not None assert trade.fee_close_currency is not None + + +@pytest.mark.usefixtures("init_persistence") +def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): + freqtrade = get_patched_freqtradebot(mocker, default_conf) + mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') + + create_mock_trades(fee) + trades = Trade.get_trades().all() + + freqtrade.reupdate_buy_order_fees(trades[0]) + assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) + assert mock_uts.call_count == 1 + assert mock_uts.call_args_list[0][0][0] == trades[0] + assert mock_uts.call_args_list[0][0][1] == '1234' + assert log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog) + mock_uts.reset_mock() + caplog.clear() + + # Test with trade without orders + trade = Trade( + pair='XRP/ETH', + stake_amount=0.001, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_date=arrow.utcnow().datetime, + is_open=True, + amount=20, + open_rate=0.01, + exchange='bittrex', + ) + Trade.session.add(trade) + + freqtrade.reupdate_buy_order_fees(trade) + assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) + assert mock_uts.call_count == 0 + assert not log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog) From caf0476717c90f1c98d07fb6875ee4a60eeeda0d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Sep 2020 06:46:38 +0200 Subject: [PATCH 137/224] Add test for handle_insufficient_funds --- tests/test_freqtradebot.py | 64 +++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index aef9b5f29..c95a085a6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4315,7 +4315,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) assert mock_uts.call_count == 1 assert mock_uts.call_args_list[0][0][0] == trades[0] - assert mock_uts.call_args_list[0][0][1] == '1234' + assert mock_uts.call_args_list[0][0][1] == mock_order_1()['id'] assert log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog) mock_uts.reset_mock() caplog.clear() @@ -4338,3 +4338,65 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) assert mock_uts.call_count == 0 assert not log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog) + + +@pytest.mark.usefixtures("init_persistence") +def test_handle_insufficient_funds(mocker, default_conf, fee): + freqtrade = get_patched_freqtradebot(mocker, default_conf) + mock_rlo = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.refind_lost_order') + mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_buy_order_fees') + create_mock_trades(fee) + trades = Trade.get_trades().all() + + # Trade 0 has only a open buy order, no closed order + freqtrade.handle_insufficient_funds(trades[0]) + assert mock_rlo.call_count == 0 + assert mock_bof.call_count == 1 + + mock_rlo.reset_mock() + mock_bof.reset_mock() + + # Trade 1 has closed buy and sell orders + freqtrade.handle_insufficient_funds(trades[1]) + assert mock_rlo.call_count == 1 + assert mock_bof.call_count == 0 + + mock_rlo.reset_mock() + mock_bof.reset_mock() + + # Trade 2 has closed buy and sell orders + freqtrade.handle_insufficient_funds(trades[2]) + assert mock_rlo.call_count == 1 + assert mock_bof.call_count == 0 + + mock_rlo.reset_mock() + mock_bof.reset_mock() + + # Trade 3 has an opne buy order + freqtrade.handle_insufficient_funds(trades[3]) + assert mock_rlo.call_count == 0 + assert mock_bof.call_count == 1 + + +@pytest.mark.usefixtures("init_persistence") +def test_refind_lost_order(mocker, default_conf, fee, caplog): + freqtrade = get_patched_freqtradebot(mocker, default_conf) + mock_rlo = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.refind_lost_order') + mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_buy_order_fees') + + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + side_effect=[ + mock_order_2_sell(), + mock_order_3_sell(), + mock_order_1(), + mock_order_2(), + mock_order_3(), + mock_order_4(), + ]) + + create_mock_trades(fee) + trades = Trade.get_trades().all() + + # freqtrade.refind_lost_order(trades[0]) + + # TODO: Implement test here. From 98840eef3cd9d7f1008b95f4dad8b36a9d3ab66b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Sep 2020 07:01:43 +0200 Subject: [PATCH 138/224] Add 5th mock trade --- tests/commands/test_commands.py | 2 +- tests/conftest.py | 5 +++- tests/conftest_trades.py | 51 +++++++++++++++++++++++++++++++++ tests/data/test_btanalysis.py | 2 +- tests/test_freqtradebot.py | 13 +++++---- tests/test_persistence.py | 4 +-- 6 files changed, 66 insertions(+), 11 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 26875fb7f..290a3fa77 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1116,7 +1116,7 @@ def test_show_trades(mocker, fee, capsys, caplog): pargs = get_args(args) pargs['config'] = None start_show_trades(pargs) - assert log_has("Printing 4 Trades: ", caplog) + assert log_has("Printing 5 Trades: ", caplog) captured = capsys.readouterr() assert "Trade(id=1" in captured.out assert "Trade(id=2" in captured.out diff --git a/tests/conftest.py b/tests/conftest.py index a63b4e314..1ef495301 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,7 @@ from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, - mock_trade_4) + mock_trade_4, mock_trade_5) logging.getLogger('').setLevel(logging.INFO) @@ -186,6 +186,9 @@ def create_mock_trades(fee): trade = mock_trade_4(fee) Trade.session.add(trade) + trade = mock_trade_5(fee) + Trade.session.add(trade) + @pytest.fixture(autouse=True) def patch_coingekko(mocker) -> None: diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index c990f6cdc..d3e19603b 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -172,3 +172,54 @@ def mock_trade_4(fee): o = Order.parse_from_ccxt_object(mock_order_4(), 'ETH/BTC', 'buy') trade.orders.append(o) return trade + + +def mock_order_5(): + return { + 'id': 'prod_buy_3455', + 'symbol': 'XRP/BTC', + 'status': 'closed', + 'side': 'buy', + 'type': 'limit', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + } + + +def mock_order_5_stoploss(): + return { + 'id': 'prod_stoploss_3455', + 'symbol': 'XRP/BTC', + 'status': 'open', + 'side': 'sell', + 'type': 'stop_loss_limit', + 'price': 0.123, + 'amount': 123.0, + 'filled': 0.0, + 'remaining': 123.0, + } + + +def mock_trade_5(fee): + """ + Simulate prod entry with stoploss + """ + trade = Trade( + pair='XRP/BTC', + stake_amount=0.001, + amount=123.0, + amount_requested=124.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.123, + exchange='bittrex', + strategy='SampleStrategy', + stoploss_order_id='prod_stoploss_3455' + ) + o = Order.parse_from_ccxt_object(mock_order_5(), 'ETH/BTC', 'buy') + trade.orders.append(o) + o = Order.parse_from_ccxt_object(mock_order_5_stoploss(), 'ETH/BTC', 'sell') + trade.orders.append(o) + return trade diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index e2ca66bd8..3b5672e65 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -110,7 +110,7 @@ def test_load_trades_from_db(default_conf, fee, mocker): trades = load_trades_from_db(db_url=default_conf['db_url']) assert init_mock.call_count == 1 - assert len(trades) == 4 + assert len(trades) == 5 assert isinstance(trades, DataFrame) assert "pair" in trades.columns assert "open_date" in trades.columns diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c95a085a6..883c63737 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4213,7 +4213,7 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi freqtrade = get_patched_freqtradebot(mocker, default_conf) create_mock_trades(fee) trades = Trade.query.all() - assert len(trades) == 4 + assert len(trades) == 5 freqtrade.cancel_all_open_orders() assert buy_mock.call_count == 1 assert sell_mock.call_count == 1 @@ -4244,14 +4244,15 @@ def test_update_open_orders(mocker, default_conf, fee, caplog): assert log_has_re(r"Error updating Order .*", caplog) caplog.clear() - assert len(Order.get_open_orders()) == 1 + assert len(Order.get_open_orders()) == 2 matching_buy_order = mock_order_4() matching_buy_order.update({ 'status': 'closed', }) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=matching_buy_order) freqtrade.update_open_orders() - assert len(Order.get_open_orders()) == 0 + # Only stoploss order is kept open + assert len(Order.get_open_orders()) == 1 @pytest.mark.usefixtures("init_persistence") @@ -4276,7 +4277,7 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): create_mock_trades(fee) trades = Trade.get_trades().all() - assert len(trades) == 4 + assert len(trades) == 5 for trade in trades: assert trade.fee_open_cost is None assert trade.fee_open_currency is None @@ -4285,8 +4286,8 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): freqtrade.update_closed_trades_without_assigned_fees() - # trades = Trade.get_trades().all() - assert len(trades) == 4 + trades = Trade.get_trades().all() + assert len(trades) == 5 for trade in trades: if trade.is_open: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 788debace..48b918128 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -734,7 +734,7 @@ def test_adjust_min_max_rates(fee): def test_get_open(default_conf, fee): create_mock_trades(fee) - assert len(Trade.get_open_trades()) == 2 + assert len(Trade.get_open_trades()) == 3 @pytest.mark.usefixtures("init_persistence") @@ -1004,7 +1004,7 @@ def test_total_open_trades_stakes(fee): assert res == 0 create_mock_trades(fee) res = Trade.total_open_trades_stakes() - assert res == 0.002 + assert res == 0.003 @pytest.mark.usefixtures("init_persistence") From 25938efee665389eb8c013b942205dceaf1e23d0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Sep 2020 07:50:52 +0200 Subject: [PATCH 139/224] Add partial test for refind_order --- freqtrade/freqtradebot.py | 2 +- tests/conftest_trades.py | 2 +- tests/test_freqtradebot.py | 51 +++++++++++++++++++++++++++----------- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1af0e85b7..c20a57205 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -333,7 +333,7 @@ class FreqtradeBot: stoploss_order=order.ft_order_side == 'stoploss') except ExchangeError: - logger.warning(f"Error updating {order.order_id}") + logger.warning(f"Error updating {order.order_id}.") # # BUY / enter positions / open trades logic and methods diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index d3e19603b..03af7fa1d 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -220,6 +220,6 @@ def mock_trade_5(fee): ) o = Order.parse_from_ccxt_object(mock_order_5(), 'ETH/BTC', 'buy') trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_5_stoploss(), 'ETH/BTC', 'sell') + o = Order.parse_from_ccxt_object(mock_order_5_stoploss(), 'ETH/BTC', 'stoploss') trade.orders.append(o) return trade diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 883c63737..625c6ed7e 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -29,7 +29,7 @@ from tests.conftest import (create_mock_trades, get_patched_freqtradebot, patch_wallet, patch_whitelist) from tests.conftest_trades import (mock_order_1, mock_order_2, mock_order_2_sell, mock_order_3, - mock_order_3_sell, mock_order_4) + mock_order_3_sell, mock_order_4, mock_order_5_stoploss) def patch_RPCManager(mocker) -> MagicMock: @@ -4382,22 +4382,45 @@ def test_handle_insufficient_funds(mocker, default_conf, fee): @pytest.mark.usefixtures("init_persistence") def test_refind_lost_order(mocker, default_conf, fee, caplog): freqtrade = get_patched_freqtradebot(mocker, default_conf) - mock_rlo = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.refind_lost_order') - mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_buy_order_fees') + mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') - mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', - side_effect=[ - mock_order_2_sell(), - mock_order_3_sell(), - mock_order_1(), - mock_order_2(), - mock_order_3(), - mock_order_4(), - ]) + mock_fo = mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order') create_mock_trades(fee) trades = Trade.get_trades().all() - # freqtrade.refind_lost_order(trades[0]) + caplog.clear() - # TODO: Implement test here. + # No open order + freqtrade.refind_lost_order(trades[0]) + order = mock_order_1() + assert log_has_re(r"Order Order(.*order_id=" + order['id'] + ".*) is no longer open.", caplog) + assert mock_fo.call_count == 0 + assert mock_uts.call_count == 0 + + caplog.clear() + mock_fo.reset_mock() + + # Open buy order + freqtrade.refind_lost_order(trades[3]) + order = mock_order_4() + assert log_has_re(r"Trying to refind Order\(.*", caplog) + assert mock_fo.call_count == 0 + assert mock_uts.call_count == 0 + + caplog.clear() + mock_fo.reset_mock() + + # Open stoploss order + freqtrade.refind_lost_order(trades[4]) + order = mock_order_5_stoploss() + assert log_has_re(r"Trying to refind Order\(.*", caplog) + assert mock_fo.call_count == 1 + assert mock_uts.call_count == 1 + + # Test error case + mock_fo = mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + side_effect=ExchangeError()) + freqtrade.refind_lost_order(trades[4]) + caplog.clear() + assert log_has(f"Error updating {order['id']}.", caplog) From 083c358044d53b2303f2dbbeb260d2b1fad09dd9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Sep 2020 07:57:02 +0200 Subject: [PATCH 140/224] Fix wrong sequence in test --- tests/test_freqtradebot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 625c6ed7e..542206e62 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4418,9 +4418,10 @@ def test_refind_lost_order(mocker, default_conf, fee, caplog): assert mock_fo.call_count == 1 assert mock_uts.call_count == 1 + caplog.clear() + # Test error case mock_fo = mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', side_effect=ExchangeError()) freqtrade.refind_lost_order(trades[4]) - caplog.clear() assert log_has(f"Error updating {order['id']}.", caplog) From 4cf66e2fba76d3aab734bffe94063ca906c74bbc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2020 13:29:22 +0000 Subject: [PATCH 141/224] Bump progressbar2 from 3.52.1 to 3.53.1 Bumps [progressbar2](https://github.com/WoLpH/python-progressbar) from 3.52.1 to 3.53.1. - [Release notes](https://github.com/WoLpH/python-progressbar/releases) - [Changelog](https://github.com/WoLpH/python-progressbar/blob/develop/CHANGES.rst) - [Commits](https://github.com/WoLpH/python-progressbar/compare/v3.52.1...v3.53.1) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 4d884b4fe..b47331aa3 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -7,4 +7,4 @@ scikit-learn==0.23.2 scikit-optimize==0.8.1 filelock==3.0.12 joblib==0.16.0 -progressbar2==3.52.1 +progressbar2==3.53.1 From 986e767d6c4ec460cc35b95db178e19bbef9de2b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2020 13:29:27 +0000 Subject: [PATCH 142/224] Bump blosc from 1.9.1 to 1.9.2 Bumps [blosc](https://github.com/blosc/python-blosc) from 1.9.1 to 1.9.2. - [Release notes](https://github.com/blosc/python-blosc/releases) - [Changelog](https://github.com/Blosc/python-blosc/blob/master/RELEASE_NOTES.rst) - [Commits](https://github.com/blosc/python-blosc/compare/v1.9.1...v1.9.2) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 58912bc0c..5efc6a2ee 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -14,7 +14,7 @@ tabulate==0.8.7 pycoingecko==1.3.0 jinja2==2.11.2 tables==3.6.1 -blosc==1.9.1 +blosc==1.9.2 # find first, C search in arrays py_find_1st==1.1.4 From d8dae46544987a2f5d672d1128851d47d5f6d9bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2020 13:29:31 +0000 Subject: [PATCH 143/224] Bump ccxt from 1.34.3 to 1.34.11 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.34.3 to 1.34.11. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.34.3...1.34.11) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 58912bc0c..faa022c4c 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.34.3 +ccxt==1.34.11 SQLAlchemy==1.3.19 python-telegram-bot==12.8 arrow==0.16.0 From 8c97b83b8c44de9f7b921cba6bec3bdeb39e5527 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2020 13:29:36 +0000 Subject: [PATCH 144/224] Bump pandas from 1.1.1 to 1.1.2 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.1.1 to 1.1.2. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.1.1...v1.1.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 66f4cbc5f..a1a1fb250 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ -r requirements-common.txt numpy==1.19.1 -pandas==1.1.1 +pandas==1.1.2 From 4480b3b39374439296407128b816a797749865c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Sep 2020 15:39:35 +0200 Subject: [PATCH 145/224] Fix error in documentation (wrong sequence of steps) --- docs/strategy-customization.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index a1de1044c..615be0247 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -580,10 +580,6 @@ All columns of the informative dataframe will be available on the returning data A custom implementation for this is possible, and can be done as follows: ``` python - # Rename columns to be unique - informative.columns = [f"{col}_{inf_tf}" for col in informative.columns] - # Assuming inf_tf = '1d' - then the columns will now be: - # date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d # Shift date by 1 candle # This is necessary since the data is always the "open date" @@ -592,6 +588,11 @@ All columns of the informative dataframe will be available on the returning data # Only do this if the timeframes are different: informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes, 'm') + # Rename columns to be unique + informative.columns = [f"{col}_{inf_tf}" for col in informative.columns] + # Assuming inf_tf = '1d' - then the columns will now be: + # date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d + # Combine the 2 dataframes # all indicators on the informative sample MUST be calculated before this point dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_merge_{inf_tf}', how='left') From c3e0397743a61227c741585fc5c97bb45a486cee Mon Sep 17 00:00:00 2001 From: Blackhawke Date: Wed, 9 Sep 2020 09:16:11 -0700 Subject: [PATCH 146/224] Added full "source" command to virtualenv in easy install --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index c03be55d1..da82d632f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -63,7 +63,7 @@ With this option, the script will install the bot and most dependencies: You will need to have git and python3.6+ installed beforehand for this to work. * Mandatory software as: `ta-lib` -* Setup your virtualenv under `.env/` +* Setup your virtualenv: `source .env/bin/activate` This option is a combination of installation tasks, `--reset` and `--config`. From 3c521f55b234bd1f7109342c963d72cdf5fec9a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Sep 2020 07:40:19 +0200 Subject: [PATCH 147/224] Add 6th mock trade --- tests/commands/test_commands.py | 3 +- tests/conftest.py | 5 ++- tests/conftest_trades.py | 68 +++++++++++++++++++++++++++++---- tests/data/test_btanalysis.py | 3 +- tests/test_freqtradebot.py | 18 ++++----- tests/test_persistence.py | 6 +-- 6 files changed, 81 insertions(+), 22 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 290a3fa77..a35fc9fb8 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -18,6 +18,7 @@ from freqtrade.state import RunMode from tests.conftest import (create_mock_trades, get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) +from tests.conftest_trades import MOCK_TRADE_COUNT def test_setup_utils_configuration(): @@ -1116,7 +1117,7 @@ def test_show_trades(mocker, fee, capsys, caplog): pargs = get_args(args) pargs['config'] = None start_show_trades(pargs) - assert log_has("Printing 5 Trades: ", caplog) + assert log_has(f"Printing {MOCK_TRADE_COUNT} Trades: ", caplog) captured = capsys.readouterr() assert "Trade(id=1" in captured.out assert "Trade(id=2" in captured.out diff --git a/tests/conftest.py b/tests/conftest.py index 1ef495301..fe55c8784 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,7 @@ from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, - mock_trade_4, mock_trade_5) + mock_trade_4, mock_trade_5, mock_trade_6) logging.getLogger('').setLevel(logging.INFO) @@ -189,6 +189,9 @@ def create_mock_trades(fee): trade = mock_trade_5(fee) Trade.session.add(trade) + trade = mock_trade_6(fee) + Trade.session.add(trade) + @pytest.fixture(autouse=True) def patch_coingekko(mocker) -> None: diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 03af7fa1d..43bf15e51 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -1,6 +1,9 @@ from freqtrade.persistence.models import Order, Trade +MOCK_TRADE_COUNT = 6 + + def mock_order_1(): return { 'id': '1234', @@ -80,9 +83,9 @@ def mock_trade_2(fee): open_order_id='dry_run_sell_12345', strategy='DefaultStrategy', ) - o = Order.parse_from_ccxt_object(mock_order_2(), 'ETH/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_2(), 'ETC/BTC', 'buy') trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_2_sell(), 'ETH/BTC', 'sell') + o = Order.parse_from_ccxt_object(mock_order_2_sell(), 'ETC/BTC', 'sell') trade.orders.append(o) return trade @@ -132,9 +135,9 @@ def mock_trade_3(fee): exchange='bittrex', is_open=False, ) - o = Order.parse_from_ccxt_object(mock_order_3(), 'ETH/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_3(), 'XRP/BTC', 'buy') trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_3_sell(), 'ETH/BTC', 'sell') + o = Order.parse_from_ccxt_object(mock_order_3_sell(), 'XRP/BTC', 'sell') trade.orders.append(o) return trade @@ -169,7 +172,7 @@ def mock_trade_4(fee): open_order_id='prod_buy_12345', strategy='DefaultStrategy', ) - o = Order.parse_from_ccxt_object(mock_order_4(), 'ETH/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_4(), 'ETC/BTC', 'buy') trade.orders.append(o) return trade @@ -218,8 +221,59 @@ def mock_trade_5(fee): strategy='SampleStrategy', stoploss_order_id='prod_stoploss_3455' ) - o = Order.parse_from_ccxt_object(mock_order_5(), 'ETH/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_5(), 'XRP/BTC', 'buy') trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_5_stoploss(), 'ETH/BTC', 'stoploss') + o = Order.parse_from_ccxt_object(mock_order_5_stoploss(), 'XRP/BTC', 'stoploss') + trade.orders.append(o) + return trade + + +def mock_order_6(): + return { + 'id': 'prod_buy_6', + 'symbol': 'LTC/BTC', + 'status': 'closed', + 'side': 'buy', + 'type': 'limit', + 'price': 0.15, + 'amount': 2.0, + 'filled': 2.0, + 'remaining': 0.0, + } + + +def mock_order_6_sell(): + return { + 'id': 'prod_sell_6', + 'symbol': 'LTC/BTC', + 'status': 'open', + 'side': 'sell', + 'type': 'limit', + 'price': 0.20, + 'amount': 2.0, + 'filled': 0.0, + 'remaining': 2.0, + } + + +def mock_trade_6(fee): + """ + Simulate prod entry with open sell order + """ + trade = Trade( + pair='LTC/BTC', + stake_amount=0.001, + amount=2.0, + amount_requested=2.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.15, + exchange='bittrex', + strategy='SampleStrategy', + open_order_id="prod_sell_6", + ) + o = Order.parse_from_ccxt_object(mock_order_6(), 'LTC/BTC', 'buy') + trade.orders.append(o) + o = Order.parse_from_ccxt_object(mock_order_6_sell(), 'LTC/BTC', 'stoploss') trade.orders.append(o) return trade diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 3b5672e65..564dae0b1 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -20,6 +20,7 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, from freqtrade.data.history import load_data, load_pair_history from freqtrade.optimize.backtesting import BacktestResult from tests.conftest import create_mock_trades +from tests.conftest_trades import MOCK_TRADE_COUNT def test_get_latest_backtest_filename(testdatadir, mocker): @@ -110,7 +111,7 @@ def test_load_trades_from_db(default_conf, fee, mocker): trades = load_trades_from_db(db_url=default_conf['db_url']) assert init_mock.call_count == 1 - assert len(trades) == 5 + assert len(trades) == MOCK_TRADE_COUNT assert isinstance(trades, DataFrame) assert "pair" in trades.columns assert "open_date" in trades.columns diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 542206e62..ace44bfae 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -27,7 +27,7 @@ from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, patch_wallet, patch_whitelist) -from tests.conftest_trades import (mock_order_1, mock_order_2, +from tests.conftest_trades import (MOCK_TRADE_COUNT, mock_order_1, mock_order_2, mock_order_2_sell, mock_order_3, mock_order_3_sell, mock_order_4, mock_order_5_stoploss) @@ -4206,17 +4206,17 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order_ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limit_sell_order): default_conf['cancel_open_orders_on_exit'] = True mocker.patch('freqtrade.exchange.Exchange.fetch_order', - side_effect=[ExchangeError(), limit_sell_order, limit_buy_order]) + side_effect=[ExchangeError(), limit_sell_order, limit_buy_order, limit_sell_order, ]) buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_buy') sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_sell') freqtrade = get_patched_freqtradebot(mocker, default_conf) create_mock_trades(fee) trades = Trade.query.all() - assert len(trades) == 5 + assert len(trades) == MOCK_TRADE_COUNT freqtrade.cancel_all_open_orders() assert buy_mock.call_count == 1 - assert sell_mock.call_count == 1 + assert sell_mock.call_count == 2 @pytest.mark.usefixtures("init_persistence") @@ -4244,15 +4244,15 @@ def test_update_open_orders(mocker, default_conf, fee, caplog): assert log_has_re(r"Error updating Order .*", caplog) caplog.clear() - assert len(Order.get_open_orders()) == 2 + assert len(Order.get_open_orders()) == 3 matching_buy_order = mock_order_4() matching_buy_order.update({ 'status': 'closed', }) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=matching_buy_order) freqtrade.update_open_orders() - # Only stoploss order is kept open - assert len(Order.get_open_orders()) == 1 + # Only stoploss and sell orders are kept open + assert len(Order.get_open_orders()) == 2 @pytest.mark.usefixtures("init_persistence") @@ -4277,7 +4277,7 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): create_mock_trades(fee) trades = Trade.get_trades().all() - assert len(trades) == 5 + assert len(trades) == MOCK_TRADE_COUNT for trade in trades: assert trade.fee_open_cost is None assert trade.fee_open_currency is None @@ -4287,7 +4287,7 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): freqtrade.update_closed_trades_without_assigned_fees() trades = Trade.get_trades().all() - assert len(trades) == 5 + assert len(trades) == MOCK_TRADE_COUNT for trade in trades: if trade.is_open: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 48b918128..d2da1c6a2 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -731,10 +731,10 @@ def test_adjust_min_max_rates(fee): @pytest.mark.usefixtures("init_persistence") -def test_get_open(default_conf, fee): +def test_get_open(fee): create_mock_trades(fee) - assert len(Trade.get_open_trades()) == 3 + assert len(Trade.get_open_trades()) == 4 @pytest.mark.usefixtures("init_persistence") @@ -1004,7 +1004,7 @@ def test_total_open_trades_stakes(fee): assert res == 0 create_mock_trades(fee) res = Trade.total_open_trades_stakes() - assert res == 0.003 + assert res == 0.004 @pytest.mark.usefixtures("init_persistence") From 23f569ea38aded041e61ad5b3daa6f73d37b8247 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Sep 2020 08:03:26 +0200 Subject: [PATCH 148/224] Add test for sell order refind, improve overall test for this function --- tests/conftest_trades.py | 2 +- tests/test_freqtradebot.py | 60 ++++++++++++++++++++++++++++++++++---- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 43bf15e51..78388f022 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -274,6 +274,6 @@ def mock_trade_6(fee): ) o = Order.parse_from_ccxt_object(mock_order_6(), 'LTC/BTC', 'buy') trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_6_sell(), 'LTC/BTC', 'stoploss') + o = Order.parse_from_ccxt_object(mock_order_6_sell(), 'LTC/BTC', 'sell') trade.orders.append(o) return trade diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ace44bfae..29792c248 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -29,7 +29,7 @@ from tests.conftest import (create_mock_trades, get_patched_freqtradebot, patch_wallet, patch_whitelist) from tests.conftest_trades import (MOCK_TRADE_COUNT, mock_order_1, mock_order_2, mock_order_2_sell, mock_order_3, - mock_order_3_sell, mock_order_4, mock_order_5_stoploss) + mock_order_3_sell, mock_order_4, mock_order_5_stoploss, mock_order_6_sell) def patch_RPCManager(mocker) -> MagicMock: @@ -4384,7 +4384,12 @@ def test_refind_lost_order(mocker, default_conf, fee, caplog): freqtrade = get_patched_freqtradebot(mocker, default_conf) mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') - mock_fo = mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order') + mock_fo = mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + return_value={'status': 'open'}) + + def reset_open_orders(trade): + trade.open_order_id = None + trade.stoploss_order_id = None create_mock_trades(fee) trades = Trade.get_trades().all() @@ -4392,36 +4397,81 @@ def test_refind_lost_order(mocker, default_conf, fee, caplog): caplog.clear() # No open order - freqtrade.refind_lost_order(trades[0]) + trade = trades[0] + reset_open_orders(trade) + assert trade.open_order_id is None + assert trade.stoploss_order_id is None + + freqtrade.refind_lost_order(trade) order = mock_order_1() assert log_has_re(r"Order Order(.*order_id=" + order['id'] + ".*) is no longer open.", caplog) assert mock_fo.call_count == 0 assert mock_uts.call_count == 0 + # No change to orderid - as update_trade_state is mocked + assert trade.open_order_id is None + assert trade.stoploss_order_id is None caplog.clear() mock_fo.reset_mock() # Open buy order - freqtrade.refind_lost_order(trades[3]) + trade = trades[3] + reset_open_orders(trade) + assert trade.open_order_id is None + assert trade.stoploss_order_id is None + + freqtrade.refind_lost_order(trade) order = mock_order_4() assert log_has_re(r"Trying to refind Order\(.*", caplog) assert mock_fo.call_count == 0 assert mock_uts.call_count == 0 + # No change to orderid - as update_trade_state is mocked + assert trade.open_order_id is None + assert trade.stoploss_order_id is None caplog.clear() mock_fo.reset_mock() # Open stoploss order - freqtrade.refind_lost_order(trades[4]) + trade = trades[4] + reset_open_orders(trade) + assert trade.open_order_id is None + assert trade.stoploss_order_id is None + + freqtrade.refind_lost_order(trade) order = mock_order_5_stoploss() assert log_has_re(r"Trying to refind Order\(.*", caplog) assert mock_fo.call_count == 1 assert mock_uts.call_count == 1 + # stoploss_order_id is "refound" and added to the trade + assert trade.open_order_id is None + assert trade.stoploss_order_id is not None + + caplog.clear() + mock_fo.reset_mock() + mock_uts.reset_mock() + + # Open sell order + trade = trades[5] + reset_open_orders(trade) + assert trade.open_order_id is None + assert trade.stoploss_order_id is None + + freqtrade.refind_lost_order(trade) + order = mock_order_6_sell() + assert log_has_re(r"Trying to refind Order\(.*", caplog) + assert mock_fo.call_count == 1 + assert mock_uts.call_count == 1 + # sell-orderid is "refound" and added to the trade + assert trade.open_order_id == order['id'] + assert trade.stoploss_order_id is None caplog.clear() # Test error case mock_fo = mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', side_effect=ExchangeError()) + order = mock_order_5_stoploss() + freqtrade.refind_lost_order(trades[4]) assert log_has(f"Error updating {order['id']}.", caplog) From 6a08fee25b88b8f4824bcc5f76f2dc3ee7c96529 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Sep 2020 08:04:04 +0200 Subject: [PATCH 149/224] Fix wrong import in documentation --- docs/strategy-customization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 615be0247..14d5fcd84 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -483,7 +483,7 @@ if self.dp: ### Complete Data-provider sample ```python -from freqtrade.strategy import IStrategy, merge_informative_pairs +from freqtrade.strategy import IStrategy, merge_informative_pair from pandas import DataFrame class SampleStrategy(IStrategy): From 4db8c779fc30eb5efb0ec6661e0cfa46a47ff007 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Sep 2020 08:19:40 +0200 Subject: [PATCH 150/224] Fix formatting issues --- tests/test_freqtradebot.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 29792c248..03b338bab 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -27,9 +27,11 @@ from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, patch_wallet, patch_whitelist) -from tests.conftest_trades import (MOCK_TRADE_COUNT, mock_order_1, mock_order_2, - mock_order_2_sell, mock_order_3, - mock_order_3_sell, mock_order_4, mock_order_5_stoploss, mock_order_6_sell) +from tests.conftest_trades import (MOCK_TRADE_COUNT, mock_order_1, + mock_order_2, mock_order_2_sell, + mock_order_3, mock_order_3_sell, + mock_order_4, mock_order_5_stoploss, + mock_order_6_sell) def patch_RPCManager(mocker) -> MagicMock: @@ -4206,7 +4208,8 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order_ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limit_sell_order): default_conf['cancel_open_orders_on_exit'] = True mocker.patch('freqtrade.exchange.Exchange.fetch_order', - side_effect=[ExchangeError(), limit_sell_order, limit_buy_order, limit_sell_order, ]) + side_effect=[ + ExchangeError(), limit_sell_order, limit_buy_order, limit_sell_order]) buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_buy') sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_sell') From 85d90645c73a7fbef781c6afe998f8cfad84beca Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Sep 2020 15:42:34 +0200 Subject: [PATCH 151/224] Remove duplciate check for buy orders --- freqtrade/freqtradebot.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c20a57205..7d94b6cee 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -324,9 +324,6 @@ class FreqtradeBot: if fo and fo['status'] == 'open': # Assume this as the open order trade.open_order_id = order.order_id - else: - # No action for buy orders ... - continue if fo: logger.info(f"Found {order} for trade {trade}.jj") self.update_trade_state(trade, order.order_id, fo, From b8773de5b0d39163c9ded61e312578791ad9bb98 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Sep 2020 06:44:20 +0200 Subject: [PATCH 152/224] scoped sessions should be closed after requests --- freqtrade/rpc/api_server.py | 10 +++++++++- tests/rpc/test_rpc_apiserver.py | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 4bbc8a1dc..0ae0698cd 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -17,8 +17,9 @@ from werkzeug.serving import make_server from freqtrade.__init__ import __version__ from freqtrade.constants import DATETIME_PRINT_FORMAT -from freqtrade.rpc.rpc import RPC, RPCException +from freqtrade.persistence import Trade from freqtrade.rpc.fiat_convert import CryptoToFiatConverter +from freqtrade.rpc.rpc import RPC, RPCException logger = logging.getLogger(__name__) @@ -70,6 +71,11 @@ def rpc_catch_errors(func: Callable[..., Any]): return func_wrapper +def shutdown_session(exception=None): + # Remove scoped session + Trade.session.remove() + + class ApiServer(RPC): """ This class runs api server and provides rpc.rpc functionality to it @@ -104,6 +110,8 @@ class ApiServer(RPC): self.jwt = JWTManager(self.app) self.app.json_encoder = ArrowJSONEncoder + self.app.teardown_appcontext(shutdown_session) + # Register application handling self.register_rest_rpc_urls() diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d9f5bf781..f8256f1ba 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -471,6 +471,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): assert rc.json == {"error": "Error querying _edge: Edge is not enabled."} +@pytest.mark.usefixtures("init_persistence") def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, limit_sell_order): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) @@ -498,6 +499,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li assert rc.json['best_pair'] == '' assert rc.json['best_rate'] == 0 + trade = Trade.query.first() trade.update(limit_sell_order) trade.close_date = datetime.utcnow() From 41942e3af1c93a7d1301724c4467863c124f3110 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Sep 2020 06:59:07 +0200 Subject: [PATCH 153/224] Update docstring for select_order --- freqtrade/persistence/models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 79b3d491b..dc123a8d9 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -522,8 +522,10 @@ class Trade(_DECL_BASE): def select_order(self, order_side: str, status: Optional[str]) -> Optional[Order]: """ - Returns latest order for this orderside and status - Returns None if nothing is found + Finds latest order for this orderside and status + :param order_side: Side of the order (either 'buy' or 'sell') + :param status: Optionally filter on open / closed orders + :return: latest Order object if it exists, else None """ orders = [o for o in self.orders if o.side == order_side] if status: From aa8832f70e3b8fa8451664d7b802b2603213ebeb Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Sep 2020 07:12:10 +0200 Subject: [PATCH 154/224] Convert select_order to use ft_is_open flag --- freqtrade/freqtradebot.py | 6 ++--- freqtrade/persistence/models.py | 8 +++--- tests/test_freqtradebot.py | 2 +- tests/test_persistence.py | 45 +++++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7d94b6cee..71ced3212 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -260,7 +260,7 @@ class FreqtradeBot: if not trade.is_open and not trade.fee_updated('sell'): # Get sell fee - order = trade.select_order('sell', 'closed') + order = trade.select_order('sell', False) if order: logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.") self.update_trade_state(trade, order.order_id, @@ -269,7 +269,7 @@ class FreqtradeBot: trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() for trade in trades: if trade.is_open and not trade.fee_updated('buy'): - order = trade.select_order('buy', 'closed') + order = trade.select_order('buy', False) if order: logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") self.update_trade_state(trade, order.order_id) @@ -291,7 +291,7 @@ class FreqtradeBot: Handles trades where the initial fee-update did not work. """ logger.info(f"Trying to reupdate buy fees for {trade}") - order = trade.select_order('buy', 'closed') + order = trade.select_order('buy', False) if order: logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") self.update_trade_state(trade, order.order_id) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index dc123a8d9..5e7adfc74 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -520,16 +520,16 @@ class Trade(_DECL_BASE): profit_ratio = (close_trade_price / self.open_trade_price) - 1 return float(f"{profit_ratio:.8f}") - def select_order(self, order_side: str, status: Optional[str]) -> Optional[Order]: + def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]: """ Finds latest order for this orderside and status :param order_side: Side of the order (either 'buy' or 'sell') - :param status: Optionally filter on open / closed orders + :param is_open: Only search for open orders? :return: latest Order object if it exists, else None """ orders = [o for o in self.orders if o.side == order_side] - if status: - orders = [o for o in orders if o.status == status] + if is_open is not None: + orders = [o for o in orders if o.ft_is_open == is_open] if len(orders) > 0: return orders[-1] else: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 03b338bab..3c5621e7a 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4295,7 +4295,7 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): for trade in trades: if trade.is_open: # Exclude Trade 4 - as the order is still open. - if trade.select_order('buy', 'closed'): + if trade.select_order('buy', False): assert trade.fee_open_cost is not None assert trade.fee_open_currency is not None else: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index d2da1c6a2..2bf2ce311 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1078,3 +1078,48 @@ def test_update_order_from_ccxt(): ccxt_order.update({'id': 'somethingelse'}) with pytest.raises(DependencyException, match=r"Order-id's don't match"): o.update_from_ccxt_object(ccxt_order) + + +@pytest.mark.usefixtures("init_persistence") +def test_select_order(fee): + create_mock_trades(fee) + + trades = Trade.get_trades().all() + + # Open buy order, no sell order + order = trades[0].select_order('buy', True) + assert order is None + order = trades[0].select_order('buy', False) + assert order is not None + order = trades[0].select_order('sell', None) + assert order is None + + # closed buy order, and open sell order + order = trades[1].select_order('buy', True) + assert order is None + order = trades[1].select_order('buy', False) + assert order is not None + order = trades[1].select_order('buy', None) + assert order is not None + order = trades[1].select_order('sell', True) + assert order is None + order = trades[1].select_order('sell', False) + assert order is not None + + # Has open buy order + order = trades[3].select_order('buy', True) + assert order is not None + order = trades[3].select_order('buy', False) + assert order is None + + # Open sell order + order = trades[4].select_order('buy', True) + assert order is None + order = trades[4].select_order('buy', False) + assert order is not None + + order = trades[4].select_order('sell', True) + assert order is not None + assert order.ft_order_side == 'stoploss' + order = trades[4].select_order('sell', False) + assert order is None From 0c9301e74a313a40e3829aed1ab701e04858d262 Mon Sep 17 00:00:00 2001 From: caudurodev Date: Fri, 11 Sep 2020 08:41:33 +0200 Subject: [PATCH 155/224] FIX: added missing ) for SQLite insert --- docs/sql_cheatsheet.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index cf785ced6..249e935ef 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -47,6 +47,7 @@ sqlite3 ```sql CREATE TABLE trades +( id INTEGER NOT NULL, exchange VARCHAR NOT NULL, pair VARCHAR NOT NULL, From 90d97c536d5b0a8ad7d863304023e418fe6f766e Mon Sep 17 00:00:00 2001 From: caudurodev Date: Fri, 11 Sep 2020 08:42:42 +0200 Subject: [PATCH 156/224] FIX: added missing ) for SQLite insert --- docs/sql_cheatsheet.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 249e935ef..168d416ab 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -46,8 +46,7 @@ sqlite3 ### Trade table structure ```sql -CREATE TABLE trades -( +CREATE TABLE trades( id INTEGER NOT NULL, exchange VARCHAR NOT NULL, pair VARCHAR NOT NULL, From 50f0483d9aab4a0c73b6f7eb65d7babd3946817d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Sep 2020 20:00:36 +0200 Subject: [PATCH 157/224] FIx fluky test in test_api_logs --- tests/rpc/test_rpc_apiserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index f8256f1ba..626586a4a 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -435,7 +435,7 @@ def test_api_logs(botclient): assert len(rc.json) == 2 assert 'logs' in rc.json # Using a fixed comparison here would make this test fail! - assert rc.json['log_count'] > 10 + assert rc.json['log_count'] > 1 assert len(rc.json['logs']) == rc.json['log_count'] assert isinstance(rc.json['logs'][0], list) From 77c28187a6a63b250eecd7ed0a8151665efbff3e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Sep 2020 20:06:05 +0200 Subject: [PATCH 158/224] Don't run refind order on stoploss --- freqtrade/freqtradebot.py | 4 ++- tests/test_freqtradebot.py | 66 +++++++++++++++++++------------------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 71ced3212..4ecafca67 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -897,7 +897,9 @@ class FreqtradeBot: except InsufficientFundsError as e: logger.warning(f"Unable to place stoploss order {e}.") # Try to figure out what went wrong - self.handle_insufficient_funds(trade) + # TODO: test without refinding order logic + # TODO: Also reenable the test test_create_stoploss_order_insufficient_funds + # self.handle_insufficient_funds(trade) except InvalidOrderException as e: trade.stoploss_order_id = None diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3c5621e7a..90e398c91 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1333,42 +1333,42 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, assert rpc_mock.call_args_list[1][0][0]['order_type'] == 'market' -def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, fee, - limit_buy_order_open, limit_sell_order): - sell_mock = MagicMock(return_value={'id': limit_sell_order['id']}) - freqtrade = get_patched_freqtradebot(mocker, default_conf) +# def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, fee, +# limit_buy_order_open, limit_sell_order): +# sell_mock = MagicMock(return_value={'id': limit_sell_order['id']}) +# freqtrade = get_patched_freqtradebot(mocker, default_conf) - mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': 0.00001172, - 'ask': 0.00001173, - 'last': 0.00001172 - }), - buy=MagicMock(return_value=limit_buy_order_open), - sell=sell_mock, - get_fee=fee, - fetch_order=MagicMock(return_value={'status': 'canceled'}), - stoploss=MagicMock(side_effect=InsufficientFundsError()), - ) - patch_get_signal(freqtrade) - freqtrade.strategy.order_types['stoploss_on_exchange'] = True +# mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') +# mocker.patch.multiple( +# 'freqtrade.exchange.Exchange', +# fetch_ticker=MagicMock(return_value={ +# 'bid': 0.00001172, +# 'ask': 0.00001173, +# 'last': 0.00001172 +# }), +# buy=MagicMock(return_value=limit_buy_order_open), +# sell=sell_mock, +# get_fee=fee, +# fetch_order=MagicMock(return_value={'status': 'canceled'}), +# stoploss=MagicMock(side_effect=InsufficientFundsError()), +# ) +# patch_get_signal(freqtrade) +# freqtrade.strategy.order_types['stoploss_on_exchange'] = True - freqtrade.enter_positions() - trade = Trade.query.first() - caplog.clear() - freqtrade.create_stoploss_order(trade, 200) - # stoploss_orderid was empty before - assert trade.stoploss_order_id is None - assert mock_insuf.call_count == 1 - mock_insuf.reset_mock() +# freqtrade.enter_positions() +# trade = Trade.query.first() +# caplog.clear() +# freqtrade.create_stoploss_order(trade, 200) +# # stoploss_orderid was empty before +# assert trade.stoploss_order_id is None +# assert mock_insuf.call_count == 1 +# mock_insuf.reset_mock() - trade.stoploss_order_id = 'stoploss_orderid' - freqtrade.create_stoploss_order(trade, 200) - # No change to stoploss-orderid - assert trade.stoploss_order_id == 'stoploss_orderid' - assert mock_insuf.call_count == 1 +# trade.stoploss_order_id = 'stoploss_orderid' +# freqtrade.create_stoploss_order(trade, 200) +# # No change to stoploss-orderid +# assert trade.stoploss_order_id == 'stoploss_orderid' +# assert mock_insuf.call_count == 1 @pytest.mark.usefixtures("init_persistence") From 3f52b6d6d5115e7cfa22d8b1860104223e4a15e2 Mon Sep 17 00:00:00 2001 From: Blackhawke Date: Fri, 11 Sep 2020 12:01:45 -0700 Subject: [PATCH 159/224] Move "source" restored ".env/" --- docs/installation.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index da82d632f..153de7065 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -63,7 +63,7 @@ With this option, the script will install the bot and most dependencies: You will need to have git and python3.6+ installed beforehand for this to work. * Mandatory software as: `ta-lib` -* Setup your virtualenv: `source .env/bin/activate` +* Setup your virtualenv under `.env/` This option is a combination of installation tasks, `--reset` and `--config`. @@ -79,6 +79,12 @@ This option will hard reset your branch (only if you are on either `master` or ` DEPRECATED - use `freqtrade new-config -c config.json` instead. +** Setup your virtual environment ** + +Each time you open a new terminal, you must run `source .env/bin/activate` + + + ------ ## Custom Installation From ba8e93e2a11ce397dab9528b39a7529f7db82fcd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Sep 2020 16:43:22 +0200 Subject: [PATCH 160/224] Remove requirements-common.txt in an attempt to simplify installation --- Dockerfile | 2 +- Dockerfile.armhf | 2 +- requirements-common.txt | 37 ------------------------------------- requirements.txt | 39 ++++++++++++++++++++++++++++++++++++--- setup.py | 3 +-- 5 files changed, 39 insertions(+), 44 deletions(-) delete mode 100644 requirements-common.txt diff --git a/Dockerfile b/Dockerfile index e1220e3b8..22b0c43a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* ENV LD_LIBRARY_PATH /usr/local/lib # Install dependencies -COPY requirements.txt requirements-common.txt requirements-hyperopt.txt /freqtrade/ +COPY requirements.txt requirements-hyperopt.txt /freqtrade/ RUN pip install numpy --no-cache-dir \ && pip install -r requirements-hyperopt.txt --no-cache-dir diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 5c5e4a885..0633008ea 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -17,7 +17,7 @@ RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* ENV LD_LIBRARY_PATH /usr/local/lib # Install dependencies -COPY requirements.txt requirements-common.txt /freqtrade/ +COPY requirements.txt /freqtrade/ RUN pip install numpy --no-cache-dir \ && pip install -r requirements.txt --no-cache-dir diff --git a/requirements-common.txt b/requirements-common.txt deleted file mode 100644 index d0f0ef370..000000000 --- a/requirements-common.txt +++ /dev/null @@ -1,37 +0,0 @@ -# requirements without requirements installable via conda -# mainly used for Raspberry pi installs -ccxt==1.34.11 -SQLAlchemy==1.3.19 -python-telegram-bot==12.8 -arrow==0.16.0 -cachetools==4.1.1 -requests==2.24.0 -urllib3==1.25.10 -wrapt==1.12.1 -jsonschema==3.2.0 -TA-Lib==0.4.18 -tabulate==0.8.7 -pycoingecko==1.3.0 -jinja2==2.11.2 -tables==3.6.1 -blosc==1.9.2 - -# find first, C search in arrays -py_find_1st==1.1.4 - -# Load ticker files 30% faster -python-rapidjson==0.9.1 - -# Notify systemd -sdnotify==0.3.2 - -# Api server -flask==1.1.2 -flask-jwt-extended==3.24.1 -flask-cors==3.0.9 - -# Support for colorized terminal output -colorama==0.4.3 -# Building config files interactively -questionary==1.5.2 -prompt-toolkit==3.0.7 diff --git a/requirements.txt b/requirements.txt index a1a1fb250..73ad7f4ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,38 @@ -# Load common requirements --r requirements-common.txt - numpy==1.19.1 pandas==1.1.2 + +ccxt==1.34.11 +SQLAlchemy==1.3.19 +python-telegram-bot==12.8 +arrow==0.16.0 +cachetools==4.1.1 +requests==2.24.0 +urllib3==1.25.10 +wrapt==1.12.1 +jsonschema==3.2.0 +TA-Lib==0.4.18 +tabulate==0.8.7 +pycoingecko==1.3.0 +jinja2==2.11.2 +tables==3.6.1 +blosc==1.9.2 + +# find first, C search in arrays +py_find_1st==1.1.4 + +# Load ticker files 30% faster +python-rapidjson==0.9.1 + +# Notify systemd +sdnotify==0.3.2 + +# Api server +flask==1.1.2 +flask-jwt-extended==3.24.1 +flask-cors==3.0.9 + +# Support for colorized terminal output +colorama==0.4.3 +# Building config files interactively +questionary==1.5.2 +prompt-toolkit==3.0.7 diff --git a/setup.py b/setup.py index 7213d3092..88d754668 100644 --- a/setup.py +++ b/setup.py @@ -62,7 +62,7 @@ setup(name='freqtrade', setup_requires=['pytest-runner', 'numpy'], tests_require=['pytest', 'pytest-asyncio', 'pytest-cov', 'pytest-mock', ], install_requires=[ - # from requirements-common.txt + # from requirements.txt 'ccxt>=1.24.96', 'SQLAlchemy', 'python-telegram-bot', @@ -82,7 +82,6 @@ setup(name='freqtrade', 'jinja2', 'questionary', 'prompt-toolkit', - # from requirements.txt 'numpy', 'pandas', 'tables', From d65d886d7de343d763c83f602fe9dc1d3da5ac39 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Sep 2020 16:54:21 +0200 Subject: [PATCH 161/224] add branch to CI - change branch-detection to support tags as well --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c6141344..a50b0886e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: - master - develop - github_actions_tests + - reduce_requirements_files tags: release: types: [published] @@ -226,7 +227,7 @@ jobs: - name: Extract branch name shell: bash - run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" + run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF##*/})" id: extract_branch - name: Build distribution From 07ccda9146ba8faf89ef509574b0183359e50255 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Sep 2020 16:59:45 +0200 Subject: [PATCH 162/224] Fix syntax for released CI --- .github/workflows/ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a50b0886e..16bf1959d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,10 +6,9 @@ on: - master - develop - github_actions_tests - - reduce_requirements_files tags: - release: - types: [published] + release: + types: [published] pull_request: schedule: - cron: '0 5 * * 4' From 60538368aca0c24bc03f5ac00c2daa67666ce04e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2020 06:35:00 +0000 Subject: [PATCH 163/224] Bump pytest from 6.0.1 to 6.0.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.0.1 to 6.0.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/6.0.1...6.0.2) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 44f0c7265..aa7145053 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.8.3 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 mypy==0.782 -pytest==6.0.1 +pytest==6.0.2 pytest-asyncio==0.14.0 pytest-cov==2.10.1 pytest-mock==3.3.1 From 3c76945d5e5c0929fdae88f07b741a1ef6a2e2f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2020 06:35:14 +0000 Subject: [PATCH 164/224] Bump numpy from 1.19.1 to 1.19.2 Bumps [numpy](https://github.com/numpy/numpy) from 1.19.1 to 1.19.2. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/master/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.19.1...v1.19.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 73ad7f4ab..34b8381e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.19.1 +numpy==1.19.2 pandas==1.1.2 ccxt==1.34.11 From 9d3caae9e3f302409e65dd75e151240bf5f3ea18 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2020 06:35:36 +0000 Subject: [PATCH 165/224] Bump plotly from 4.9.0 to 4.10.0 Bumps [plotly](https://github.com/plotly/plotly.py) from 4.9.0 to 4.10.0. - [Release notes](https://github.com/plotly/plotly.py/releases) - [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.py/compare/v4.9.0...v4.10.0) Signed-off-by: dependabot[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index 51d14d636..a91b3bd38 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==4.9.0 +plotly==4.10.0 From 23c1ae5d4af117d17da8bee6bee26d82cee80e82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2020 06:35:41 +0000 Subject: [PATCH 166/224] Bump nbconvert from 5.6.1 to 6.0.2 Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 5.6.1 to 6.0.2. - [Release notes](https://github.com/jupyter/nbconvert/releases) - [Commits](https://github.com/jupyter/nbconvert/compare/5.6.1...6.0.2) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 44f0c7265..72d5a795d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -15,4 +15,4 @@ pytest-mock==3.3.1 pytest-random-order==1.0.4 # Convert jupyter notebooks to markdown documents -nbconvert==5.6.1 +nbconvert==6.0.2 From 6d30740b55f41d6d878e08ce120e2bfcc0b508d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2020 07:07:14 +0000 Subject: [PATCH 167/224] Bump ccxt from 1.34.11 to 1.34.25 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.34.11 to 1.34.25. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.34.11...1.34.25) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 34b8381e5..5da544a3c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.2 pandas==1.1.2 -ccxt==1.34.11 +ccxt==1.34.25 SQLAlchemy==1.3.19 python-telegram-bot==12.8 arrow==0.16.0 From 962fed24b090b76f4bac0468b949fea493b5d333 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Sep 2020 17:34:13 +0200 Subject: [PATCH 168/224] Readd refind_order logic --- freqtrade/freqtradebot.py | 4 +-- tests/test_freqtradebot.py | 66 +++++++++++++++++++------------------- 2 files changed, 34 insertions(+), 36 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4ecafca67..71ced3212 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -897,9 +897,7 @@ class FreqtradeBot: except InsufficientFundsError as e: logger.warning(f"Unable to place stoploss order {e}.") # Try to figure out what went wrong - # TODO: test without refinding order logic - # TODO: Also reenable the test test_create_stoploss_order_insufficient_funds - # self.handle_insufficient_funds(trade) + self.handle_insufficient_funds(trade) except InvalidOrderException as e: trade.stoploss_order_id = None diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 90e398c91..3c5621e7a 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1333,42 +1333,42 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, assert rpc_mock.call_args_list[1][0][0]['order_type'] == 'market' -# def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, fee, -# limit_buy_order_open, limit_sell_order): -# sell_mock = MagicMock(return_value={'id': limit_sell_order['id']}) -# freqtrade = get_patched_freqtradebot(mocker, default_conf) +def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, fee, + limit_buy_order_open, limit_sell_order): + sell_mock = MagicMock(return_value={'id': limit_sell_order['id']}) + freqtrade = get_patched_freqtradebot(mocker, default_conf) -# mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') -# mocker.patch.multiple( -# 'freqtrade.exchange.Exchange', -# fetch_ticker=MagicMock(return_value={ -# 'bid': 0.00001172, -# 'ask': 0.00001173, -# 'last': 0.00001172 -# }), -# buy=MagicMock(return_value=limit_buy_order_open), -# sell=sell_mock, -# get_fee=fee, -# fetch_order=MagicMock(return_value={'status': 'canceled'}), -# stoploss=MagicMock(side_effect=InsufficientFundsError()), -# ) -# patch_get_signal(freqtrade) -# freqtrade.strategy.order_types['stoploss_on_exchange'] = True + mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=MagicMock(return_value={ + 'bid': 0.00001172, + 'ask': 0.00001173, + 'last': 0.00001172 + }), + buy=MagicMock(return_value=limit_buy_order_open), + sell=sell_mock, + get_fee=fee, + fetch_order=MagicMock(return_value={'status': 'canceled'}), + stoploss=MagicMock(side_effect=InsufficientFundsError()), + ) + patch_get_signal(freqtrade) + freqtrade.strategy.order_types['stoploss_on_exchange'] = True -# freqtrade.enter_positions() -# trade = Trade.query.first() -# caplog.clear() -# freqtrade.create_stoploss_order(trade, 200) -# # stoploss_orderid was empty before -# assert trade.stoploss_order_id is None -# assert mock_insuf.call_count == 1 -# mock_insuf.reset_mock() + freqtrade.enter_positions() + trade = Trade.query.first() + caplog.clear() + freqtrade.create_stoploss_order(trade, 200) + # stoploss_orderid was empty before + assert trade.stoploss_order_id is None + assert mock_insuf.call_count == 1 + mock_insuf.reset_mock() -# trade.stoploss_order_id = 'stoploss_orderid' -# freqtrade.create_stoploss_order(trade, 200) -# # No change to stoploss-orderid -# assert trade.stoploss_order_id == 'stoploss_orderid' -# assert mock_insuf.call_count == 1 + trade.stoploss_order_id = 'stoploss_orderid' + freqtrade.create_stoploss_order(trade, 200) + # No change to stoploss-orderid + assert trade.stoploss_order_id == 'stoploss_orderid' + assert mock_insuf.call_count == 1 @pytest.mark.usefixtures("init_persistence") From ec01f20bf85a8b960bf607ec5f224accc52194a1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Sep 2020 20:26:55 +0200 Subject: [PATCH 169/224] Add ratio to sell reason stats --- freqtrade/optimize/optimize_reports.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index b5e5da4af..771ac91fb 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -122,7 +122,7 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List profit_mean = result['profit_percent'].mean() profit_sum = result["profit_percent"].sum() - profit_percent_tot = round(result['profit_percent'].sum() * 100.0 / max_open_trades, 2) + profit_percent_tot = result['profit_percent'].sum() / max_open_trades tabular_data.append( { @@ -136,7 +136,8 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List 'profit_sum': profit_sum, 'profit_sum_pct': round(profit_sum * 100, 2), 'profit_total_abs': result['profit_abs'].sum(), - 'profit_total_pct': profit_percent_tot, + 'profit_total': profit_percent_tot, + 'profit_total_pct': round(profit_percent_tot * 100, 2), } ) return tabular_data From dd87938a5eb8ae78c371d2a0c95fce686c217dab Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 08:33:12 +0200 Subject: [PATCH 170/224] Fix bug causing close_date to be set again --- freqtrade/persistence/models.py | 11 +++++++---- tests/test_persistence.py | 31 ++++++++++++++++++------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 5e7adfc74..816e23fd3 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -383,15 +383,18 @@ class Trade(_DECL_BASE): self.open_rate = Decimal(safe_value_fallback(order, 'average', 'price')) self.amount = Decimal(safe_value_fallback(order, 'filled', 'amount')) self.recalc_open_trade_price() - logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') + if self.is_open: + logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') self.open_order_id = None elif order_type in ('market', 'limit') and order['side'] == 'sell': + if self.is_open: + logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.') self.close(safe_value_fallback(order, 'average', 'price')) - logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.') elif order_type in ('stop_loss_limit', 'stop-loss', 'stop'): self.stoploss_order_id = None self.close_rate_requested = self.stop_loss - logger.info(f'{order_type.upper()} is hit for {self}.') + if self.is_open: + logger.info(f'{order_type.upper()} is hit for {self}.') self.close(order['average']) else: raise ValueError(f'Unknown order type: {order_type}') @@ -405,7 +408,7 @@ class Trade(_DECL_BASE): self.close_rate = Decimal(rate) self.close_profit = self.calc_profit_ratio() self.close_profit_abs = self.calc_profit() - self.close_date = datetime.utcnow() + self.close_date = self.close_date or datetime.utcnow() self.is_open = False self.sell_order_status = 'closed' self.open_order_id = None diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 2bf2ce311..723143cfc 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -9,7 +9,7 @@ from sqlalchemy import create_engine from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.persistence import Order, Trade, clean_dry_run_db, init -from tests.conftest import create_mock_trades, log_has +from tests.conftest import create_mock_trades, log_has, log_has_re def test_init_create_session(default_conf): @@ -93,6 +93,8 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog): stake_amount=0.001, open_rate=0.01, amount=5, + is_open=True, + open_date=arrow.utcnow().datetime, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -107,9 +109,9 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog): assert trade.open_rate == 0.00001099 assert trade.close_profit is None assert trade.close_date is None - assert log_has("LIMIT_BUY has been fulfilled for Trade(id=2, " - "pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=closed).", - caplog) + assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=2, " + r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=.*\).", + caplog) caplog.clear() trade.open_order_id = 'something' @@ -118,9 +120,9 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog): assert trade.close_rate == 0.00001173 assert trade.close_profit == 0.06201058 assert trade.close_date is not None - assert log_has("LIMIT_SELL has been fulfilled for Trade(id=2, " - "pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=closed).", - caplog) + assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=2, " + r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=.*\).", + caplog) @pytest.mark.usefixtures("init_persistence") @@ -131,8 +133,10 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): stake_amount=0.001, amount=5, open_rate=0.01, + is_open=True, fee_open=fee.return_value, fee_close=fee.return_value, + open_date=arrow.utcnow().datetime, exchange='bittrex', ) @@ -142,20 +146,21 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): assert trade.open_rate == 0.00004099 assert trade.close_profit is None assert trade.close_date is None - assert log_has("MARKET_BUY has been fulfilled for Trade(id=1, " - "pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=closed).", - caplog) + assert log_has_re(r"MARKET_BUY has been fulfilled for Trade\(id=1, " + r"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=.*\).", + caplog) caplog.clear() + trade.is_open = True trade.open_order_id = 'something' trade.update(market_sell_order) assert trade.open_order_id is None assert trade.close_rate == 0.00004173 assert trade.close_profit == 0.01297561 assert trade.close_date is not None - assert log_has("MARKET_SELL has been fulfilled for Trade(id=1, " - "pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=closed).", - caplog) + assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " + r"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=.*\).", + caplog) @pytest.mark.usefixtures("init_persistence") From 254875e6b362fd8acb2aff51289b5d6fad8adbd6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 08:42:15 +0200 Subject: [PATCH 171/224] Add test for new close functionality * Don't updates close_date if the trade was already closed --- tests/test_persistence.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 723143cfc..adfa18876 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -189,6 +189,36 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): assert trade.calc_profit_ratio() == 0.06201058 +@pytest.mark.usefixtures("init_persistence") +def test_trade_close(limit_buy_order, limit_sell_order, fee): + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + open_rate=0.01, + amount=5, + is_open=True, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_date=arrow.Arrow(2020, 2, 1, 15, 5, 1).datetime, + exchange='bittrex', + ) + assert trade.close_profit is None + assert trade.close_date is None + assert trade.is_open is True + trade.close(0.02) + assert trade.is_open is False + assert trade.close_profit == 0.99002494 + assert trade.close_date is not None + + new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime, + assert trade.close_date != new_date + # Close should NOT update close_date if the trade has been closed already + assert trade.is_open is False + trade.close_date = new_date + trade.close(0.02) + assert trade.close_date == new_date + + @pytest.mark.usefixtures("init_persistence") def test_calc_close_trade_price_exception(limit_buy_order, fee): trade = Trade( From bfd0e3553accf22775de52e4aec00b038de33116 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 08:42:37 +0200 Subject: [PATCH 172/224] Don't build this branch anymore in CI --- .github/workflows/ci.yml | 2 -- freqtrade/exchange/common.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81e450c0f..2bb7b195a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,8 +5,6 @@ on: branches: - master - develop - - github_actions_tests - - db_keep_orders tags: release: types: [published] diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 2abac9286..9abd42aa7 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) # Maximum default retry count. # Functions are always called RETRY_COUNT + 1 times (for the original call) API_RETRY_COUNT = 4 -API_FETCH_ORDER_RETRY_COUNT = 3 +API_FETCH_ORDER_RETRY_COUNT = 5 BAD_EXCHANGES = { "bitmex": "Various reasons.", From 35857b3ddea9c4a73332a38677db533cacd8e55b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 09:10:34 +0200 Subject: [PATCH 173/224] Datetime should support --timerange too --- freqtrade/commands/arguments.py | 2 +- freqtrade/commands/data_commands.py | 6 ++++ tests/commands/test_commands.py | 45 +++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 7268c3c8f..498c73fb3 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -57,7 +57,7 @@ ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"] ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"] ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange", - "timeframes", "erase", "dataformat_ohlcv", "dataformat_trades"] + "timeframes", "erase", "dataformat_ohlcv", "dataformat_trades", "timerange"] ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", "export", "exportfilename", diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index da1eb0cf5..956a8693e 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -25,11 +25,17 @@ def start_download_data(args: Dict[str, Any]) -> None: """ config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) + if 'days' in config and 'timerange' in config: + raise OperationalException("--days and --timerange are mutually exclusive. " + "You can only specify one or the other.") timerange = TimeRange() if 'days' in config: time_since = arrow.utcnow().shift(days=-config['days']).strftime("%Y%m%d") timerange = TimeRange.parse_timerange(f'{time_since}-') + if 'timerange' in config: + timerange = timerange.parse_timerange(config['timerange']) + if 'pairs' not in config: raise OperationalException( "Downloading data requires a list of pairs. " diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 26875fb7f..99b7139b0 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -2,6 +2,7 @@ import re from pathlib import Path from unittest.mock import MagicMock, PropertyMock +import arrow import pytest from freqtrade.commands import (start_convert_data, start_create_userdir, @@ -552,6 +553,50 @@ def test_download_data_keyboardInterrupt(mocker, caplog, markets): assert dl_mock.call_count == 1 +def test_download_data_timerange(mocker, caplog, markets): + dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', + MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) + patch_exchange(mocker) + mocker.patch( + 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) + ) + args = [ + "download-data", + "--exchange", "binance", + "--pairs", "ETH/BTC", "XRP/BTC", + "--days", "20", + "--timerange", "20200101-" + ] + with pytest.raises(OperationalException, + match=r"--days and --timerange are mutually.*"): + start_download_data(get_args(args)) + assert dl_mock.call_count == 0 + + args = [ + "download-data", + "--exchange", "binance", + "--pairs", "ETH/BTC", "XRP/BTC", + "--days", "20", + ] + start_download_data(get_args(args)) + assert dl_mock.call_count == 1 + # 20days ago + days_ago = arrow.get(arrow.utcnow().shift(days=-20).date()).timestamp + assert dl_mock.call_args_list[0][1]['timerange'].startts == days_ago + + dl_mock.reset_mock() + args = [ + "download-data", + "--exchange", "binance", + "--pairs", "ETH/BTC", "XRP/BTC", + "--timerange", "20200101-" + ] + start_download_data(get_args(args)) + assert dl_mock.call_count == 1 + + assert dl_mock.call_args_list[0][1]['timerange'].startts == arrow.Arrow(2020, 1, 1).timestamp + + def test_download_data_no_markets(mocker, caplog): dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) From 2f6b00555a09da306f321a2effef242e958a60da Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 09:13:43 +0200 Subject: [PATCH 174/224] Document support for --timerange in download-data --- docs/data-download.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/data-download.md b/docs/data-download.md index 0b22ec9ce..932b84557 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -8,9 +8,11 @@ If no additional parameter is specified, freqtrade will download data for `"1m"` Exchange and pairs will come from `config.json` (if specified using `-c/--config`). Otherwise `--exchange` becomes mandatory. +You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101`). For incremental downloads, the relative approach should be used. + !!! Tip "Tip: Updating existing data" If you already have backtesting data available in your data-directory and would like to refresh this data up to today, use `--days xx` with a number slightly higher than the missing number of days. Freqtrade will keep the available data and only download the missing data. - Be carefull though: If the number is too small (which would result in a few missing days), the whole dataset will be removed and only xx days will be downloaded. + Be careful though: If the number is too small (which would result in a few missing days), the whole dataset will be removed and only xx days will be downloaded. ### Usage @@ -24,6 +26,7 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [--erase] [--data-format-ohlcv {json,jsongz,hdf5}] [--data-format-trades {json,jsongz,hdf5}] + [--timerange TIMERANGE] optional arguments: -h, --help show this help message and exit @@ -48,6 +51,8 @@ optional arguments: --data-format-trades {json,jsongz,hdf5} Storage format for downloaded trades data. (default: `jsongz`). + --timerange TIMERANGE + Specify what timerange of data to use. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). @@ -286,6 +291,7 @@ This will download historical candle (OHLCV) data for all the currency pairs you - To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust rate limits etc.) - To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`. - To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days). +- To download historical candle (OHLCV) data from a fixed starting point, use `--timerange 20200101-` - which will download all data from January 1st, 2020. - Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data. - To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options. From 77d01896954c69cd10f8097713b3b39ce3dc7284 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 09:37:11 +0200 Subject: [PATCH 175/224] Remove not needed argument in update_trade_state --- freqtrade/data/history/history_utils.py | 1 - freqtrade/freqtradebot.py | 18 +++++++----------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index dd09c4c05..ac234a72e 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -136,7 +136,6 @@ def _load_cached_data_for_updating(pair: str, timeframe: str, timerange: Optiona start = None if timerange: if timerange.starttype == 'date': - # TODO: convert to date for conversion start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc) # Intentionally don't pass timerange in - since we need to load the full dataset. diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 71ced3212..eec09a17c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -307,8 +307,7 @@ class FreqtradeBot: logger.info(f"Trying to refind {order}") fo = None if not order.ft_is_open: - # TODO: Does this need to be info level? - logger.info(f"Order {order} is no longer open.") + logger.debug(f"Order {order} is no longer open.") continue if order.ft_order_side == 'buy': # Skip buy side - this is handled by reupdate_buy_order_fees @@ -1125,7 +1124,7 @@ class FreqtradeBot: # we need to fall back to the values from order if corder does not contain these keys. trade.amount = filled_amount trade.stake_amount = trade.amount * trade.open_rate - self.update_trade_state(trade, trade.open_order_id, corder, trade.amount) + self.update_trade_state(trade, trade.open_order_id, corder) trade.open_order_id = None logger.info('Partial buy order timeout for %s.', trade) @@ -1357,16 +1356,14 @@ class FreqtradeBot: # Common update trade state methods # - def update_trade_state(self, trade: Trade, order_id: str, action_order: dict = None, - order_amount: float = None, stoploss_order: bool = False) -> bool: + def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None, + stoploss_order: bool = False) -> bool: """ Checks trades with open orders and updates the amount if necessary Handles closing both buy and sell orders. :param trade: Trade object of the trade we're analyzing :param order_id: Order-id of the order we're analyzing :param action_order: Already aquired order object - :param order_amount: Order-amount - only used in case of partially cancelled buy order - TODO: Investigate if this is really needed, or covered by getting filled in here again. :return: True if order has been cancelled without being filled partially, False otherwise """ if not order_id: @@ -1387,7 +1384,7 @@ class FreqtradeBot: # Try update amount (binance-fix) try: - new_amount = self.get_real_amount(trade, order, order_amount) + new_amount = self.get_real_amount(trade, order) if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount, abs_tol=constants.MATH_CLOSE_PREC): order['amount'] = new_amount @@ -1425,7 +1422,7 @@ class FreqtradeBot: return real_amount return amount - def get_real_amount(self, trade: Trade, order: Dict, order_amount: float = None) -> float: + def get_real_amount(self, trade: Trade, order: Dict) -> float: """ Detect and update trade fee. Calls trade.update_fee() uppon correct detection. @@ -1434,8 +1431,7 @@ class FreqtradeBot: :return: identical (or new) amount for the trade """ # Init variables - if order_amount is None: - order_amount = safe_value_fallback(order, 'filled', 'amount') + order_amount = safe_value_fallback(order, 'filled', 'amount') # Only run for closed orders if trade.fee_updated(order.get('side', '')) or order['status'] == 'open': return order_amount From 1f086e1466e80cb138227bf42fe75360c199504d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 09:46:32 +0200 Subject: [PATCH 176/224] Modify test loglevel --- tests/test_freqtradebot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3c5621e7a..0c12c05bb 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4384,6 +4384,7 @@ def test_handle_insufficient_funds(mocker, default_conf, fee): @pytest.mark.usefixtures("init_persistence") def test_refind_lost_order(mocker, default_conf, fee, caplog): + caplog.set_level(logging.DEBUG) freqtrade = get_patched_freqtradebot(mocker, default_conf) mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') From 5daaed144949e8ffde666c2e7fc22b9e9db85ee9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 11:25:00 +0200 Subject: [PATCH 177/224] Help endpoint does not make sense for the rest api server. therefore, remove the TODO. --- freqtrade/rpc/api_server.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 0ae0698cd..db22ce453 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -222,9 +222,6 @@ class ApiServer(RPC): self.app.add_url_rule(f'{BASE_URI}/forcesell', 'forcesell', view_func=self._forcesell, methods=['POST']) - # TODO: Implement the following - # help (?) - @require_login def page_not_found(self, error): """ From bf95fe2e5cee112fddf83e8d9b09cfa5d3e9e916 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 11:33:55 +0200 Subject: [PATCH 178/224] have the 2 timerange arguments next to each other --- docs/data-download.md | 9 ++++----- freqtrade/commands/arguments.py | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 932b84557..9065bb050 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -20,13 +20,12 @@ You can use a relative timerange (`--days 20`) or an absolute starting point (`- usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-p PAIRS [PAIRS ...]] [--pairs-file FILE] - [--days INT] [--dl-trades] - [--exchange EXCHANGE] + [--days INT] [--timerange TIMERANGE] + [--dl-trades] [--exchange EXCHANGE] [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...]] [--erase] [--data-format-ohlcv {json,jsongz,hdf5}] [--data-format-trades {json,jsongz,hdf5}] - [--timerange TIMERANGE] optional arguments: -h, --help show this help message and exit @@ -35,6 +34,8 @@ optional arguments: separated. --pairs-file FILE File containing a list of pairs to download. --days INT Download data for given number of days. + --timerange TIMERANGE + Specify what timerange of data to use. --dl-trades Download trades instead of OHLCV data. The bot will resample trades to the desired timeframe as specified as --timeframes/-t. @@ -51,8 +52,6 @@ optional arguments: --data-format-trades {json,jsongz,hdf5} Storage format for downloaded trades data. (default: `jsongz`). - --timerange TIMERANGE - Specify what timerange of data to use. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 498c73fb3..b61a4933e 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -56,8 +56,8 @@ ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"] ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"] -ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange", - "timeframes", "erase", "dataformat_ohlcv", "dataformat_trades", "timerange"] +ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "timerange", "download_trades", "exchange", + "timeframes", "erase", "dataformat_ohlcv", "dataformat_trades"] ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", "export", "exportfilename", From 476319da45b017bdc09e7691a93102ab4481583d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 17:21:56 +0200 Subject: [PATCH 179/224] Clarify --timerange documentation --- docs/data-download.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data-download.md b/docs/data-download.md index 9065bb050..9f0486262 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -290,7 +290,7 @@ This will download historical candle (OHLCV) data for all the currency pairs you - To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust rate limits etc.) - To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`. - To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days). -- To download historical candle (OHLCV) data from a fixed starting point, use `--timerange 20200101-` - which will download all data from January 1st, 2020. +- To download historical candle (OHLCV) data from a fixed starting point, use `--timerange 20200101-` - which will download all data from January 1st, 2020. Eventually set end dates are ignored. - Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data. - To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options. From f0d7f18cf98c9ebd6d2b381fd9e092cb824f47af Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 17:32:22 +0200 Subject: [PATCH 180/224] Pad wins / draws / losses for hyperopt with spaces instead of 0's --- freqtrade/optimize/hyperopt.py | 7 ++++--- tests/optimize/test_hyperopt.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 6d29be08e..37de3bc4b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -324,8 +324,9 @@ class Hyperopt: 'results_metrics.avg_profit', 'results_metrics.total_profit', 'results_metrics.profit', 'results_metrics.duration', 'loss', 'is_initial_point', 'is_best']] - trials.columns = ['Best', 'Epoch', 'Trades', 'W/D/L', 'Avg profit', 'Total profit', - 'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best'] + trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', + 'Total profit', 'Profit', 'Avg duration', 'Objective', + 'is_initial_point', 'is_best'] trials['is_profit'] = False trials.loc[trials['is_initial_point'], 'Best'] = '* ' trials.loc[trials['is_best'], 'Best'] = 'Best' @@ -574,7 +575,7 @@ class Hyperopt: 'wins': wins, 'draws': draws, 'losses': losses, - 'winsdrawslosses': f"{wins:04}/{draws:04}/{losses:04}", + 'winsdrawslosses': f"{wins:>4} {draws:>4} {losses:>4}", 'avg_profit': backtesting_results.profit_percent.mean() * 100.0, 'median_profit': backtesting_results.profit_percent.median() * 100.0, 'total_profit': backtesting_results.profit_abs.sum(), diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index e7a26cd5f..d58b91209 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -813,7 +813,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'draws': 0, 'duration': 100.0, 'losses': 0, - 'winsdrawslosses': '0001/0000/0000', + 'winsdrawslosses': ' 1 0 0', 'median_profit': 2.3117, 'profit': 2.3117, 'total_profit': 0.000233, From 2a7935e35e55cf6daeeb937a0f16ff1bfcdaa8fc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 17:51:31 +0200 Subject: [PATCH 181/224] Rename custom_notification to startup_notification --- freqtrade/rpc/rpc.py | 2 +- freqtrade/rpc/rpc_manager.py | 4 ++-- freqtrade/rpc/telegram.py | 2 +- freqtrade/rpc/webhook.py | 2 +- tests/rpc/test_rpc_manager.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 4 ++-- tests/rpc/test_rpc_webhook.py | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f4e5d3b8e..649dd0f58 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -27,7 +27,7 @@ logger = logging.getLogger(__name__) class RPCMessageType(Enum): STATUS_NOTIFICATION = 'status' WARNING_NOTIFICATION = 'warning' - CUSTOM_NOTIFICATION = 'custom' + STARTUP_NOTIFICATION = 'startup' BUY_NOTIFICATION = 'buy' BUY_CANCEL_NOTIFICATION = 'buy_cancel' SELL_NOTIFICATION = 'sell' diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 2cb44fec8..a9038d81c 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -76,7 +76,7 @@ class RPCManager: exchange_name = config['exchange']['name'] strategy_name = config.get('strategy', '') self.send_msg({ - 'type': RPCMessageType.CUSTOM_NOTIFICATION, + 'type': RPCMessageType.STARTUP_NOTIFICATION, 'status': f'*Exchange:* `{exchange_name}`\n' f'*Stake per trade:* `{stake_amount} {stake_currency}`\n' f'*Minimum ROI:* `{minimal_roi}`\n' @@ -85,7 +85,7 @@ class RPCManager: f'*Strategy:* `{strategy_name}`' }) self.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, + 'type': RPCMessageType.STARTUP_NOTIFICATION, 'status': f'Searching for {stake_currency} pairs to buy and sell ' f'based on {pairlist.short_desc()}' }) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a01efaed6..6d529a69f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -190,7 +190,7 @@ class Telegram(RPC): elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION: message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg) - elif msg['type'] == RPCMessageType.CUSTOM_NOTIFICATION: + elif msg['type'] == RPCMessageType.STARTUP_NOTIFICATION: message = '{status}'.format(**msg) else: diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 322d990ee..7235e7c15 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -48,7 +48,7 @@ class Webhook(RPC): elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: valuedict = self._config['webhook'].get('webhooksellcancel', None) elif msg['type'] in (RPCMessageType.STATUS_NOTIFICATION, - RPCMessageType.CUSTOM_NOTIFICATION, + RPCMessageType.STARTUP_NOTIFICATION, RPCMessageType.WARNING_NOTIFICATION): valuedict = self._config['webhook'].get('webhookstatus', None) else: diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index edf6bae4d..5972bf864 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -124,10 +124,10 @@ def test_send_msg_webhook_CustomMessagetype(mocker, default_conf, caplog) -> Non rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules] - rpc_manager.send_msg({'type': RPCMessageType.CUSTOM_NOTIFICATION, + rpc_manager.send_msg({'type': RPCMessageType.STARTUP_NOTIFICATION, 'status': 'TestMessage'}) assert log_has( - "Message type RPCMessageType.CUSTOM_NOTIFICATION not implemented by handler webhook.", + "Message type RPCMessageType.STARTUP_NOTIFICATION not implemented by handler webhook.", caplog) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 6feacd4bd..20f478b8a 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1485,7 +1485,7 @@ def test_warning_notification(default_conf, mocker) -> None: assert msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Warning:* `message`' -def test_custom_notification(default_conf, mocker) -> None: +def test_startup_notification(default_conf, mocker) -> None: msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -1495,7 +1495,7 @@ def test_custom_notification(default_conf, mocker) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) telegram = Telegram(freqtradebot) telegram.send_msg({ - 'type': RPCMessageType.CUSTOM_NOTIFICATION, + 'type': RPCMessageType.STARTUP_NOTIFICATION, 'status': '*Custom:* `Hello World`' }) assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`' diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 1ced62746..2d2c5a4f4 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -150,7 +150,7 @@ def test_send_msg(default_conf, mocker): default_conf["webhook"]["webhooksellcancel"]["value3"].format(**msg)) for msgtype in [RPCMessageType.STATUS_NOTIFICATION, RPCMessageType.WARNING_NOTIFICATION, - RPCMessageType.CUSTOM_NOTIFICATION]: + RPCMessageType.STARTUP_NOTIFICATION]: # Test notification msg = { 'type': msgtype, From e53b88bde38b4309cef663436020e50e873716fe Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 19:38:33 +0200 Subject: [PATCH 182/224] Introduce notification_settings for telegram --- config_full.json.example | 11 ++++++++++- freqtrade/constants.py | 14 ++++++++++++++ freqtrade/rpc/telegram.py | 18 ++++++++++++++---- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index d5bfd3fe1..659580fb1 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -116,7 +116,16 @@ "telegram": { "enabled": true, "token": "your_telegram_token", - "chat_id": "your_telegram_chat_id" + "chat_id": "your_telegram_chat_id", + "notification_settings": { + "status": "on", + "warning": "on", + "startup": "on", + "buy": "on", + "sell": "on", + "buy_cancel": "on", + "sell_cancel": "on" + } }, "api_server": { "enabled": false, diff --git a/freqtrade/constants.py b/freqtrade/constants.py index c71b94bcb..de663bd4b 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -39,6 +39,8 @@ USERPATH_HYPEROPTS = 'hyperopts' USERPATH_STRATEGIES = 'strategies' USERPATH_NOTEBOOKS = 'notebooks' +TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent'] + # Soure files with destination directories within user-directory USER_DATA_FILES = { 'sample_strategy.py': USERPATH_STRATEGIES, @@ -201,6 +203,18 @@ CONF_SCHEMA = { 'enabled': {'type': 'boolean'}, 'token': {'type': 'string'}, 'chat_id': {'type': 'string'}, + 'notification_settings': { + 'type': 'object', + 'properties': { + 'status': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'warning': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'startup': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'buy': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'sell': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'buy_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS} + } + } }, 'required': ['enabled', 'token', 'chat_id'] }, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6d529a69f..905ed6755 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -132,6 +132,13 @@ class Telegram(RPC): def send_msg(self, msg: Dict[str, Any]) -> None: """ Send a message to telegram channel """ + noti = self._config['telegram'].get('notification_settings', {} + ).get(msg['type'].value, 'on') + if noti == 'off': + logger.info(f"Notification {msg['type']} not sent.") + # Notification disabled + return + if msg['type'] == RPCMessageType.BUY_NOTIFICATION: if self._fiat_converter: msg['stake_amount_fiat'] = self._fiat_converter.convert_amount( @@ -196,7 +203,7 @@ class Telegram(RPC): else: raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) - self._send_msg(message) + self._send_msg(message, disable_notification=(noti == 'silent')) def _get_sell_emoji(self, msg): """ @@ -773,7 +780,8 @@ class Telegram(RPC): f"*Current state:* `{val['state']}`" ) - def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: + def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN, + disable_notification: bool = False) -> None: """ Send given markdown message :param msg: message @@ -794,7 +802,8 @@ class Telegram(RPC): self._config['telegram']['chat_id'], text=msg, parse_mode=parse_mode, - reply_markup=reply_markup + reply_markup=reply_markup, + disable_notification=disable_notification, ) except NetworkError as network_err: # Sometimes the telegram server resets the current connection, @@ -807,7 +816,8 @@ class Telegram(RPC): self._config['telegram']['chat_id'], text=msg, parse_mode=parse_mode, - reply_markup=reply_markup + reply_markup=reply_markup, + disable_notification=disable_notification, ) except TelegramError as telegram_err: logger.warning( From 413d7ddf70f5750858d2bafb618deceb1ae01d39 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 19:42:56 +0200 Subject: [PATCH 183/224] Document telegram notification settings --- docs/telegram-usage.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 5f804386d..b718d40a6 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -41,6 +41,34 @@ Talk to the [userinfobot](https://telegram.me/userinfobot) Get your "Id", you will use it for the config parameter `chat_id`. +## Control telegram noise + +Freqtrade provides means to control the verbosity of your telegram bot. +Each setting has the follwoing possible values: + +* `on` - Messages will be sent, and user will be notified. +* `silent` - Message will be sent, Notification will be without sound / vibration. +* `off` - Skip sending a message-type all together. + +Example configuration showing the different settings: + +``` json +"telegram": { + "enabled": true, + "token": "your_telegram_token", + "chat_id": "your_telegram_chat_id", + "notification_settings": { + "status": "silent", + "warning": "on", + "startup": "off", + "buy": "silent", + "sell": "on", + "buy_cancel": "silent", + "sell_cancel": "on" + } + }, +``` + ## Telegram commands Per default, the Telegram bot shows predefined commands. Some commands From 2554dc48e4958cfb38733d1ce3c49cf6c256f9a0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 20:04:12 +0200 Subject: [PATCH 184/224] Add test for notification settings --- freqtrade/rpc/rpc.py | 3 +++ freqtrade/rpc/rpc_manager.py | 2 +- freqtrade/rpc/telegram.py | 4 ++-- freqtrade/rpc/webhook.py | 2 +- tests/rpc/test_rpc_manager.py | 2 +- tests/rpc/test_rpc_telegram.py | 26 +++++++++++++++++++++----- tests/rpc/test_rpc_webhook.py | 2 +- 7 files changed, 30 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 649dd0f58..b32af1596 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -36,6 +36,9 @@ class RPCMessageType(Enum): def __repr__(self): return self.value + def __str__(self): + return self.value + class RPCException(Exception): """ diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index a9038d81c..e54749369 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -59,7 +59,7 @@ class RPCManager: try: mod.send_msg(msg) except NotImplementedError: - logger.error(f"Message type {msg['type']} not implemented by handler {mod.name}.") + logger.error(f"Message type '{msg['type']}' not implemented by handler {mod.name}.") def startup_messages(self, config: Dict[str, Any], pairlist) -> None: if config['dry_run']: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 905ed6755..75c330dd0 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -133,9 +133,9 @@ class Telegram(RPC): """ Send a message to telegram channel """ noti = self._config['telegram'].get('notification_settings', {} - ).get(msg['type'].value, 'on') + ).get(msg['type'], 'on') if noti == 'off': - logger.info(f"Notification {msg['type']} not sent.") + logger.info(f"Notification '{msg['type']}' not sent.") # Notification disabled return diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 7235e7c15..f089550c3 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -54,7 +54,7 @@ class Webhook(RPC): else: raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) if not valuedict: - logger.info("Message type %s not configured for webhooks", msg['type']) + logger.info("Message type '%s' not configured for webhooks", msg['type']) return payload = {key: value.format(**msg) for (key, value) in valuedict.items()} diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 5972bf864..e8d0f648e 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -127,7 +127,7 @@ def test_send_msg_webhook_CustomMessagetype(mocker, default_conf, caplog) -> Non rpc_manager.send_msg({'type': RPCMessageType.STARTUP_NOTIFICATION, 'status': 'TestMessage'}) assert log_has( - "Message type RPCMessageType.STARTUP_NOTIFICATION not implemented by handler webhook.", + "Message type 'startup' not implemented by handler webhook.", caplog) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 20f478b8a..3958a825a 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1299,16 +1299,14 @@ def test_show_config_handle(default_conf, update, mocker) -> None: assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] -def test_send_msg_buy_notification(default_conf, mocker) -> None: +def test_send_msg_buy_notification(default_conf, mocker, caplog) -> 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({ + msg = { 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', @@ -1321,7 +1319,10 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None: 'current_rate': 1.099e-05, 'amount': 1333.3333333333335, 'open_date': arrow.utcnow().shift(hours=-1) - }) + } + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + telegram.send_msg(msg) assert msg_mock.call_args[0][0] \ == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' \ '*Amount:* `1333.33333333`\n' \ @@ -1329,6 +1330,21 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None: '*Current Rate:* `0.00001099`\n' \ '*Total:* `(0.001000 BTC, 12.345 USD)`' + freqtradebot.config['telegram']['notification_settings'] = {'buy': 'off'} + caplog.clear() + msg_mock.reset_mock() + telegram.send_msg(msg) + msg_mock.call_count == 0 + log_has("Notification 'buy' not sent.", caplog) + + freqtradebot.config['telegram']['notification_settings'] = {'buy': 'silent'} + caplog.clear() + msg_mock.reset_mock() + + telegram.send_msg(msg) + msg_mock.call_count == 1 + msg_mock.call_args_list[0][1]['disable_notification'] is True + def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: msg_mock = MagicMock() diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 2d2c5a4f4..9256a5316 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -174,7 +174,7 @@ def test_exception_send_msg(default_conf, mocker, caplog): webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION}) - assert log_has(f"Message type {RPCMessageType.BUY_NOTIFICATION} not configured for webhooks", + assert log_has(f"Message type '{RPCMessageType.BUY_NOTIFICATION}' not configured for webhooks", caplog) default_conf["webhook"] = get_webhook_dict() From a95dbdbde4a2706a19e3edf4190695b644902a6b Mon Sep 17 00:00:00 2001 From: HumanBot Date: Sat, 19 Sep 2020 14:31:23 -0400 Subject: [PATCH 185/224] Added 1M and 1y timeframes Huobi Pro allows monthly and yearly data downloading --- freqtrade/commands/cli_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 8eb5c3ce8..458aae325 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -375,7 +375,7 @@ AVAILABLE_CLI_OPTIONS = { help='Specify which tickers to download. Space-separated list. ' 'Default: `1m 5m`.', choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', - '6h', '8h', '12h', '1d', '3d', '1w'], + '6h', '8h', '12h', '1d', '3d', '1w','1M', '1y'], default=['1m', '5m'], nargs='+', ), From 8c9a600dec579bb8d960cb53e5c6e8907bf5fa0f Mon Sep 17 00:00:00 2001 From: HumanBot Date: Sat, 19 Sep 2020 14:36:12 -0400 Subject: [PATCH 186/224] changed epochs from 5000 to 500 5000 is an overkill for the hyperopt process, repetitive 500 produce better predictions --- docs/hyperopt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 530faf700..3f7a27ef0 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -229,7 +229,7 @@ Because hyperopt tries a lot of combinations to find the best parameters it will We strongly recommend to use `screen` or `tmux` to prevent any connection loss. ```bash -freqtrade hyperopt --config config.json --hyperopt -e 5000 --spaces all +freqtrade hyperopt --config config.json --hyperopt -e 500 --spaces all ``` Use `` as the name of the custom hyperopt used. From a31de431edfa46fcce2d411e8dd17180e20204a2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 20:38:42 +0200 Subject: [PATCH 187/224] Explicitly convert to type to string --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 75c330dd0..87e52980a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -133,7 +133,7 @@ class Telegram(RPC): """ Send a message to telegram channel """ noti = self._config['telegram'].get('notification_settings', {} - ).get(msg['type'], 'on') + ).get(str(msg['type']), 'on') if noti == 'off': logger.info(f"Notification '{msg['type']}' not sent.") # Notification disabled From f51f445011f216dc491faf33cb659908d6b37263 Mon Sep 17 00:00:00 2001 From: HumanBot Date: Sat, 19 Sep 2020 14:45:36 -0400 Subject: [PATCH 188/224] 1M and 1y timeframes added Huobi Pro timeframes added --- docs/data-download.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 9f0486262..63a56959f 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -22,7 +22,7 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-p PAIRS [PAIRS ...]] [--pairs-file FILE] [--days INT] [--timerange TIMERANGE] [--dl-trades] [--exchange EXCHANGE] - [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...]] + [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...]] [--erase] [--data-format-ohlcv {json,jsongz,hdf5}] [--data-format-trades {json,jsongz,hdf5}] @@ -41,7 +41,7 @@ optional arguments: as --timeframes/-t. --exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no config is provided. - -t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...] + -t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...] Specify which tickers to download. Space-separated list. Default: `1m 5m`. --erase Clean all existing data for the selected @@ -104,7 +104,7 @@ usage: freqtrade convert-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-p PAIRS [PAIRS ...]] --format-from {json,jsongz,hdf5} --format-to {json,jsongz,hdf5} [--erase] - [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...]] + [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...]] optional arguments: -h, --help show this help message and exit @@ -117,7 +117,7 @@ optional arguments: Destination format for data conversion. --erase Clean all existing data for the selected exchange/pairs/timeframes. - -t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...] + -t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...] Specify which tickers to download. Space-separated list. Default: `1m 5m`. From c349499985714ac6c6616f7a4b8e64816f994bfb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 11:18:45 +0200 Subject: [PATCH 189/224] Also add 2w (supported by kraken) --- docs/data-download.md | 8 ++++---- freqtrade/commands/cli_options.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 63a56959f..3a7e47c8b 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -22,7 +22,7 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-p PAIRS [PAIRS ...]] [--pairs-file FILE] [--days INT] [--timerange TIMERANGE] [--dl-trades] [--exchange EXCHANGE] - [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...]] + [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]] [--erase] [--data-format-ohlcv {json,jsongz,hdf5}] [--data-format-trades {json,jsongz,hdf5}] @@ -41,7 +41,7 @@ optional arguments: as --timeframes/-t. --exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no config is provided. - -t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...] + -t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...] Specify which tickers to download. Space-separated list. Default: `1m 5m`. --erase Clean all existing data for the selected @@ -104,7 +104,7 @@ usage: freqtrade convert-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-p PAIRS [PAIRS ...]] --format-from {json,jsongz,hdf5} --format-to {json,jsongz,hdf5} [--erase] - [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...]] + [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]] optional arguments: -h, --help show this help message and exit @@ -117,7 +117,7 @@ optional arguments: Destination format for data conversion. --erase Clean all existing data for the selected exchange/pairs/timeframes. - -t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...] + -t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...] Specify which tickers to download. Space-separated list. Default: `1m 5m`. diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 458aae325..81b8de1af 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -375,7 +375,7 @@ AVAILABLE_CLI_OPTIONS = { help='Specify which tickers to download. Space-separated list. ' 'Default: `1m 5m`.', choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', - '6h', '8h', '12h', '1d', '3d', '1w','1M', '1y'], + '6h', '8h', '12h', '1d', '3d', '1w', '2w', '1M', '1y'], default=['1m', '5m'], nargs='+', ), From 2b1d0b4ab5f78c2825c49d228eb3725ed647799f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 11:45:08 +0200 Subject: [PATCH 190/224] Rename references to "master" branch to "stable" closes #2496 --- .github/workflows/ci.yml | 7 ++++--- .github/workflows/docker_update_readme.yml | 2 +- CONTRIBUTING.md | 9 +++++---- README.md | 8 ++++---- docker-compose.yml | 2 +- docs/configuration.md | 2 +- docs/developer.md | 18 +++++++++--------- docs/docker.md | 6 +++--- docs/docker_quickstart.md | 6 +++--- docs/index.md | 2 +- docs/installation.md | 12 ++++++------ setup.sh | 14 +++++++------- 12 files changed, 45 insertions(+), 43 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8bc01fa6..392641677 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - stable - develop tags: release: @@ -193,7 +194,7 @@ jobs: steps: - name: Cleanup previous runs on this branch uses: rokroskar/workflow-run-cleanup-action@v0.2.2 - if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/master' && github.repository == 'freqtrade/freqtrade'" + if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/stable' && github.repository == 'freqtrade/freqtrade'" env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" @@ -235,7 +236,7 @@ jobs: - name: Publish to PyPI (Test) uses: pypa/gh-action-pypi-publish@master - if: (steps.extract_branch.outputs.branch == 'master' || github.event_name == 'release') + if: (steps.extract_branch.outputs.branch == 'stable' || github.event_name == 'release') with: user: __token__ password: ${{ secrets.pypi_test_password }} @@ -243,7 +244,7 @@ jobs: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@master - if: (steps.extract_branch.outputs.branch == 'master' || github.event_name == 'release') + if: (steps.extract_branch.outputs.branch == 'stable' || github.event_name == 'release') with: user: __token__ password: ${{ secrets.pypi_password }} diff --git a/.github/workflows/docker_update_readme.yml b/.github/workflows/docker_update_readme.yml index 57a7e591e..95e69be2a 100644 --- a/.github/workflows/docker_update_readme.yml +++ b/.github/workflows/docker_update_readme.yml @@ -2,7 +2,7 @@ name: Update Docker Hub Description on: push: branches: - - master + - stable jobs: dockerHubDescription: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 90594866a..97f62154d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,8 +8,9 @@ Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/ Few pointers for contributions: -- Create your PR against the `develop` branch, not `master`. -- New features need to contain unit tests and must be PEP8 conformant (max-line-length = 100). +- Create your PR against the `develop` branch, not `stable`. +- New features need to contain unit tests, must conform to PEP8 (max-line-length = 100) and should be documented with the introduction PR. +- PR's can be declared as `[WIP]` - which signify Work in Progress Pull Requests (which are not finished). If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. @@ -18,7 +19,7 @@ or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. Best start by reading the [documentation](https://www.freqtrade.io/) to get a feel for what is possible with the bot, or head straight to the [Developer-documentation](https://www.freqtrade.io/en/latest/developer/) (WIP) which should help you getting started. -## Before sending the PR: +## Before sending the PR ### 1. Run unit tests @@ -114,6 +115,6 @@ Contributors may be given commit privileges. Preference will be given to those w 1. Access to resources for cross-platform development and testing. 1. Time to devote to the project regularly. -Being a Committer does not grant write permission on `develop` or `master` for security reasons (Users trust Freqtrade with their Exchange API keys). +Being a Committer does not grant write permission on `develop` or `stable` for security reasons (Users trust Freqtrade with their Exchange API keys). After being Committer for some time, a Committer may be named Core Committer and given full repository access. diff --git a/README.md b/README.md index 90f303c6d..feea47299 100644 --- a/README.md +++ b/README.md @@ -127,8 +127,8 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor The project is currently setup in two main branches: -- `develop` - This branch has often new features, but might also cause breaking changes. -- `master` - This branch contains the latest stable release. The bot 'should' be stable on this branch, and is generally well tested. +- `develop` - This branch has often new features, but might also contain breaking changes. We try hard to keep this branch as stable as possible. +- `stable` - This branch contains the latest stable release. This branch is generally well tested. - `feat/*` - These are feature branches, which are being worked on heavily. Please don't use these unless you want to test a specific feature. ## Support @@ -171,11 +171,11 @@ Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/ **Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. -**Important:** Always create your PR against the `develop` branch, not `master`. +**Important:** Always create your PR against the `develop` branch, not `stable`. ## Requirements -### Uptodate clock +### Up-to-date clock The clock must be accurate, syncronized to a NTP server very frequently to avoid problems with communication to the exchanges. diff --git a/docker-compose.yml b/docker-compose.yml index 49d83aa5e..527e6d62a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: freqtrade: - image: freqtradeorg/freqtrade:master + image: freqtradeorg/freqtrade:stable # image: freqtradeorg/freqtrade:develop # Build step - only needed when additional dependencies are needed # build: diff --git a/docs/configuration.md b/docs/configuration.md index bf141f8e8..d6e26f80e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -375,7 +375,7 @@ Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports exchange markets and trading APIs. The complete up-to-date list can be found in the [CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested by the development team with only Bittrex, Binance and Kraken, - so the these are the only officially supported exhanges: + so the these are the only officially supported exchanges: - [Bittrex](https://bittrex.com/): "bittrex" - [Binance](https://www.binance.com/): "binance" diff --git a/docs/developer.md b/docs/developer.md index 9d47258b7..6eeaf47f4 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -263,13 +263,13 @@ jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown freqtrade This documents some decisions taken for the CI Pipeline. * CI runs on all OS variants, Linux (ubuntu), macOS and Windows. -* Docker images are build for the branches `master` and `develop`. -* Raspberry PI Docker images are postfixed with `_pi` - so tags will be `:master_pi` and `develop_pi`. +* Docker images are build for the branches `stable` and `develop`. +* Raspberry PI Docker images are postfixed with `_pi` - so tags will be `:stable_pi` and `develop_pi`. * Docker images contain a file, `/freqtrade/freqtrade_commit` containing the commit this image is based of. * Full docker image rebuilds are run once a week via schedule. * Deployments run on ubuntu. * ta-lib binaries are contained in the build_helpers directory to avoid fails related to external unavailability. -* All tests must pass for a PR to be merged to `master` or `develop`. +* All tests must pass for a PR to be merged to `stable` or `develop`. ## Creating a release @@ -286,19 +286,19 @@ git checkout -b new_release Determine if crucial bugfixes have been made between this commit and the current state, and eventually cherry-pick these. -* Merge the release branch (master) into this branch. +* Merge the release branch (stable) into this branch. * Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7.1` should we need to do a second release that month. Version numbers must follow allowed versions from PEP0440 to avoid failures pushing to pypi. * Commit this part -* push that branch to the remote and create a PR against the master branch +* push that branch to the remote and create a PR against the stable branch ### Create changelog from git commits !!! Note - Make sure that the master branch is up-to-date! + Make sure that the release branch is up-to-date! ``` bash # Needs to be done before merging / pulling that branch. -git log --oneline --no-decorate --no-merges master..new_release +git log --oneline --no-decorate --no-merges stable..new_release ``` To keep the release-log short, best wrap the full git changelog into a collapsible details section. @@ -314,11 +314,11 @@ To keep the release-log short, best wrap the full git changelog into a collapsib ### Create github release / tag -Once the PR against master is merged (best right after merging): +Once the PR against stable is merged (best right after merging): * Use the button "Draft a new release" in the Github UI (subsection releases). * Use the version-number specified as tag. -* Use "master" as reference (this step comes after the above PR is merged). +* Use "stable" as reference (this step comes after the above PR is merged). * Use the above changelog as release comment (as codeblock) ## Releases diff --git a/docs/docker.md b/docs/docker.md index 3fe335cf0..601c842f7 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -10,9 +10,9 @@ Pull the image from docker hub. Branches / tags available can be checked out on [Dockerhub tags page](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/). ```bash -docker pull freqtradeorg/freqtrade:master +docker pull freqtradeorg/freqtrade:stable # Optionally tag the repository so the run-commands remain shorter -docker tag freqtradeorg/freqtrade:master freqtrade +docker tag freqtradeorg/freqtrade:stable freqtrade ``` To update the image, simply run the above commands again and restart your running container. @@ -20,7 +20,7 @@ To update the image, simply run the above commands again and restart your runnin Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image). !!! Note "Docker image update frequency" - The official docker images with tags `master`, `develop` and `latest` are automatically rebuild once a week to keep the base image uptodate. + The official docker images with tags `stable`, `develop` and `latest` are automatically rebuild once a week to keep the base image up-to-date. In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`. ### Prepare the configuration files diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index c033e827b..caea4f599 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -29,7 +29,7 @@ Create a new directory and place the [docker-compose file](https://github.com/fr mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository - curl https://raw.githubusercontent.com/freqtrade/freqtrade/master/docker-compose.yml -o docker-compose.yml + curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml # Pull the freqtrade image docker-compose pull @@ -46,7 +46,7 @@ Create a new directory and place the [docker-compose file](https://github.com/fr mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository - curl https://raw.githubusercontent.com/freqtrade/freqtrade/master/docker-compose.yml -o docker-compose.yml + curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml # Pull the freqtrade image docker-compose pull @@ -61,7 +61,7 @@ Create a new directory and place the [docker-compose file](https://github.com/fr !!! Note "Change your docker Image" You have to change the docker image in the docker-compose file for your Raspberry build to work properly. ``` yml - image: freqtradeorg/freqtrade:master_pi + image: freqtradeorg/freqtrade:stable_pi # image: freqtradeorg/freqtrade:develop_pi ``` diff --git a/docs/index.md b/docs/index.md index 397c549aa..e7fc54628 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,7 +8,7 @@
    Fork -Download +Download Follow @freqtrade diff --git a/docs/installation.md b/docs/installation.md index 5f4807b99..9b15c9685 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -31,7 +31,7 @@ Freqtrade provides the Linux/MacOS Easy Installation script to install all depen The easiest way to install and run Freqtrade is to clone the bot Github repository and then run the Easy Installation script, if it's available for your platform. !!! Note "Version considerations" - When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests). The `master` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable). + When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests). The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable). !!! Note Python3.6 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository. @@ -41,11 +41,11 @@ This can be achieved with the following commands: ```bash git clone https://github.com/freqtrade/freqtrade.git cd freqtrade -# git checkout master # Optional, see (1) +# git checkout stable # Optional, see (1) ./setup.sh --install ``` -(1) This command switches the cloned repository to the use of the `master` branch. It's not needed if you wish to stay on the `develop` branch. You may later switch between branches at any time with the `git checkout master`/`git checkout develop` commands. +(1) This command switches the cloned repository to the use of the `stable` branch. It's not needed if you wish to stay on the `develop` branch. You may later switch between branches at any time with the `git checkout stable`/`git checkout develop` commands. ## Easy Installation Script (Linux/MacOS) @@ -56,7 +56,7 @@ $ ./setup.sh usage: -i,--install Install freqtrade from scratch -u,--update Command git pull to update. - -r,--reset Hard reset your develop/master branch. + -r,--reset Hard reset your develop/stable branch. -c,--config Easy config generator (Will override your existing file). ``` @@ -76,7 +76,7 @@ This option will pull the last version of your current branch and update your vi ** --reset ** -This option will hard reset your branch (only if you are on either `master` or `develop`) and recreate your virtualenv. +This option will hard reset your branch (only if you are on either `stable` or `develop`) and recreate your virtualenv. ** --config ** @@ -174,7 +174,7 @@ Clone the git repository: ```bash git clone https://github.com/freqtrade/freqtrade.git cd freqtrade -git checkout master +git checkout stable ``` #### 4. Install python dependencies diff --git a/setup.sh b/setup.sh index 918c41e6b..049a6a77e 100755 --- a/setup.sh +++ b/setup.sh @@ -120,13 +120,13 @@ function update() { updateenv } -# Reset Develop or Master branch +# Reset Develop or Stable branch function reset() { echo "----------------------------" echo "Reseting branch and virtual env" echo "----------------------------" - if [ "1" == $(git branch -vv |grep -cE "\* develop|\* master") ] + if [ "1" == $(git branch -vv |grep -cE "\* develop|\* stable") ] then read -p "Reset git branch? (This will remove all changes you made!) [y/N]? " @@ -138,14 +138,14 @@ function reset() { then echo "- Hard resetting of 'develop' branch." git reset --hard origin/develop - elif [ "1" == $(git branch -vv |grep -c "* master") ] + elif [ "1" == $(git branch -vv |grep -c "* stable") ] then - echo "- Hard resetting of 'master' branch." - git reset --hard origin/master + echo "- Hard resetting of 'stable' branch." + git reset --hard origin/stable fi fi else - echo "Reset ignored because you are not on 'master' or 'develop'." + echo "Reset ignored because you are not on 'stable' or 'develop'." fi if [ -d ".env" ]; then @@ -270,7 +270,7 @@ function help() { echo "usage:" echo " -i,--install Install freqtrade from scratch" echo " -u,--update Command git pull to update." - echo " -r,--reset Hard reset your develop/master branch." + echo " -r,--reset Hard reset your develop/stable branch." echo " -c,--config Easy config generator (Will override your existing file)." echo " -p,--plot Install dependencies for Plotting scripts." } From b3f0bfd77f8b2f5f5c107a7f6b2880ddf9ba0896 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 11:51:12 +0200 Subject: [PATCH 191/224] Fix a few random typos --- docs/faq.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 48f52a566..beed89801 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -89,7 +89,7 @@ Same fix should be done in the configuration file, if order types are defined in ### How do I search the bot logs for something? -By default, the bot writes its log into stderr stream. This is implemented this way so that you can easily separate the bot's diagnostics messages from Backtesting, Edge and Hyperopt results, output from other various Freqtrade utility subcommands, as well as from the output of your custom `print()`'s you may have inserted into your strategy. So if you need to search the log messages with the grep utility, you need to redirect stderr to stdout and disregard stdout. +By default, the bot writes its log into stderr stream. This is implemented this way so that you can easily separate the bot's diagnostics messages from Backtesting, Edge and Hyperopt results, output from other various Freqtrade utility sub-commands, as well as from the output of your custom `print()`'s you may have inserted into your strategy. So if you need to search the log messages with the grep utility, you need to redirect stderr to stdout and disregard stdout. * In unix shells, this normally can be done as simple as: ```shell @@ -114,7 +114,7 @@ and then grep it as: ```shell $ cat /path/to/mylogfile.log | grep 'something' ``` -or even on the fly, as the bot works and the logfile grows: +or even on the fly, as the bot works and the log file grows: ```shell $ tail -f /path/to/mylogfile.log | grep 'something' ``` @@ -137,7 +137,7 @@ compute. Since hyperopt uses Bayesian search, running for too many epochs may not produce greater results. -It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10.000 epocs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going. +It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10.000 epochs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going. ```bash freqtrade hyperopt -e 1000 @@ -153,7 +153,7 @@ for i in {1..100}; do freqtrade hyperopt -e 1000; done * Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. -* If you wonder why it can take from 20 minutes to days to do 1000 epocs here are some answers: +* If you wonder why it can take from 20 minutes to days to do 1000 epochs here are some answers: This answer was written during the release 0.15.1, when we had: @@ -167,7 +167,7 @@ already 8\*10^9\*10 evaluations. A roughly total of 80 billion evals. Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th of the search space, assuming that the bot never tests the same parameters more than once. -* The time it takes to run 1000 hyperopt epocs depends on things like: The available cpu, harddisk, ram, timeframe, timerange, indicator settings, indicator count, amount of coins that hyperopt test strategies on and the resulting trade count - which can be 650 trades in a year or 10.0000 trades depending if the strategy aims for big profits by trading rarely or for many low profit trades. +* The time it takes to run 1000 hyperopt epochs depends on things like: The available cpu, hard-disk, ram, timeframe, timerange, indicator settings, indicator count, amount of coins that hyperopt test strategies on and the resulting trade count - which can be 650 trades in a year or 10.0000 trades depending if the strategy aims for big profits by trading rarely or for many low profit trades. Example: 4% profit 650 times vs 0,3% profit a trade 10.000 times in a year. If we assume you set the --timerange to 365 days. @@ -180,7 +180,7 @@ Example: The Edge module is mostly a result of brainstorming of [@mishaker](https://github.com/mishaker) and [@creslinux](https://github.com/creslinux) freqtrade team members. -You can find further info on expectancy, winrate, risk management and position size in the following sources: +You can find further info on expectancy, win rate, risk management and position size in the following sources: - https://www.tradeciety.com/ultimate-math-guide-for-traders/ - http://www.vantharp.com/tharp-concepts/expectancy.asp From 637fe35549ab3c9d30f6361adc02f53a11b5ba2a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 11:53:47 +0200 Subject: [PATCH 192/224] Fix typo in release documentation --- docs/developer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer.md b/docs/developer.md index 6eeaf47f4..21934916f 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -294,7 +294,7 @@ Determine if crucial bugfixes have been made between this commit and the current ### Create changelog from git commits !!! Note - Make sure that the release branch is up-to-date! + Make sure that the `stable` branch is up-to-date! ``` bash # Needs to be done before merging / pulling that branch. From b72cccae3c04e279f9f1e09580789576f7721e79 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 13:09:34 +0200 Subject: [PATCH 193/224] Add note about download-data in combination with startup period closes #2673 --- docs/data-download.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/data-download.md b/docs/data-download.md index 3a7e47c8b..e9c5c1865 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -71,6 +71,11 @@ Common arguments: ``` +!!! Note "Startup period" + `download-data` is a strategy-independent command. The idea is to download a big chunk of data once, and then iteratively increase the amount of data stored. + + For that reason, `download-data` does not care about the "startup-period" defined in a strategy. It's up to the user to download additional days if the backtest should start at a specific point in time (while respecting startup period). + ### Data format Freqtrade currently supports 3 data-formats for both OHLCV and trades data: From 8ff7ce8b17ad8b9df409f64a314e99ec44d67099 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 11:40:16 +0000 Subject: [PATCH 194/224] Introduce devcontainer --- .devcontainer/devcontainer.json | 38 ++++++++++++++++++++++++++++++++ .devcontainer/docker-compose.yml | 10 +++++++++ 2 files changed, 48 insertions(+) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/docker-compose.yml diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..916f4e911 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,38 @@ +{ + "name": "freqtrade Develop", + + // Update the 'dockerComposeFile' list if you have more compose files or use different names. + // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. + "dockerComposeFile": [ + "docker-compose.yml" + // "docker-compose.vscode.yml" + ], + + "service": "freqtrade_develop", + + // The optional 'workspaceFolder' property is the path VS Code should open by default when + // connected. This is typically a file mount in .devcontainer/docker-compose.yml + "workspaceFolder": "/freqtrade/", + + "settings": { + "terminal.integrated.shell.linux": null + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [], + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Uncomment the next line if you want start specific services in your Docker Compose config. + // "runServices": [], + + // Uncomment the next line if you want to keep your containers running after VS Code shuts down. + // "shutdownAction": "none", + + // Uncomment the next line to run commands after the container is created - for example installing curl. + // "postCreateCommand": "sudo apt-get update && apt-get install -y git", + + // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "ftuser" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 000000000..d5927c4a9 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,10 @@ +--- +version: '3' +services: + freqtrade_develop: + build: + context: .. + dockerfile: "Dockerfile.develop" + volumes: + - ..:/freqtrade:cached + command: /bin/sh -c "while sleep 1000; do :; done" From 7ead4f9fa3625dc06872494c34a7c198bd264e41 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 14:16:36 +0200 Subject: [PATCH 195/224] Update devcontainer settings --- .devcontainer/devcontainer.json | 63 +++++++++++++++++++------------- .devcontainer/docker-compose.yml | 9 ++++- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 916f4e911..7a5e43cf1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,38 +1,49 @@ { - "name": "freqtrade Develop", + "name": "freqtrade Develop", - // Update the 'dockerComposeFile' list if you have more compose files or use different names. - // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. - "dockerComposeFile": [ - "docker-compose.yml" - // "docker-compose.vscode.yml" - ], + // Update the 'dockerComposeFile' list if you have more compose files or use different names. + // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. + "dockerComposeFile": [ + "docker-compose.yml" + // "docker-compose.vscode.yml" + ], - "service": "freqtrade_develop", + "service": "ft_vscode", - // The optional 'workspaceFolder' property is the path VS Code should open by default when - // connected. This is typically a file mount in .devcontainer/docker-compose.yml - "workspaceFolder": "/freqtrade/", + // The optional 'workspaceFolder' property is the path VS Code should open by default when + // connected. This is typically a file mount in .devcontainer/docker-compose.yml + "workspaceFolder": "/freqtrade/", - "settings": { - "terminal.integrated.shell.linux": null - }, + "settings": { + "terminal.integrated.shell.linux": "/bin/bash", + "editor.insertSpaces": true, + "files.trimTrailingWhitespace": true, + "[markdown]": { + "files.trimTrailingWhitespace": false, + }, + "python.pythonPath": "/usr/local/bin/python", + }, - // Add the IDs of extensions you want installed when the container is created. - "extensions": [], + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "davidanson.vscode-markdownlint", + "ms-azuretools.vscode-docker", + ], - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], - // Uncomment the next line if you want start specific services in your Docker Compose config. - // "runServices": [], + // Uncomment the next line if you want start specific services in your Docker Compose config. + // "runServices": [], - // Uncomment the next line if you want to keep your containers running after VS Code shuts down. - // "shutdownAction": "none", + // Uncomment the next line if you want to keep your containers running after VS Code shuts down. + // "shutdownAction": "none", - // Uncomment the next line to run commands after the container is created - for example installing curl. - // "postCreateCommand": "sudo apt-get update && apt-get install -y git", + // Uncomment the next line to run commands after the container is created - for example installing curl. + // "postCreateCommand": "sudo apt-get update && apt-get install -y git", - // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "ftuser" + // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "ftuser" } diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index d5927c4a9..93ffee309 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -1,10 +1,17 @@ --- version: '3' services: - freqtrade_develop: + ft_vscode: build: context: .. dockerfile: "Dockerfile.develop" volumes: - ..:/freqtrade:cached + - freqtrade-vscode-server:/home/ftuser/.vscode-server + - freqtrade-bashhistory:/home/ftuser/commandhistory + command: /bin/sh -c "while sleep 1000; do :; done" + +volumes: + freqtrade-vscode-server: + freqtrade-bashhistory: From 20e5c1b3885229af7fc1479346953404d7dabc2e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 12:16:58 +0000 Subject: [PATCH 196/224] Update Developer documentation related to docker --- docs/developer.md | 44 ++++++-------------------------------------- 1 file changed, 6 insertions(+), 38 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index 9d47258b7..e359ff34a 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -50,50 +50,18 @@ def test_method_to_test(caplog): ``` -### Local docker usage +### Devcontainer setup -The fastest and easiest way to start up is to use docker-compose.develop which gives developers the ability to start the bot up with all the required dependencies, *without* needing to install any freqtrade specific dependencies on your local machine. +The fastest and easiest way to get started is to use [VSCode](https://code.visualstudio.com/) with the Remote container extension. +This gives developers the ability to start the bot with all required dependencies *without* needing to install any freqtrade specific dependencies on your local machine. #### Install -* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [VSCode](https://code.visualstudio.com/) * [docker](https://docs.docker.com/install/) -* [docker-compose](https://docs.docker.com/compose/install/) +* [Remote container extension documentation](https://code.visualstudio.com/docs/remote) -#### Starting the bot - -##### Use the develop dockerfile - -``` bash -rm docker-compose.yml && mv docker-compose.develop.yml docker-compose.yml -``` - -#### Docker Compose - -##### Starting - -``` bash -docker-compose up -``` - -![Docker compose up](https://user-images.githubusercontent.com/419355/65456322-47f63a80-de06-11e9-90c6-3c74d1bad0b8.png) - -##### Rebuilding - -``` bash -docker-compose build -``` - -##### Executing (effectively SSH into the container) - -The `exec` command requires that the container already be running, if you want to start it -that can be effected by `docker-compose up` or `docker-compose run freqtrade_develop` - -``` bash -docker-compose exec freqtrade_develop /bin/bash -``` - -![image](https://user-images.githubusercontent.com/419355/65456522-ba671a80-de06-11e9-9598-df9ca0d8dcac.png) +For more information about the [Remote container extension](https://code.visualstudio.com/docs/remote), best consult the documentation. ## ErrorHandling From cf85a178f33da910010a423cebe90f0aa076d0df Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 12:34:57 +0000 Subject: [PATCH 197/224] Update developer documentation related to devcontainer --- docs/developer.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index e359ff34a..22de01f78 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -21,11 +21,24 @@ This will spin up a local server (usually on port 8000) so you can see if everyt ## Developer setup -To configure a development environment, best use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ". -Alternatively (if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -e .[all]`. +To configure a development environment, you can either use the provided [DevContainer](#devcontainer-setup), or use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ". +Alternatively (e.g. if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -e .[all]`. This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`. +### Devcontainer setup + +The fastest and easiest way to get started is to use [VSCode](https://code.visualstudio.com/) with the Remote container extension. +This gives developers the ability to start the bot with all required dependencies *without* needing to install any freqtrade specific dependencies on your local machine. + +#### Devcontainer dependencies + +* [VSCode](https://code.visualstudio.com/) +* [docker](https://docs.docker.com/install/) +* [Remote container extension documentation](https://code.visualstudio.com/docs/remote) + +For more information about the [Remote container extension](https://code.visualstudio.com/docs/remote), best consult the documentation. + ### Tests New code should be covered by basic unittests. Depending on the complexity of the feature, Reviewers may request more in-depth unittests. @@ -50,19 +63,6 @@ def test_method_to_test(caplog): ``` -### Devcontainer setup - -The fastest and easiest way to get started is to use [VSCode](https://code.visualstudio.com/) with the Remote container extension. -This gives developers the ability to start the bot with all required dependencies *without* needing to install any freqtrade specific dependencies on your local machine. - -#### Install - -* [VSCode](https://code.visualstudio.com/) -* [docker](https://docs.docker.com/install/) -* [Remote container extension documentation](https://code.visualstudio.com/docs/remote) - -For more information about the [Remote container extension](https://code.visualstudio.com/docs/remote), best consult the documentation. - ## ErrorHandling Freqtrade Exceptions all inherit from `FreqtradeException`. From 0a7b6f73c9fc895869ab4b2b4c0178e95bd8de3d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 12:35:08 +0000 Subject: [PATCH 198/224] Move devcontainer stuff to .devcontainer --- .devcontainer/Dockerfile | 22 ++++++++++++++++++++++ .devcontainer/devcontainer.json | 5 ----- .devcontainer/docker-compose.yml | 7 +++++-- docker-compose.develop.yml | 20 -------------------- 4 files changed, 27 insertions(+), 27 deletions(-) create mode 100644 .devcontainer/Dockerfile delete mode 100644 docker-compose.develop.yml diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..1c2ab8de0 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,22 @@ +FROM freqtradeorg/freqtrade:develop + +# Install dependencies +COPY requirements-dev.txt /freqtrade/ +RUN apt-get update \ + && apt-get -y install git sudo vim \ + && apt-get clean \ + && pip install numpy --no-cache-dir \ + # Install ALL dependencies + && pip install -r requirements-dev.txt --no-cache-dir \ + # Install documentation dependencies (to enable mkdocs) + && pip install -r docs/requirements-docs.txt --no-cache-dir \ + && useradd -m ftuser \ + && mkdir -p /home/ftuser/.vscode-server /home/ftuser/.vscode-server-insiders /home/ftuser/commandhistory \ + && echo "export PROMPT_COMMAND='history -a'" >> /home/ftuser/.bashrc \ + && echo "export HISTFILE=~/commandhistory/.bash_history" >> /home/ftuser/.bashrc \ + && chown ftuser: -R /home/ftuser/ + +USER ftuser + +# Empty the ENTRYPOINT to allow all commands +ENTRYPOINT [] diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7a5e43cf1..1882e3bdf 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,17 +1,12 @@ { "name": "freqtrade Develop", - // Update the 'dockerComposeFile' list if you have more compose files or use different names. - // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. "dockerComposeFile": [ "docker-compose.yml" - // "docker-compose.vscode.yml" ], "service": "ft_vscode", - // The optional 'workspaceFolder' property is the path VS Code should open by default when - // connected. This is typically a file mount in .devcontainer/docker-compose.yml "workspaceFolder": "/freqtrade/", "settings": { diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 93ffee309..7cf3ba2f5 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -4,14 +4,17 @@ services: ft_vscode: build: context: .. - dockerfile: "Dockerfile.develop" + dockerfile: ".devcontainer/Dockerfile" volumes: - ..:/freqtrade:cached - freqtrade-vscode-server:/home/ftuser/.vscode-server - freqtrade-bashhistory:/home/ftuser/commandhistory - + # Expose API port + ports: + - "127.0.0.1:8080:8080" command: /bin/sh -c "while sleep 1000; do :; done" + volumes: freqtrade-vscode-server: freqtrade-bashhistory: diff --git a/docker-compose.develop.yml b/docker-compose.develop.yml deleted file mode 100644 index 562b5960a..000000000 --- a/docker-compose.develop.yml +++ /dev/null @@ -1,20 +0,0 @@ ---- -version: '3' -services: - freqtrade_develop: - build: - context: . - dockerfile: "./Dockerfile.develop" - volumes: - - ".:/freqtrade" - entrypoint: - - "freqtrade" - - freqtrade_bash: - build: - context: . - dockerfile: "./Dockerfile.develop" - volumes: - - ".:/freqtrade" - entrypoint: - - "/bin/bash" From 4355f36cd6eae5e96acf8bef52bcbb9a8c6cd795 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 12:36:47 +0000 Subject: [PATCH 199/224] Add gitconfig to devcontainer --- .devcontainer/docker-compose.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 7cf3ba2f5..7b5e64609 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -6,7 +6,11 @@ services: context: .. dockerfile: ".devcontainer/Dockerfile" volumes: + # Allow git usage within container + - "/home/${USER}/.ssh:/home/ftuser/.ssh:ro" + - "/home/${USER}/.gitconfig:/home/ftuser/.gitconfig:ro" - ..:/freqtrade:cached + # Persist bash-history - freqtrade-vscode-server:/home/ftuser/.vscode-server - freqtrade-bashhistory:/home/ftuser/commandhistory # Expose API port From 096079a595f03294ffea6a03f70b2f4c90fbea93 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 12:41:17 +0000 Subject: [PATCH 200/224] Install autopep8 --- .devcontainer/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 1c2ab8de0..3430cac5a 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -5,7 +5,7 @@ COPY requirements-dev.txt /freqtrade/ RUN apt-get update \ && apt-get -y install git sudo vim \ && apt-get clean \ - && pip install numpy --no-cache-dir \ + && pip install autopep8--no-cache-dir \ # Install ALL dependencies && pip install -r requirements-dev.txt --no-cache-dir \ # Install documentation dependencies (to enable mkdocs) From 129cbf5ef58ad991465dbd5b2ce1971f63ac035a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 14:58:15 +0200 Subject: [PATCH 201/224] Add more Dockerfiles --- Dockerfile.develop => docker/Dockerfile.develop | 1 + docker/Dockerfile.jupyter | 7 +++++++ docker/Dockerfile.plot | 9 +++++++++ .../Dockerfile.technical | 0 docker/docker-compose-jupyter.yml | 16 ++++++++++++++++ 5 files changed, 33 insertions(+) rename Dockerfile.develop => docker/Dockerfile.develop (99%) create mode 100644 docker/Dockerfile.jupyter create mode 100644 docker/Dockerfile.plot rename Dockerfile.technical => docker/Dockerfile.technical (100%) create mode 100644 docker/docker-compose-jupyter.yml diff --git a/Dockerfile.develop b/docker/Dockerfile.develop similarity index 99% rename from Dockerfile.develop rename to docker/Dockerfile.develop index 8f6871c55..cb49984e2 100644 --- a/Dockerfile.develop +++ b/docker/Dockerfile.develop @@ -2,6 +2,7 @@ FROM freqtradeorg/freqtrade:develop # Install dependencies COPY requirements-dev.txt /freqtrade/ + RUN pip install numpy --no-cache-dir \ && pip install -r requirements-dev.txt --no-cache-dir diff --git a/docker/Dockerfile.jupyter b/docker/Dockerfile.jupyter new file mode 100644 index 000000000..b7499eeef --- /dev/null +++ b/docker/Dockerfile.jupyter @@ -0,0 +1,7 @@ +FROM freqtradeorg/freqtrade:develop_plot + + +RUN pip install jupyterlab --no-cache-dir + +# Empty the ENTRYPOINT to allow all commands +ENTRYPOINT [] diff --git a/docker/Dockerfile.plot b/docker/Dockerfile.plot new file mode 100644 index 000000000..9313a34b9 --- /dev/null +++ b/docker/Dockerfile.plot @@ -0,0 +1,9 @@ +FROM freqtradeorg/freqtrade:develop + +# Install dependencies +COPY requirements-plot.txt /freqtrade/ + +RUN pip install -r requirements-plot.txt --no-cache-dir + +# Empty the ENTRYPOINT to allow all commands +ENTRYPOINT [] diff --git a/Dockerfile.technical b/docker/Dockerfile.technical similarity index 100% rename from Dockerfile.technical rename to docker/Dockerfile.technical diff --git a/docker/docker-compose-jupyter.yml b/docker/docker-compose-jupyter.yml new file mode 100644 index 000000000..4b396d0f5 --- /dev/null +++ b/docker/docker-compose-jupyter.yml @@ -0,0 +1,16 @@ +--- +version: '3' +services: + freqtrade: + build: + context: .. + dockerfile: docker/Dockerfile.jupyter + restart: unless-stopped + container_name: freqtrade + ports: + - "18889:8888" + volumes: + - "./user_data:/freqtrade/user_data" + # Default command used when running `docker compose up` + command: > + jupyter lab --port=8888 --ip 0.0.0.0 --allow-root From 85ab6e43baee20e9b6666f7077fa7f6760af3c9d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 14:58:27 +0200 Subject: [PATCH 202/224] Build _plot dockerfile --- build_helpers/publish_docker.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker.sh index 03a95161b..8e132ecba 100755 --- a/build_helpers/publish_docker.sh +++ b/build_helpers/publish_docker.sh @@ -2,6 +2,7 @@ # Replace / with _ to create a valid tag TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") +TAG_PLOT=${TAG}_plot echo "Running for ${TAG}" # Add commit and commit_message to docker container @@ -16,6 +17,7 @@ else docker pull ${IMAGE_NAME}:${TAG} docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} . fi +docker build --cache-from freqtrade:${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot . if [ $? -ne 0 ]; then echo "failed building image" @@ -32,6 +34,7 @@ fi # Tag image for upload docker tag freqtrade:$TAG ${IMAGE_NAME}:$TAG +docker tag freqtrade:$TAG_PLOT ${IMAGE_NAME}:$TAG_PLOT if [ $? -ne 0 ]; then echo "failed tagging image" return 1 From 40132bbea400497143f464688d9733f6e4a0bdf0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 14:58:37 +0200 Subject: [PATCH 203/224] Add this branch to CI --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8bc01fa6..34683ce9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: - master - develop + - add_devcontainer tags: release: types: [published] From f9efbed0765ed0597544f7c2f798219088086478 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 14:59:13 +0200 Subject: [PATCH 204/224] Ignore userdata from docker build --- .dockerignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.dockerignore b/.dockerignore index 223b3b110..09f4c9f0c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,3 +13,4 @@ CONTRIBUTING.md MANIFEST.in README.md freqtrade.service +user_data From 30c1253f75949871011843c96e5ea9c12a507aeb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 15:02:07 +0200 Subject: [PATCH 205/224] Use correct ports for jupyter compose file --- docker/docker-compose-jupyter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose-jupyter.yml b/docker/docker-compose-jupyter.yml index 4b396d0f5..14e45983d 100644 --- a/docker/docker-compose-jupyter.yml +++ b/docker/docker-compose-jupyter.yml @@ -8,7 +8,7 @@ services: restart: unless-stopped container_name: freqtrade ports: - - "18889:8888" + - "127.0.0.1:8888:8888" volumes: - "./user_data:/freqtrade/user_data" # Default command used when running `docker compose up` From ab190f7a5b5acc9ca02935b08edad81c442a96e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 15:12:30 +0200 Subject: [PATCH 206/224] Document jupyter with docker usage --- docs/data-analysis.md | 22 +++++++++++++++------- docs/docker_quickstart.md | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index fc4693b17..17da98935 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -1,12 +1,22 @@ # Analyzing bot data with Jupyter notebooks -You can analyze the results of backtests and trading history easily using Jupyter notebooks. Sample notebooks are located at `user_data/notebooks/`. +You can analyze the results of backtests and trading history easily using Jupyter notebooks. Sample notebooks are located at `user_data/notebooks/` after initializing the user directory with `freqtrade create-userdir --userdir user_data`. -## Pro tips +## Quick start with docker + +Freqtrade provides a docker-compose file which starts up a jupyter lab server. +You can run this server using the following command: `docker-compose -f docker/docker-compose-jupyter.yml up` + +This will create a dockercontainer running jupyter lab, which will be accessible using `https://127.0.0.1:8888/lab`. +Please use the link that's printed in the console after startup for simplified login. + +For more information, Please visit the [Data analysis with Docker](docker_quickstart.md#data-analayis-using-docker-compose) section. + +### Pro tips * See [jupyter.org](https://jupyter.org/documentation) for usage instructions. * Don't forget to start a Jupyter notebook server from within your conda or venv environment or use [nb_conda_kernels](https://github.com/Anaconda-Platform/nb_conda_kernels)* -* Copy the example notebook before use so your changes don't get clobbered with the next freqtrade update. +* Copy the example notebook before use so your changes don't get overwritten with the next freqtrade update. ### Using virtual environment with system-wide Jupyter installation @@ -28,10 +38,8 @@ ipython kernel install --user --name=freqtrade !!! Note This section is provided for completeness, the Freqtrade Team won't provide full support for problems with this setup and will recommend to install Jupyter in the virtual environment directly, as that is the easiest way to get jupyter notebooks up and running. For help with this setup please refer to the [Project Jupyter](https://jupyter.org/) [documentation](https://jupyter.org/documentation) or [help channels](https://jupyter.org/community). - -## Fine print - -Some tasks don't work especially well in notebooks. For example, anything using asynchronous execution is a problem for Jupyter. Also, freqtrade's primary entry point is the shell cli, so using pure python in a notebook bypasses arguments that provide required objects and parameters to helper functions. You may need to set those values or create expected objects manually. +!!! Warning + Some tasks don't work especially well in notebooks. For example, anything using asynchronous execution is a problem for Jupyter. Also, freqtrade's primary entry point is the shell cli, so using pure python in a notebook bypasses arguments that provide required objects and parameters to helper functions. You may need to set those values or create expected objects manually. ## Recommended workflow diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index c033e827b..853d57f3d 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -160,3 +160,21 @@ You'll then also need to modify the `docker-compose.yml` file and uncomment the ``` You can then run `docker-compose build` to build the docker image, and run it using the commands described above. + +## Data analayis using docker compose + +Freqtrade provides a docker-compose file which starts up a jupyter lab server. +You can run this server using the following command: + +``` bash +docker-compose -f docker/docker-compose-jupyter.yml up +``` + +This will create a dockercontainer running jupyter lab, which will be accessible using `https://127.0.0.1:8888/lab`. +Please use the link that's printed in the console after startup for simplified login. + +Since part of this image is built on your machine, it is recommended to rebuild the image from time to time to keep freqtrade (and dependencies) uptodate. + +``` bash +docker-compose -f docker/docker-compose-jupyter.yml build --no-cache +``` From b02c0904b6bcbbc1616798fb0edfb2fd67cc9d9c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 15:17:54 +0200 Subject: [PATCH 207/224] Use buildarg to use correct parent variable --- build_helpers/publish_docker.sh | 2 +- docker/Dockerfile.plot | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker.sh index 8e132ecba..53e18063c 100755 --- a/build_helpers/publish_docker.sh +++ b/build_helpers/publish_docker.sh @@ -17,7 +17,7 @@ else docker pull ${IMAGE_NAME}:${TAG} docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} . fi -docker build --cache-from freqtrade:${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot . +docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot . if [ $? -ne 0 ]; then echo "failed building image" diff --git a/docker/Dockerfile.plot b/docker/Dockerfile.plot index 9313a34b9..1843efdcb 100644 --- a/docker/Dockerfile.plot +++ b/docker/Dockerfile.plot @@ -1,4 +1,5 @@ -FROM freqtradeorg/freqtrade:develop +ARG sourceimage=develop +FROM freqtradeorg/freqtrade:${sourceimage} # Install dependencies COPY requirements-plot.txt /freqtrade/ From 8ff1429e68b611db4d90ed4d4e5a6f3ecd5e6267 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 15:38:53 +0200 Subject: [PATCH 208/224] Add user_data to backtesting --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 22b0c43a7..cdf96abcd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,8 @@ RUN pip install numpy --no-cache-dir \ # Install and execute COPY . /freqtrade/ -RUN pip install -e . --no-cache-dir +RUN pip install -e . --no-cache-dir \ + && mkdir /freqtrade/user_data/ ENTRYPOINT ["freqtrade"] # Default to trade mode CMD [ "trade" ] From 3c460d37b66744a43ab6eaaa6fed19bcb0556231 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 16:20:01 +0200 Subject: [PATCH 209/224] Document existence of PLOT image --- docker-compose.yml | 2 ++ docs/developer.md | 1 + docs/docker_quickstart.md | 11 +++++++++++ 3 files changed, 14 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 49d83aa5e..ca8554b43 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,8 @@ services: freqtrade: image: freqtradeorg/freqtrade:master # image: freqtradeorg/freqtrade:develop + # Use plotting image + # image: freqtradeorg/freqtrade:develop_plot # Build step - only needed when additional dependencies are needed # build: # context: . diff --git a/docs/developer.md b/docs/developer.md index 22de01f78..7c0d61094 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -232,6 +232,7 @@ This documents some decisions taken for the CI Pipeline. * CI runs on all OS variants, Linux (ubuntu), macOS and Windows. * Docker images are build for the branches `master` and `develop`. +* Docker images containing Plot dependencies are also available as `master_plot` and `develop_plot`. * Raspberry PI Docker images are postfixed with `_pi` - so tags will be `:master_pi` and `develop_pi`. * Docker images contain a file, `/freqtrade/freqtrade_commit` containing the commit this image is based of. * Full docker image rebuilds are run once a week via schedule. diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 853d57f3d..857c3d0cc 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -161,6 +161,17 @@ You'll then also need to modify the `docker-compose.yml` file and uncomment the You can then run `docker-compose build` to build the docker image, and run it using the commands described above. +## Plotting with docker-compose + +Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file. +You can then use these commands as follows: + +``` bash +docker-compose run --rm freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH --timerange=20180801-20180805 +``` + +The output will be stored in the `user_data/plot` directory, and can be opened with any modern browser. + ## Data analayis using docker compose Freqtrade provides a docker-compose file which starts up a jupyter lab server. From 261b267160a240f196bdafc44fd848a4ae2e6879 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 16:20:17 +0200 Subject: [PATCH 210/224] Don't build devcontainer on push --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34683ce9b..e8bc01fa6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,6 @@ on: branches: - master - develop - - add_devcontainer tags: release: types: [published] From 7dadca421aca7dea08415061dba2adb3fd4c9a97 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 16:26:48 +0200 Subject: [PATCH 211/224] Update location of docker files --- docs/docker.md | 8 ++++---- docs/docker_quickstart.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/docker.md b/docs/docker.md index 3fe335cf0..59d03164b 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -70,16 +70,16 @@ cp -n config.json.example config.json Best start by pulling the official docker image from dockerhub as explained [here](#download-the-official-docker-image) to speed up building. -To add additional libraries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image. +To add additional libraries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image. ```bash -docker build -t freqtrade -f Dockerfile.technical . +docker build -t freqtrade -f docker/Dockerfile.technical . ``` -If you are developing using Docker, use `Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies: +If you are developing using Docker, use `docker/Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies: ```bash -docker build -f Dockerfile.develop -t freqtrade-dev . +docker build -f docker/Dockerfile.develop -t freqtrade-dev . ``` !!! Warning "Include your config file manually" diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 857c3d0cc..ad82aea3f 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -148,7 +148,7 @@ Head over to the [Backtesting Documentation](backtesting.md) to learn more. ### Additional dependencies with docker-compose If your strategy requires dependencies not included in the default image (like [technical](https://github.com/freqtrade/technical)) - it will be necessary to build the image on your host. -For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) for an example). +For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [docker/Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.technical) for an example). You'll then also need to modify the `docker-compose.yml` file and uncomment the build step, as well as rename the image to avoid naming collisions. From 50aec1d6d3d224023ffe278f1eca1601b0e6e8ba Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 20:19:07 +0200 Subject: [PATCH 212/224] Jupyter service should be called differently --- .devcontainer/Dockerfile | 8 ++------ docker/docker-compose-jupyter.yml | 2 +- docs/docker_quickstart.md | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 3430cac5a..b333dc19d 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -5,12 +5,8 @@ COPY requirements-dev.txt /freqtrade/ RUN apt-get update \ && apt-get -y install git sudo vim \ && apt-get clean \ - && pip install autopep8--no-cache-dir \ - # Install ALL dependencies - && pip install -r requirements-dev.txt --no-cache-dir \ - # Install documentation dependencies (to enable mkdocs) - && pip install -r docs/requirements-docs.txt --no-cache-dir \ - && useradd -m ftuser \ + && pip install autopep8 -r docs/requirements-docs.txt -r requirements-dev.txt --no-cache-dir \ + && useradd -u 1000 -U -m ftuser \ && mkdir -p /home/ftuser/.vscode-server /home/ftuser/.vscode-server-insiders /home/ftuser/commandhistory \ && echo "export PROMPT_COMMAND='history -a'" >> /home/ftuser/.bashrc \ && echo "export HISTFILE=~/commandhistory/.bash_history" >> /home/ftuser/.bashrc \ diff --git a/docker/docker-compose-jupyter.yml b/docker/docker-compose-jupyter.yml index 14e45983d..11a01705c 100644 --- a/docker/docker-compose-jupyter.yml +++ b/docker/docker-compose-jupyter.yml @@ -1,7 +1,7 @@ --- version: '3' services: - freqtrade: + ft_jupyterlab: build: context: .. dockerfile: docker/Dockerfile.jupyter diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index ad82aea3f..dd89ff2e7 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -178,7 +178,7 @@ Freqtrade provides a docker-compose file which starts up a jupyter lab server. You can run this server using the following command: ``` bash -docker-compose -f docker/docker-compose-jupyter.yml up +docker-compose --rm -f docker/docker-compose-jupyter.yml up ``` This will create a dockercontainer running jupyter lab, which will be accessible using `https://127.0.0.1:8888/lab`. From 4cb5c9c85fa941e09e256148d38b815becac5395 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2020 05:39:56 +0000 Subject: [PATCH 213/224] Bump mkdocs-material from 5.5.12 to 5.5.13 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.5.12 to 5.5.13. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/5.5.12...5.5.13) Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 6408616a0..d4c93928e 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==5.5.12 +mkdocs-material==5.5.13 mdx_truly_sane_lists==1.2 From d1b3a16c1369d8af8f4ed6ed5b1442b8b002a363 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2020 05:39:56 +0000 Subject: [PATCH 214/224] Bump ccxt from 1.34.25 to 1.34.40 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.34.25 to 1.34.40. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.34.25...1.34.40) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5da544a3c..44d2f29a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.2 pandas==1.1.2 -ccxt==1.34.25 +ccxt==1.34.40 SQLAlchemy==1.3.19 python-telegram-bot==12.8 arrow==0.16.0 From be33556838161a1ee3b7657a54737318863b2f0b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2020 05:40:00 +0000 Subject: [PATCH 215/224] Bump nbconvert from 6.0.2 to 6.0.4 Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 6.0.2 to 6.0.4. - [Release notes](https://github.com/jupyter/nbconvert/releases) - [Commits](https://github.com/jupyter/nbconvert/compare/6.0.2...6.0.4) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c14a146fa..ffe2763a6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -15,4 +15,4 @@ pytest-mock==3.3.1 pytest-random-order==1.0.4 # Convert jupyter notebooks to markdown documents -nbconvert==6.0.2 +nbconvert==6.0.4 From 4b06c9e0aeb03bb2fcbf1c3dc260c09fccd95edb Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 22 Sep 2020 19:37:18 +0200 Subject: [PATCH 216/224] Add test verifying wrong behaviour --- tests/test_wallets.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 884470014..450dabc4d 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -19,12 +19,17 @@ def test_sync_wallet_at_boot(mocker, default_conf): "used": 0.0, "total": 0.260739 }, + "USDT": { + "free": 20, + "used": 20, + "total": 40 + }, }) ) freqtrade = get_patched_freqtradebot(mocker, default_conf) - assert len(freqtrade.wallets._wallets) == 2 + assert len(freqtrade.wallets._wallets) == 3 assert freqtrade.wallets._wallets['BNT'].free == 1.0 assert freqtrade.wallets._wallets['BNT'].used == 2.0 assert freqtrade.wallets._wallets['BNT'].total == 3.0 @@ -32,6 +37,7 @@ def test_sync_wallet_at_boot(mocker, default_conf): assert freqtrade.wallets._wallets['GAS'].used == 0.0 assert freqtrade.wallets._wallets['GAS'].total == 0.260739 assert freqtrade.wallets.get_free('BNT') == 1.0 + assert 'USDT' in freqtrade.wallets._wallets assert freqtrade.wallets._last_wallet_refresh > 0 mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -51,6 +57,7 @@ def test_sync_wallet_at_boot(mocker, default_conf): freqtrade.wallets.update() + # USDT is missing from the 2nd result - so should not be in this either. assert len(freqtrade.wallets._wallets) == 2 assert freqtrade.wallets._wallets['BNT'].free == 1.2 assert freqtrade.wallets._wallets['BNT'].used == 1.9 From 6b46a35b19737009db78ca82c843a550a64a6243 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 22 Sep 2020 19:37:31 +0200 Subject: [PATCH 217/224] Fix bug of balances not disappearing --- freqtrade/wallets.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index b913155bc..ac08f337c 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -2,6 +2,7 @@ """ Wallet """ import logging +from copy import deepcopy from typing import Any, Dict, NamedTuple import arrow @@ -93,6 +94,10 @@ class Wallets: balances[currency].get('used', None), balances[currency].get('total', None) ) + # Remove currencies no longer in get_balances output + for currency in deepcopy(self._wallets): + if currency not in balances: + del self._wallets[currency] def update(self, require_update: bool = True) -> None: """ From bb56d392a92e250ccedc3df5b01c42860cfad99a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 22 Sep 2020 20:19:46 +0200 Subject: [PATCH 218/224] Fix typo in documentation --- docs/telegram-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index b718d40a6..ce2d715a0 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -44,7 +44,7 @@ Get your "Id", you will use it for the config parameter `chat_id`. ## Control telegram noise Freqtrade provides means to control the verbosity of your telegram bot. -Each setting has the follwoing possible values: +Each setting has the following possible values: * `on` - Messages will be sent, and user will be notified. * `silent` - Message will be sent, Notification will be without sound / vibration. From 378f03a5b187da0a264003d6c583197d9a7e2632 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Sep 2020 06:37:40 +0200 Subject: [PATCH 219/224] Add relevant parameters to stored backtest result --- freqtrade/optimize/optimize_reports.py | 10 ++++++++++ tests/optimize/test_optimize_reports.py | 5 ++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 771ac91fb..18ca7a3a7 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -277,6 +277,16 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], 'max_open_trades': (config['max_open_trades'] if config['max_open_trades'] != float('inf') else -1), 'timeframe': config['timeframe'], + # Parameters relevant for backtesting + 'stoploss': config['stoploss'], + 'trailing_stop': config.get('trailing_stop', False), + 'trailing_stop_positive': config.get('trailing_stop_positive'), + 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0), + 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False), + 'minimal_roi': config['minimal_roi'], + 'use_sell_signal': config['ask_strategy']['use_sell_signal'], + 'sell_profit_only': config['ask_strategy']['sell_profit_only'], + 'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'], **daily_stats, } result['strategy'][strategy] = strat_stats diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 4f62e2e23..d61fd2bc5 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -5,7 +5,6 @@ from pathlib import Path import pandas as pd import pytest from arrow import Arrow - from freqtrade.configuration import TimeRange from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.data import history @@ -22,6 +21,7 @@ from freqtrade.optimize.optimize_reports import (generate_backtest_stats, text_table_bt_results, text_table_sell_reason, text_table_strategy) +from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.strategy.interface import SellType from tests.data.test_history import _backup_file, _clean_test_file @@ -57,6 +57,9 @@ def test_text_table_bt_results(default_conf, mocker): def test_generate_backtest_stats(default_conf, testdatadir): + default_conf.update({'strategy': 'DefaultStrategy'}) + StrategyResolver.load_strategy(default_conf) + results = {'DefStrat': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"], "profit_percent": [0.003312, 0.010801, 0.013803, 0.002780], From d49488bf0e56fb7708b81903abef2a1540cb6325 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2020 05:47:58 +0000 Subject: [PATCH 220/224] Bump python from 3.8.5-slim-buster to 3.8.6-slim-buster Bumps python from 3.8.5-slim-buster to 3.8.6-slim-buster. Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index cdf96abcd..2be65274e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8.5-slim-buster +FROM python:3.8.6-slim-buster RUN apt-get update \ && apt-get -y install curl build-essential libssl-dev sqlite3 \ From ff3e2641aed30b31dcdafcd3c87c7c5be2f3e53d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Sep 2020 20:39:00 +0200 Subject: [PATCH 221/224] generate_backtest_stats must take config options from the strategy config as a strategy can override certain options. --- freqtrade/optimize/backtesting.py | 23 ++++-- freqtrade/optimize/optimize_reports.py | 33 ++++---- tests/optimize/test_optimize_reports.py | 101 +++++++++++++----------- 3 files changed, 85 insertions(+), 72 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 005ec9fb8..bfdb67ec2 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -380,12 +380,6 @@ class Backtesting: logger.info('Using stake_currency: %s ...', self.config['stake_currency']) logger.info('Using stake_amount: %s ...', self.config['stake_amount']) - # Use max_open_trades in backtesting, except --disable-max-market-positions is set - if self.config.get('use_max_market_positions', True): - max_open_trades = self.config['max_open_trades'] - else: - logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...') - max_open_trades = 0 position_stacking = self.config.get('position_stacking', False) data, timerange = self.load_bt_data() @@ -395,6 +389,15 @@ class Backtesting: logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) self._set_strategy(strat) + # Use max_open_trades in backtesting, except --disable-max-market-positions is set + if self.config.get('use_max_market_positions', True): + # Must come from strategy config, as the strategy may modify this setting. + max_open_trades = self.strategy.config['max_open_trades'] + else: + logger.info( + 'Ignoring max_open_trades (--disable-max-market-positions was used) ...') + max_open_trades = 0 + # need to reprocess data every time to populate signals preprocessed = self.strategy.ohlcvdata_to_dataframe(data) @@ -407,7 +410,7 @@ class Backtesting: f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' f'({(max_date - min_date).days} days)..') # Execute backtest and print results - all_results[self.strategy.get_strategy_name()] = self.backtest( + results = self.backtest( processed=preprocessed, stake_amount=self.config['stake_amount'], start_date=min_date, @@ -415,8 +418,12 @@ class Backtesting: max_open_trades=max_open_trades, position_stacking=position_stacking, ) + all_results[self.strategy.get_strategy_name()] = { + 'results': results, + 'config': self.strategy.config, + } - stats = generate_backtest_stats(self.config, data, all_results, + stats = generate_backtest_stats(data, all_results, min_date=min_date, max_date=max_date) if self.config.get('export', False): store_backtest_stats(self.config['exportfilename'], stats) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 18ca7a3a7..696e63b25 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -1,7 +1,7 @@ import logging from datetime import datetime, timedelta, timezone from pathlib import Path -from typing import Any, Dict, List +from typing import Any, Dict, List, Union from arrow import Arrow from pandas import DataFrame @@ -143,19 +143,18 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List return tabular_data -def generate_strategy_metrics(stake_currency: str, max_open_trades: int, - all_results: Dict) -> List[Dict]: +def generate_strategy_metrics(all_results: Dict) -> List[Dict]: """ Generate summary per strategy - :param stake_currency: stake-currency - used to correctly name headers - :param max_open_trades: Maximum allowed open trades used for backtest :param all_results: Dict of containing results for all strategies :return: List of Dicts containing the metrics per Strategy """ tabular_data = [] for strategy, results in all_results.items(): - tabular_data.append(_generate_result_line(results, max_open_trades, strategy)) + tabular_data.append(_generate_result_line( + results['results'], results['config']['max_open_trades'], strategy) + ) return tabular_data @@ -219,25 +218,29 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: } -def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], - all_results: Dict[str, DataFrame], +def generate_backtest_stats(btdata: Dict[str, DataFrame], + all_results: Dict[str, Dict[str, Union[DataFrame, Dict]]], min_date: Arrow, max_date: Arrow ) -> Dict[str, Any]: """ - :param config: Configuration object used for backtest :param btdata: Backtest data - :param all_results: backtest result - dictionary with { Strategy: results}. + :param all_results: backtest result - dictionary in the form: + { Strategy: {'results: results, 'config: config}}. :param min_date: Backtest start date :param max_date: Backtest end date :return: Dictionary containing results per strategy and a stratgy summary. """ - stake_currency = config['stake_currency'] - max_open_trades = config['max_open_trades'] result: Dict[str, Any] = {'strategy': {}} market_change = calculate_market_change(btdata, 'close') - for strategy, results in all_results.items(): + for strategy, content in all_results.items(): + results: Dict[str, DataFrame] = content['results'] + if not isinstance(results, DataFrame): + continue + config = content['config'] + max_open_trades = config['max_open_trades'] + stake_currency = config['stake_currency'] pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, max_open_trades=max_open_trades, @@ -310,9 +313,7 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], 'drawdown_end_ts': 0, }) - strategy_results = generate_strategy_metrics(stake_currency=stake_currency, - max_open_trades=max_open_trades, - all_results=all_results) + strategy_results = generate_strategy_metrics(all_results=all_results) result['strategy_comparison'] = strategy_results diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index d61fd2bc5..fe030e315 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -60,32 +60,35 @@ def test_generate_backtest_stats(default_conf, testdatadir): default_conf.update({'strategy': 'DefaultStrategy'}) StrategyResolver.load_strategy(default_conf) - results = {'DefStrat': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", - "UNITTEST/BTC", "UNITTEST/BTC"], - "profit_percent": [0.003312, 0.010801, 0.013803, 0.002780], - "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], - "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, - Arrow(2017, 11, 14, 21, 36, 00).datetime, - Arrow(2017, 11, 14, 22, 12, 00).datetime, - Arrow(2017, 11, 14, 22, 44, 00).datetime], - "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, - Arrow(2017, 11, 14, 22, 10, 00).datetime, - Arrow(2017, 11, 14, 22, 43, 00).datetime, - Arrow(2017, 11, 14, 22, 58, 00).datetime], - "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], - "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], - "trade_duration": [123, 34, 31, 14], - "open_at_end": [False, False, False, True], - "sell_reason": [SellType.ROI, SellType.STOP_LOSS, - SellType.ROI, SellType.FORCE_SELL] - })} + results = {'DefStrat': { + 'results': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", + "UNITTEST/BTC", "UNITTEST/BTC"], + "profit_percent": [0.003312, 0.010801, 0.013803, 0.002780], + "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], + "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, + Arrow(2017, 11, 14, 21, 36, 00).datetime, + Arrow(2017, 11, 14, 22, 12, 00).datetime, + Arrow(2017, 11, 14, 22, 44, 00).datetime], + "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, + Arrow(2017, 11, 14, 22, 10, 00).datetime, + Arrow(2017, 11, 14, 22, 43, 00).datetime, + Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], + "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], + "trade_duration": [123, 34, 31, 14], + "open_at_end": [False, False, False, True], + "sell_reason": [SellType.ROI, SellType.STOP_LOSS, + SellType.ROI, SellType.FORCE_SELL] + }), + 'config': default_conf} + } timerange = TimeRange.parse_timerange('1510688220-1510700340') min_date = Arrow.fromtimestamp(1510688220) max_date = Arrow.fromtimestamp(1510700340) btdata = history.load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, fill_up_missing=True) - stats = generate_backtest_stats(default_conf, btdata, results, min_date, max_date) + stats = generate_backtest_stats(btdata, results, min_date, max_date) assert isinstance(stats, dict) assert 'strategy' in stats assert 'DefStrat' in stats['strategy'] @@ -93,29 +96,32 @@ def test_generate_backtest_stats(default_conf, testdatadir): strat_stats = stats['strategy']['DefStrat'] assert strat_stats['backtest_start'] == min_date.datetime assert strat_stats['backtest_end'] == max_date.datetime - assert strat_stats['total_trades'] == len(results['DefStrat']) + assert strat_stats['total_trades'] == len(results['DefStrat']['results']) # Above sample had no loosing trade assert strat_stats['max_drawdown'] == 0.0 - results = {'DefStrat': pd.DataFrame( - {"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"], - "profit_percent": [0.003312, 0.010801, -0.013803, 0.002780], - "profit_abs": [0.000003, 0.000011, -0.000014, 0.000003], - "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, - Arrow(2017, 11, 14, 21, 36, 00).datetime, - Arrow(2017, 11, 14, 22, 12, 00).datetime, - Arrow(2017, 11, 14, 22, 44, 00).datetime], - "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, - Arrow(2017, 11, 14, 22, 10, 00).datetime, - Arrow(2017, 11, 14, 22, 43, 00).datetime, - Arrow(2017, 11, 14, 22, 58, 00).datetime], - "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], - "close_rate": [0.002546, 0.003014, 0.0032903, 0.003217], - "trade_duration": [123, 34, 31, 14], - "open_at_end": [False, False, False, True], - "sell_reason": [SellType.ROI, SellType.STOP_LOSS, - SellType.ROI, SellType.FORCE_SELL] - })} + results = {'DefStrat': { + 'results': pd.DataFrame( + {"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"], + "profit_percent": [0.003312, 0.010801, -0.013803, 0.002780], + "profit_abs": [0.000003, 0.000011, -0.000014, 0.000003], + "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, + Arrow(2017, 11, 14, 21, 36, 00).datetime, + Arrow(2017, 11, 14, 22, 12, 00).datetime, + Arrow(2017, 11, 14, 22, 44, 00).datetime], + "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, + Arrow(2017, 11, 14, 22, 10, 00).datetime, + Arrow(2017, 11, 14, 22, 43, 00).datetime, + Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], + "close_rate": [0.002546, 0.003014, 0.0032903, 0.003217], + "trade_duration": [123, 34, 31, 14], + "open_at_end": [False, False, False, True], + "sell_reason": [SellType.ROI, SellType.STOP_LOSS, + SellType.ROI, SellType.FORCE_SELL] + }), + 'config': default_conf} + } assert strat_stats['max_drawdown'] == 0.0 assert strat_stats['drawdown_start'] == Arrow.fromtimestamp(0).datetime @@ -283,9 +289,10 @@ def test_generate_sell_reason_stats(default_conf): assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2) -def test_text_table_strategy(default_conf, mocker): +def test_text_table_strategy(default_conf): + default_conf['max_open_trades'] = 2 results = {} - results['TestStrategy1'] = pd.DataFrame( + results['TestStrategy1'] = {'results': pd.DataFrame( { 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], 'profit_percent': [0.1, 0.2, 0.3], @@ -296,8 +303,8 @@ def test_text_table_strategy(default_conf, mocker): 'losses': [0, 0, 1], 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] } - ) - results['TestStrategy2'] = pd.DataFrame( + ), 'config': default_conf} + results['TestStrategy2'] = {'results': pd.DataFrame( { 'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'], 'profit_percent': [0.4, 0.2, 0.3], @@ -308,7 +315,7 @@ def test_text_table_strategy(default_conf, mocker): 'losses': [0, 0, 1], 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] } - ) + ), 'config': default_conf} result_str = ( '| Strategy | Buys | Avg Profit % | Cum Profit % | Tot' @@ -321,9 +328,7 @@ def test_text_table_strategy(default_conf, mocker): ' 45.00 | 0:20:00 | 3 | 0 | 0 |' ) - strategy_results = generate_strategy_metrics(stake_currency='BTC', - max_open_trades=2, - all_results=results) + strategy_results = generate_strategy_metrics(all_results=results) assert text_table_strategy(strategy_results, 'BTC') == result_str From c56dd487f25a215a20f78939f81ee59651b7c68f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Sep 2020 21:00:58 +0200 Subject: [PATCH 222/224] Fix test failure --- tests/optimize/test_backtesting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index f5c313520..78a7130f9 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -14,7 +14,7 @@ from freqtrade.commands.optimize_commands import (setup_optimize_configuration, start_backtesting) from freqtrade.configuration import TimeRange from freqtrade.data import history -from freqtrade.data.btanalysis import evaluate_result_multi +from freqtrade.data.btanalysis import BT_DATA_COLUMNS, evaluate_result_multi from freqtrade.data.converter import clean_ohlcv_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange @@ -694,7 +694,7 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): patch_exchange(mocker) - backtestmock = MagicMock() + backtestmock = MagicMock(return_value=pd.DataFrame(columns=BT_DATA_COLUMNS + ['profit_abs'])) mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) From bb27b236ceb29ad0a37fefb8f1b290654c6ff5ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Sep 2020 14:55:12 +0200 Subject: [PATCH 223/224] Remove unused arguments --- freqtrade/optimize/backtesting.py | 4 ++-- tests/optimize/test_optimize_reports.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index bfdb67ec2..8d4a3a205 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -423,8 +423,8 @@ class Backtesting: 'config': self.strategy.config, } - stats = generate_backtest_stats(data, all_results, - min_date=min_date, max_date=max_date) + stats = generate_backtest_stats(data, all_results, min_date=min_date, max_date=max_date) + if self.config.get('export', False): store_backtest_stats(self.config['exportfilename'], stats) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index fe030e315..b484e4390 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -26,7 +26,7 @@ from freqtrade.strategy.interface import SellType from tests.data.test_history import _backup_file, _clean_test_file -def test_text_table_bt_results(default_conf, mocker): +def test_text_table_bt_results(): results = pd.DataFrame( { @@ -174,7 +174,7 @@ def test_store_backtest_stats(testdatadir, mocker): assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir / 'testresult')) -def test_generate_pair_metrics(default_conf, mocker): +def test_generate_pair_metrics(): results = pd.DataFrame( { @@ -222,7 +222,7 @@ def test_generate_daily_stats(testdatadir): assert res['losing_days'] == 0 -def test_text_table_sell_reason(default_conf): +def test_text_table_sell_reason(): results = pd.DataFrame( { @@ -254,7 +254,7 @@ def test_text_table_sell_reason(default_conf): stake_currency='BTC') == result_str -def test_generate_sell_reason_stats(default_conf): +def test_generate_sell_reason_stats(): results = pd.DataFrame( { @@ -333,7 +333,7 @@ def test_text_table_strategy(default_conf): assert text_table_strategy(strategy_results, 'BTC') == result_str -def test_generate_edge_table(edge_conf, mocker): +def test_generate_edge_table(): results = {} results['ETH/BTC'] = PairInfo(-0.01, 0.60, 2, 1, 3, 10, 60) From 17659001d8bf09cd290bec438fed6b3dbbba10b8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Sep 2020 09:51:19 +0200 Subject: [PATCH 224/224] Version bump to 2020.9 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index dd7009cb2..4f7825cbd 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2020.8' +__version__ = '2020.9' if __version__ == 'develop':