diff --git a/docs/backtesting.md b/docs/backtesting.md index 68782bb9c..017289905 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -194,7 +194,10 @@ Since backtesting lacks some detailed information about what happens within a ca - Buys happen at open-price - Sell signal sells happen at open-price of the following candle - Low happens before high for stoploss, protecting capital first. -- ROI sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell will be at 2%) +- ROI + - sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell will be at 2%) + - sells are never "below the candle", so a ROI of 2% may result in a sell at 2.4% if low was at 2.4% profit + - Forcesells caused by `=-1` ROI entries use low as sell value, unless N falls on the candle open (e.g. `120: -1` for 1h candles) - Stoploss sells happen exactly at stoploss price, even if low was lower - Trailing stoploss - High happens first - adjusting stoploss diff --git a/docs/configuration.md b/docs/configuration.md index 5ad1a886e..73534b6f1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -47,7 +47,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `ticker_interval` | The ticker interval to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *String* | `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency).
***Datatype:*** *String* | `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode.
*Defaults to `true`.*
***Datatype:*** *Boolean* -| `dry_run_wallet` | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason.
***Datatype:*** *Float* +| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in the Dry Run mode.
*Defaults to `1000`.*
***Datatype:*** *Float* | `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
***Datatype:*** *Boolean* | `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Dict* | `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Float (as ratio)* @@ -149,6 +149,9 @@ In this case a trade amount is calculated as: currency_balance / (max_open_trades - current_open_trades) ``` +!!! Note "When using Dry-Run Mode" + When using `"stake_amount" : "unlimited",` in combination with Dry-Run, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency. + ### Understand minimal_roi The `minimal_roi` configuration parameter is a JSON object where the key is a duration @@ -169,6 +172,9 @@ This parameter can be set in either Strategy or Configuration file. If you use i `minimal_roi` value from the strategy file. If it is not set in either Strategy or Configuration, a default of 1000% `{"0": 10}` is used, and minimal roi is disabled unless your trade generates 1000% profit. +!!! Note "Special case to forcesell after a specific time" + A special case presents using `"": -1` as ROI. This forces the bot to sell a trade after N Minutes, no matter if it's positive or negative, so represents a time-limited force-sell. + ### Understand stoploss Go to the [stoploss documentation](stoploss.md) for more details. @@ -498,8 +504,10 @@ creating trades on the exchange. } ``` -Once you will be happy with your bot performance running in the Dry-run mode, -you can switch it to production mode. +Once you will be happy with your bot performance running in the Dry-run mode, you can switch it to production mode. + +!!! Note + A simulated wallet is available during dry-run mode, and will assume a starting capital of `dry_run_wallet` (defaults to 1000). ## Switch to production mode @@ -529,7 +537,7 @@ you run it in production mode. ``` !!! Note - If you have an exchange API key yet, [see our tutorial](/pre-requisite). + If you have an exchange API key yet, [see our tutorial](installation.md#setup-your-exchange-account). You should also make sure to read the [Exchanges](exchanges.md) section of the documentation to be aware of potential configuration details specific to your exchange. diff --git a/docs/developer.md b/docs/developer.md index d731f1768..5b07aff03 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -183,17 +183,19 @@ raw = ct.fetch_ohlcv(pair, timeframe=timeframe) # convert to dataframe df1 = parse_ticker_dataframe(raw, timeframe, pair=pair, drop_incomplete=False) -print(df1["date"].tail(1)) +print(df1.tail(1)) print(datetime.utcnow()) ``` ``` output -19 2019-06-08 00:00:00+00:00 + date open high low close volume +499 2019-06-08 00:00:00+00:00 0.000007 0.000007 0.000007 0.000007 26264344.0 2019-06-09 12:30:27.873327 ``` The output will show the last entry from the Exchange as well as the current UTC date. If the day shows the same day, then the last candle can be assumed as incomplete and should be dropped (leave the setting `"ohlcv_partial_candle"` from the exchange-class untouched / True). Otherwise, set `"ohlcv_partial_candle"` to `False` to not drop Candles (shown in the example above). +Another way is to run this command multiple times in a row and observe if the volume is changing (while the date remains the same). ## Updating example notebooks @@ -246,6 +248,17 @@ Determine if crucial bugfixes have been made between this commit and the current 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. + +```markdown +
+Expand full changelog + +... Full git changelog + +
+``` + ### Create github release / tag Once the PR against master is merged (best right after merging): diff --git a/docs/exchanges.md b/docs/exchanges.md index 5bd283a69..76fa81f4a 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -61,3 +61,24 @@ print(res) ```shell $ pip3 install web3 ``` + +### Send incomplete candles to the strategy + +Most exchanges return incomplete candles via their ohlcv / klines interface. +By default, Freqtrade assumes that incomplete candles are returned and removes the last candle assuming it's an incomplete candle. + +Whether your exchange returns incomplete candles or not can be checked using [the helper script](developer.md#Incomplete-candles) from the Contributor documentation. + +If the exchange does return incomplete candles and you would like to have incomplete candles in your strategy, you can set the following parameter in the configuration file. + +``` json +{ + + "exchange": { + "_ft_has_params": {"ohlcv_partial_candle": false} + } +} +``` + +!!! Warning "Danger of repainting" + Changing this parameter makes the strategy responsible to avoid repainting and handle this accordingly. Doing this is therefore not recommended, and should only be performed by experienced users who are fully aware of the impact this setting has. diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 9c9e9fdef..f399fe816 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -6,8 +6,12 @@ algorithms included in the `scikit-optimize` package to accomplish this. The search will burn all your CPU cores, make your laptop sound like a fighter jet and still take a long time. +In general, the search for best parameters starts with a few random combinations and then uses Bayesian search with a +ML regressor algorithm (currently ExtraTreesRegressor) to quickly find a combination of parameters in the search hyperspace +that minimizes the value of the [loss function](#loss-functions). + Hyperopt requires historic data to be available, just as backtesting does. -To learn how to get data for the pairs and exchange you're interrested in, head over to the [Data Downloading](data-download.md) section of the documentation. +To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation. !!! Bug Hyperopt can crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133) @@ -170,10 +174,6 @@ with different value combinations. It will then use the given historical data an buys based on the buy signals generated with the above function and based on the results it will end with telling you which paramter combination produced the best profits. -The search for best parameters starts with a few random combinations and then uses a -regressor algorithm (currently ExtraTreesRegressor) to quickly find a parameter combination -that minimizes the value of the [loss function](#loss-functions). - The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. When you want to test an indicator that isn't used by the bot currently, remember to add it to the `populate_indicators()` method in your custom hyperopt file. @@ -284,6 +284,16 @@ number). You can also enable position stacking in the configuration file by explicitly setting `"position_stacking"=true`. +### Reproducible results + +The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with a leading asterisk sign at the Hyperopt output. + +The initial state for generation of these random values (random state) is controlled by the value of the `--random-state` command line option. You can set it to some arbitrary value of your choice to obtain reproducible results. + +If you have not set this value explicitly in the command line options, Hyperopt seeds the random state with some random value for you. The random state value for each Hyperopt run is shown in the log, so you can copy and paste it into the `--random-state` command line option to repeat the set of the initial random epochs used. + +If you have not changed anything in the command line options, configuration, timerange, Strategy and Hyperopt classes, historical data and the Loss Function -- you should obtain same hyperoptimization results with same random state value used. + ## Understand the Hyperopt Result Once Hyperopt is completed you can use the result to create a new strategy. diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index ae77c0b06..3e53c15e3 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==4.5.1 +mkdocs-material==4.6.0 mdx_truly_sane_lists==1.2 diff --git a/freqtrade/constants.py b/freqtrade/constants.py index f5e5969eb..5c7190b41 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -18,7 +18,7 @@ REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'PrecisionFilter', 'PriceFilter'] -DRY_RUN_WALLET = 999.9 +DRY_RUN_WALLET = 1000 MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons USERPATH_HYPEROPTS = 'hyperopts' @@ -75,7 +75,7 @@ CONF_SCHEMA = { }, 'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT}, 'dry_run': {'type': 'boolean'}, - 'dry_run_wallet': {'type': 'number'}, + 'dry_run_wallet': {'type': 'number', 'default': DRY_RUN_WALLET}, 'process_only_new_candles': {'type': 'boolean'}, 'minimal_roi': { 'type': 'object', @@ -275,6 +275,7 @@ CONF_SCHEMA = { 'stake_currency', 'stake_amount', 'dry_run', + 'dry_run_wallet', 'bid_strategy', 'unfilledtimeout', 'stoploss', diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 379c80060..2fc931a9b 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -108,7 +108,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: trades = pd.DataFrame([(t.pair, t.open_date.replace(tzinfo=timezone.utc), t.close_date.replace(tzinfo=timezone.utc) if t.close_date else None, - t.calc_profit(), t.calc_profit_percent(), + t.calc_profit(), t.calc_profit_ratio(), t.open_rate, t.close_rate, t.amount, (round((t.close_date.timestamp() - t.open_date.timestamp()) / 60, 2) if t.close_date else None), diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 865893a66..4c5c0521f 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -68,7 +68,7 @@ def trim_dataframe(df: DataFrame, timerange: TimeRange, df_date_col: str = 'date def load_tickerdata_file(datadir: Path, pair: str, timeframe: str, - timerange: Optional[TimeRange] = None) -> Optional[list]: + timerange: Optional[TimeRange] = None) -> List[Dict]: """ Load a pair from file, either .json.gz or .json :return: tickerlist or None if unsuccessful @@ -128,39 +128,26 @@ def load_pair_history(pair: str, timeframe: str, datadir: Path, timerange: Optional[TimeRange] = None, - refresh_pairs: bool = False, - exchange: Optional[Exchange] = None, fill_up_missing: bool = True, drop_incomplete: bool = True, startup_candles: int = 0, ) -> DataFrame: """ - Loads cached ticker history for the given pair. + Load cached ticker history for the given pair. + :param pair: Pair to load data for :param timeframe: Ticker timeframe (e.g. "5m") :param datadir: Path to the data storage location. :param timerange: Limit data to be loaded to this timerange - :param refresh_pairs: Refresh pairs from exchange. - (Note: Requires exchange to be passed as well.) - :param exchange: Exchange object (needed when using "refresh_pairs") :param fill_up_missing: Fill missing values with "No action"-candles :param drop_incomplete: Drop last candle assuming it may be incomplete. :param startup_candles: Additional candles to load at the start of the period :return: DataFrame with ohlcv data, or empty DataFrame """ - timerange_startup = deepcopy(timerange) if startup_candles > 0 and timerange_startup: timerange_startup.subtract_start(timeframe_to_seconds(timeframe) * startup_candles) - # The user forced the refresh of pairs - if refresh_pairs: - download_pair_history(datadir=datadir, - exchange=exchange, - pair=pair, - timeframe=timeframe, - timerange=timerange) - pairdata = load_tickerdata_file(datadir, pair, timeframe, timerange=timerange_startup) if pairdata: @@ -180,30 +167,22 @@ def load_pair_history(pair: str, def load_data(datadir: Path, timeframe: str, pairs: List[str], - refresh_pairs: bool = False, - exchange: Optional[Exchange] = None, timerange: Optional[TimeRange] = None, fill_up_missing: bool = True, startup_candles: int = 0, fail_without_data: bool = False ) -> Dict[str, DataFrame]: """ - Loads ticker history data for a list of pairs + Load ticker history data for a list of pairs. + :param datadir: Path to the data storage location. :param timeframe: Ticker Timeframe (e.g. "5m") :param pairs: List of pairs to load - :param refresh_pairs: Refresh pairs from exchange. - (Note: Requires exchange to be passed as well.) - :param exchange: Exchange object (needed when using "refresh_pairs") :param timerange: Limit data to be loaded to this timerange :param fill_up_missing: Fill missing values with "No action"-candles :param startup_candles: Additional candles to load at the start of the period :param fail_without_data: Raise OperationalException if no data is found. :return: dict(:) - TODO: refresh_pairs is still used by edge to keep the data uptodate. - This should be replaced in the future. Instead, writing the current candles to disk - from dataprovider should be implemented, as this would avoid loading ohlcv data twice. - exchange and refresh_pairs are then not needed here nor in load_pair_history. """ result: Dict[str, DataFrame] = {} if startup_candles > 0 and timerange: @@ -212,8 +191,6 @@ def load_data(datadir: Path, for pair in pairs: hist = load_pair_history(pair=pair, timeframe=timeframe, datadir=datadir, timerange=timerange, - refresh_pairs=refresh_pairs, - exchange=exchange, fill_up_missing=fill_up_missing, startup_candles=startup_candles) if not hist.empty: @@ -224,6 +201,27 @@ def load_data(datadir: Path, return result +def refresh_data(datadir: Path, + timeframe: str, + pairs: List[str], + exchange: Exchange, + timerange: Optional[TimeRange] = None, + ) -> None: + """ + Refresh ticker history data for a list of pairs. + + :param datadir: Path to the data storage location. + :param timeframe: Ticker Timeframe (e.g. "5m") + :param pairs: List of pairs to load + :param exchange: Exchange object + :param timerange: Limit data to be loaded to this timerange + """ + for pair in pairs: + _download_pair_history(pair=pair, timeframe=timeframe, + datadir=datadir, timerange=timerange, + exchange=exchange) + + def pair_data_filename(datadir: Path, pair: str, timeframe: str) -> Path: pair_s = pair.replace("/", "_") filename = datadir.joinpath(f'{pair_s}-{timeframe}.json') @@ -277,11 +275,11 @@ def _load_cached_data_for_updating(datadir: Path, pair: str, timeframe: str, return (data, since_ms) -def download_pair_history(datadir: Path, - exchange: Optional[Exchange], - pair: str, - timeframe: str = '5m', - timerange: Optional[TimeRange] = None) -> bool: +def _download_pair_history(datadir: Path, + exchange: Exchange, + pair: str, + timeframe: str = '5m', + timerange: Optional[TimeRange] = None) -> bool: """ Download latest candles from the exchange for the pair and timeframe passed in parameters The data is downloaded starting from the last correct data that @@ -295,11 +293,6 @@ def download_pair_history(datadir: Path, :param timerange: range of time to download :return: bool with success state """ - if not exchange: - raise OperationalException( - "Exchange needs to be initialized when downloading pair history data" - ) - try: logger.info( f'Download history data for pair: "{pair}", timeframe: {timeframe} ' @@ -312,11 +305,12 @@ def download_pair_history(datadir: Path, logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None') # Default since_ms to 30 days if nothing is given - new_data = exchange.get_historic_ohlcv(pair=pair, timeframe=timeframe, - since_ms=since_ms if since_ms - else + new_data = exchange.get_historic_ohlcv(pair=pair, + timeframe=timeframe, + since_ms=since_ms if since_ms else int(arrow.utcnow().shift( - days=-30).float_timestamp) * 1000) + days=-30).float_timestamp) * 1000 + ) data.extend(new_data) logger.debug("New Start: %s", misc.format_ms_time(data[0][0])) @@ -334,12 +328,12 @@ def download_pair_history(datadir: Path, def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str], - dl_path: Path, timerange: Optional[TimeRange] = None, + datadir: Path, timerange: Optional[TimeRange] = None, erase=False) -> List[str]: """ Refresh stored ohlcv data for backtesting and hyperopt operations. - Used by freqtrade download-data - :return: Pairs not available + Used by freqtrade download-data subcommand. + :return: List of pairs that are not available. """ pairs_not_available = [] for pair in pairs: @@ -349,23 +343,23 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes continue for timeframe in timeframes: - dl_file = pair_data_filename(dl_path, pair, timeframe) + dl_file = pair_data_filename(datadir, pair, timeframe) if erase and dl_file.exists(): logger.info( f'Deleting existing data for pair {pair}, interval {timeframe}.') dl_file.unlink() logger.info(f'Downloading pair {pair}, interval {timeframe}.') - download_pair_history(datadir=dl_path, exchange=exchange, - pair=pair, timeframe=str(timeframe), - timerange=timerange) + _download_pair_history(datadir=datadir, exchange=exchange, + pair=pair, timeframe=str(timeframe), + timerange=timerange) return pairs_not_available -def download_trades_history(datadir: Path, - exchange: Exchange, - pair: str, - timerange: Optional[TimeRange] = None) -> bool: +def _download_trades_history(datadir: Path, + exchange: Exchange, + pair: str, + timerange: Optional[TimeRange] = None) -> bool: """ Download trade history from the exchange. Appends to previously downloaded trades data. @@ -381,11 +375,11 @@ def download_trades_history(datadir: Path, logger.debug("Current Start: %s", trades[0]['datetime'] if trades else 'None') logger.debug("Current End: %s", trades[-1]['datetime'] if trades else 'None') + # Default since_ms to 30 days if nothing is given new_trades = exchange.get_historic_trades(pair=pair, since=since if since else int(arrow.utcnow().shift( days=-30).float_timestamp) * 1000, - # until=xxx, from_id=from_id, ) trades.extend(new_trades[1]) @@ -407,9 +401,9 @@ def download_trades_history(datadir: Path, def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path, timerange: TimeRange, erase=False) -> List[str]: """ - Refresh stored trades data. - Used by freqtrade download-data - :return: Pairs not available + Refresh stored trades data for backtesting and hyperopt operations. + Used by freqtrade download-data subcommand. + :return: List of pairs that are not available. """ pairs_not_available = [] for pair in pairs: @@ -425,9 +419,9 @@ def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: dl_file.unlink() logger.info(f'Downloading trades for pair {pair}.') - download_trades_history(datadir=datadir, exchange=exchange, - pair=pair, - timerange=timerange) + _download_trades_history(datadir=datadir, exchange=exchange, + pair=pair, + timerange=timerange) return pairs_not_available @@ -448,22 +442,23 @@ def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str], store_tickerdata_file(datadir, pair, timeframe, data=ohlcv) -def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: +def get_timerange(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: """ - Get the maximum timeframe for the given backtest data + Get the maximum common timerange for the given backtest data. + :param data: dictionary with preprocessed backtesting data :return: tuple containing min_date, max_date """ - timeframe = [ + timeranges = [ (arrow.get(frame['date'].min()), arrow.get(frame['date'].max())) for frame in data.values() ] - return min(timeframe, key=operator.itemgetter(0))[0], \ - max(timeframe, key=operator.itemgetter(1))[1] + return (min(timeranges, key=operator.itemgetter(0))[0], + max(timeranges, key=operator.itemgetter(1))[1]) def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime, - max_date: datetime, timeframe_mins: int) -> bool: + max_date: datetime, timeframe_min: int) -> bool: """ Validates preprocessed backtesting data for missing values and shows warnings about it that. @@ -471,10 +466,10 @@ def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime, :param pair: pair used for log output. :param min_date: start-date of the data :param max_date: end-date of the data - :param timeframe_mins: ticker Timeframe in minutes + :param timeframe_min: ticker Timeframe in minutes """ # total difference in minutes / timeframe-minutes - expected_frames = int((max_date - min_date).total_seconds() // 60 // timeframe_mins) + expected_frames = int((max_date - min_date).total_seconds() // 60 // timeframe_min) found_missing = False dflen = len(data) if dflen < expected_frames: diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index afd20cf61..e56071a98 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -80,7 +80,7 @@ class Edge: if config.get('fee'): self.fee = config['fee'] else: - self.fee = self.exchange.get_fee() + self.fee = self.exchange.get_fee(symbol=self.config['exchange']['pair_whitelist'][0]) def calculate(self) -> bool: pairs = self.config['exchange']['pair_whitelist'] @@ -94,12 +94,19 @@ class Edge: logger.info('Using stake_currency: %s ...', self.config['stake_currency']) logger.info('Using local backtesting data (using whitelist in given config) ...') + if self._refresh_pairs: + history.refresh_data( + datadir=Path(self.config['datadir']), + pairs=pairs, + exchange=self.exchange, + timeframe=self.strategy.ticker_interval, + timerange=self._timerange, + ) + data = history.load_data( datadir=Path(self.config['datadir']), pairs=pairs, timeframe=self.strategy.ticker_interval, - refresh_pairs=self._refresh_pairs, - exchange=self.exchange, timerange=self._timerange, startup_candles=self.strategy.startup_candle_count, ) @@ -113,7 +120,7 @@ class Edge: preprocessed = self.strategy.tickerdata_to_dataframe(data) # Print timeframe - min_date, max_date = history.get_timeframe(preprocessed) + min_date, max_date = history.get_timerange(preprocessed) logger.info( 'Measuring data from %s up to %s (%s days) ...', min_date.isoformat(), diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 30868df07..e4e7aacce 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -18,7 +18,7 @@ from ccxt.base.decimal_to_precision import ROUND_DOWN, ROUND_UP from pandas import DataFrame from freqtrade import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError, constants) + OperationalException, TemporaryError) from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async from freqtrade.misc import deep_merge_dicts @@ -379,15 +379,16 @@ 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)}' + _amount = self.symbol_amount_prec(pair, amount) dry_order = { "id": order_id, 'pair': pair, 'price': rate, - 'amount': amount, - "cost": amount * rate, + 'amount': _amount, + "cost": _amount * rate, 'type': ordertype, 'side': side, - 'remaining': amount, + 'remaining': _amount, 'datetime': arrow.utcnow().isoformat(), 'status': "closed" if ordertype == "market" else "open", 'fee': None, @@ -478,7 +479,7 @@ class Exchange: @retrier def get_balance(self, currency: str) -> float: if self._config['dry_run']: - return constants.DRY_RUN_WALLET + return self._config['dry_run_wallet'] # ccxt exception is already handled by get_balances balances = self.get_balances() @@ -920,7 +921,7 @@ class Exchange: raise OperationalException(e) from e @retrier - def get_fee(self, symbol='ETH/BTC', type='', side='', amount=1, + def get_fee(self, symbol, type='', side='', amount=1, price=1, taker_or_maker='maker') -> float: try: # validate that markets are loaded before trying to get fee diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3ebe89a71..4a48bba04 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -62,7 +62,11 @@ class FreqtradeBot: self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange + persistence.init(self.config.get('db_url', None), + clean_open_orders=self.config.get('dry_run', False)) + self.wallets = Wallets(self.config, self.exchange) + self.dataprovider = DataProvider(self.config, self.exchange) # Attach Dataprovider to Strategy baseclass @@ -78,9 +82,6 @@ class FreqtradeBot: self.active_pair_whitelist = self._refresh_whitelist() - persistence.init(self.config.get('db_url', None), - clean_open_orders=self.config.get('dry_run', False)) - # Set initial bot state from config initial_state = self.config.get('initial_state') self.state = State[initial_state.upper()] if initial_state else State.STOPPED @@ -231,8 +232,8 @@ class FreqtradeBot: # Check if stake_amount is fulfilled if available_amount < stake_amount: raise DependencyException( - f"Available balance({available_amount} {self.config['stake_currency']}) is " - f"lower than stake amount({stake_amount} {self.config['stake_currency']})" + f"Available balance ({available_amount} {self.config['stake_currency']}) is " + f"lower than stake amount ({stake_amount} {self.config['stake_currency']})" ) return stake_amount @@ -554,6 +555,7 @@ class FreqtradeBot: order['amount'] = new_amount # Fee was applied, so set to 0 trade.fee_open = 0 + trade.recalc_open_trade_price() except DependencyException as exception: logger.warning("Could not update trade amount: %s", exception) @@ -849,6 +851,7 @@ class FreqtradeBot: trade.amount = new_amount # Fee was applied, so set to 0 trade.fee_open = 0 + trade.recalc_open_trade_price() except DependencyException as e: logger.warning("Could not update trade amount: %s", e) @@ -970,7 +973,7 @@ class FreqtradeBot: profit_trade = trade.calc_profit(rate=profit_rate) # Use cached ticker here - it was updated seconds ago. current_rate = self.get_sell_rate(trade.pair, False) - profit_percent = trade.calc_profit_percent(profit_rate) + profit_percent = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_percent > 0 else "loss" msg = { diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d9fb1f2d1..726257cdd 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -65,7 +65,7 @@ class Backtesting: if config.get('fee'): self.fee = config['fee'] else: - self.fee = self.exchange.get_fee() + self.fee = self.exchange.get_fee(symbol=self.config['exchange']['pair_whitelist'][0]) if self.config.get('runmode') != RunMode.HYPEROPT: self.dataprovider = DataProvider(self.config, self.exchange) @@ -87,7 +87,7 @@ class Backtesting: raise OperationalException("Ticker-interval needs to be set in either configuration " "or as cli argument `--ticker-interval 5m`") self.timeframe = str(self.config.get('ticker_interval')) - self.timeframe_mins = timeframe_to_minutes(self.timeframe) + self.timeframe_min = timeframe_to_minutes(self.timeframe) # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) @@ -117,7 +117,7 @@ class Backtesting: fail_without_data=True, ) - min_date, max_date = history.get_timeframe(data) + min_date, max_date = history.get_timerange(data) logger.info( 'Loading data from %s up to %s (%s days)..', @@ -261,6 +261,45 @@ class Backtesting: ticker[pair] = [x for x in ticker_data.itertuples()] return ticker + def _get_close_rate(self, sell_row, trade: Trade, sell, trade_dur) -> float: + """ + Get close rate for backtesting result + """ + # Special handling if high or low hit STOP_LOSS or ROI + if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): + # Set close_rate to stoploss + return trade.stop_loss + elif sell.sell_type == (SellType.ROI): + roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur) + if roi is not None: + if roi == -1 and roi_entry % self.timeframe_min == 0: + # When forceselling with ROI=-1, the roi time will always be equal to trade_dur. + # If that entry is a multiple of the timeframe (so on candle open) + # - we'll use open instead of close + return sell_row.open + + # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) + close_rate = - (trade.open_rate * roi + trade.open_rate * + (1 + trade.fee_open)) / (trade.fee_close - 1) + + if (trade_dur > 0 and trade_dur == roi_entry + and roi_entry % self.timeframe_min == 0 + and sell_row.open > close_rate): + # new ROI entry came into effect. + # use Open rate if open_rate > calculated sell rate + return sell_row.open + + # Use the maximum between close_rate and low as we + # cannot sell outside of a candle. + # Applies when a new ROI setting comes in place and the whole candle is above that. + return max(close_rate, sell_row.low) + + else: + # This should not be reached... + return sell_row.open + else: + return sell_row.open + def _get_sell_trade_entry( self, pair: str, buy_row: DataFrame, partial_ticker: List, trade_count_lock: Dict, @@ -287,29 +326,10 @@ class Backtesting: sell_row.sell, low=sell_row.low, high=sell_row.high) if sell.sell_flag: trade_dur = int((sell_row.date - buy_row.date).total_seconds() // 60) - # Special handling if high or low hit STOP_LOSS or ROI - if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): - # Set close_rate to stoploss - closerate = trade.stop_loss - elif sell.sell_type == (SellType.ROI): - roi = self.strategy.min_roi_reached_entry(trade_dur) - if roi is not None: - # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) - closerate = - (trade.open_rate * roi + trade.open_rate * - (1 + trade.fee_open)) / (trade.fee_close - 1) - - # Use the maximum between closerate and low as we - # cannot sell outside of a candle. - # Applies when using {"xx": -1} as roi to force sells after xx minutes - closerate = max(closerate, sell_row.low) - else: - # This should not be reached... - closerate = sell_row.open - else: - closerate = sell_row.open + closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) return BacktestResult(pair=pair, - profit_percent=trade.calc_profit_percent(rate=closerate), + profit_percent=trade.calc_profit_ratio(rate=closerate), profit_abs=trade.calc_profit(rate=closerate), open_time=buy_row.date, close_time=sell_row.date, @@ -325,7 +345,7 @@ class Backtesting: # no sell condition found - trade stil open at end of backtest period sell_row = partial_ticker[-1] bt_res = BacktestResult(pair=pair, - profit_percent=trade.calc_profit_percent(rate=sell_row.open), + profit_percent=trade.calc_profit_ratio(rate=sell_row.open), profit_abs=trade.calc_profit(rate=sell_row.open), open_time=buy_row.date, close_time=sell_row.date, @@ -378,7 +398,7 @@ class Backtesting: lock_pair_until: Dict = {} # Indexes per pair, so some pairs are allowed to have a missing start. indexes: Dict = {} - tmp = start_date + timedelta(minutes=self.timeframe_mins) + tmp = start_date + timedelta(minutes=self.timeframe_min) # Loop timerange and get candle for each pair at that point in time while tmp < end_date: @@ -430,7 +450,7 @@ class Backtesting: lock_pair_until[pair] = end_date.datetime # Move time one configured time_interval ahead. - tmp += timedelta(minutes=self.timeframe_mins) + tmp += timedelta(minutes=self.timeframe_min) return DataFrame.from_records(trades, columns=BacktestResult._fields) def start(self) -> None: @@ -461,7 +481,7 @@ class Backtesting: # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): preprocessed[pair] = history.trim_dataframe(df, timerange) - min_date, max_date = history.get_timeframe(preprocessed) + min_date, max_date = history.get_timerange(preprocessed) logger.info( 'Backtesting with data from %s up to %s (%s days)..', diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index ceed704c2..521a4d790 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -6,6 +6,7 @@ This module contains the hyperopt logic import locale import logging +import random import sys import warnings from collections import OrderedDict @@ -22,7 +23,7 @@ from joblib import (Parallel, cpu_count, delayed, dump, load, from pandas import DataFrame from freqtrade import OperationalException -from freqtrade.data.history import get_timeframe, trim_dataframe +from freqtrade.data.history import get_timerange, trim_dataframe from freqtrade.misc import plural, round_dict from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules @@ -368,7 +369,7 @@ class Hyperopt: processed = load(self.tickerdata_pickle) - min_date, max_date = get_timeframe(processed) + min_date, max_date = get_timerange(processed) backtesting_results = self.backtesting.backtest( { @@ -426,7 +427,7 @@ class Hyperopt: f"Avg profit {results_metrics['avg_profit']: 6.2f}%. " f"Total profit {results_metrics['total_profit']: 11.8f} {stake_cur} " f"({results_metrics['profit']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " - f"Avg duration {results_metrics['duration']:5.1f} mins." + f"Avg duration {results_metrics['duration']:5.1f} min." ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8') def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: @@ -436,7 +437,7 @@ class Hyperopt: acq_optimizer="auto", n_initial_points=INITIAL_POINTS, acq_optimizer_kwargs={'n_jobs': cpu_count}, - random_state=self.config.get('hyperopt_random_state', None), + random_state=self.random_state, ) def fix_optimizer_models_list(self): @@ -475,7 +476,13 @@ class Hyperopt: logger.info(f"Loaded {len(trials)} previous evaluations from disk.") return trials + def _set_random_state(self, random_state: Optional[int]) -> int: + return random_state or random.randint(1, 2**16 - 1) + def start(self) -> None: + self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None)) + logger.info(f"Using optimizer random state: {self.random_state}") + data, timerange = self.backtesting.load_bt_data() preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data) @@ -483,7 +490,7 @@ class Hyperopt: # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): preprocessed[pair] = trim_dataframe(df, timerange) - min_date, max_date = get_timeframe(data) + min_date, max_date = get_timerange(data) logger.info( 'Hyperopting with data from %s up to %s (%s days)..', diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index dbbdb8b91..856f3eee7 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -106,7 +106,7 @@ class IHyperOpt(ABC): roi_t_alpha = 1.0 roi_p_alpha = 1.0 - timeframe_mins = timeframe_to_minutes(IHyperOpt.ticker_interval) + timeframe_min = timeframe_to_minutes(IHyperOpt.ticker_interval) # We define here limits for the ROI space parameters automagically adapted to the # timeframe used by the bot: @@ -117,8 +117,8 @@ class IHyperOpt(ABC): # # The scaling is designed so that it maps exactly to the legacy Freqtrade roi_space() # method for the 5m ticker interval. - roi_t_scale = timeframe_mins / 5 - roi_p_scale = math.log1p(timeframe_mins) / math.log1p(5) + roi_t_scale = timeframe_min / 5 + roi_p_scale = math.log1p(timeframe_min) / math.log1p(5) roi_limits = { 'roi_t1_min': int(10 * roi_t_scale * roi_t_alpha), 'roi_t1_max': int(120 * roi_t_scale * roi_t_alpha), diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 735c740c3..993b68bc7 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -86,7 +86,7 @@ def check_migrate(engine) -> None: logger.debug(f'trying {table_back_name}') # Check for latest column - if not has_column(cols, 'stop_loss_pct'): + if not has_column(cols, 'open_trade_price'): logger.info(f'Running database migration - backup available as {table_back_name}') fee_open = get_column_def(cols, 'fee_open', 'fee') @@ -104,6 +104,8 @@ def check_migrate(engine) -> None: sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') ticker_interval = get_column_def(cols, 'ticker_interval', 'null') + open_trade_price = get_column_def(cols, 'open_trade_price', + f'amount * open_rate * (1 + {fee_open})') # Schema migration necessary engine.execute(f"alter table trades rename to {table_back_name}") @@ -121,7 +123,7 @@ def check_migrate(engine) -> None: stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stoploss_order_id, stoploss_last_update, max_rate, min_rate, sell_reason, strategy, - ticker_interval + ticker_interval, open_trade_price ) select id, lower(exchange), case @@ -140,7 +142,8 @@ def check_migrate(engine) -> None: {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, - {strategy} strategy, {ticker_interval} ticker_interval + {strategy} strategy, {ticker_interval} ticker_interval, + {open_trade_price} open_trade_price from {table_back_name} """) @@ -182,6 +185,8 @@ class Trade(_DECL_BASE): fee_close = Column(Float, nullable=False, default=0.0) open_rate = Column(Float) open_rate_requested = Column(Float) + # open_trade_price - calcuated via _calc_open_trade_price + open_trade_price = Column(Float) close_rate = Column(Float) close_rate_requested = Column(Float) close_profit = Column(Float) @@ -210,6 +215,10 @@ class Trade(_DECL_BASE): strategy = Column(String, nullable=True) ticker_interval = Column(Integer, nullable=True) + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.recalc_open_trade_price() + def __repr__(self): open_since = self.open_date.strftime('%Y-%m-%d %H:%M:%S') if self.is_open else 'closed' @@ -302,6 +311,7 @@ class Trade(_DECL_BASE): # Update open rate and actual amount self.open_rate = Decimal(order['price']) self.amount = Decimal(order['amount']) + self.recalc_open_trade_price() logger.info('%s_BUY has been fulfilled for %s.', order_type.upper(), self) self.open_order_id = None elif order_type in ('market', 'limit') and order['side'] == 'sell': @@ -322,7 +332,7 @@ class Trade(_DECL_BASE): and marks trade as closed """ self.close_rate = Decimal(rate) - self.close_profit = self.calc_profit_percent() + self.close_profit = self.calc_profit_ratio() self.close_date = datetime.utcnow() self.is_open = False self.open_order_id = None @@ -331,31 +341,36 @@ class Trade(_DECL_BASE): self ) - def calc_open_trade_price(self, fee: Optional[float] = None) -> float: + def _calc_open_trade_price(self) -> float: """ - Calculate the open_rate including fee. - :param fee: fee to use on the open rate (optional). - If rate is not set self.fee will be used + Calculate the open_rate including open_fee. :return: Price in of the open trade incl. Fees """ - buy_trade = (Decimal(self.amount) * Decimal(self.open_rate)) - fees = buy_trade * Decimal(fee or self.fee_open) + buy_trade = Decimal(self.amount) * Decimal(self.open_rate) + fees = buy_trade * Decimal(self.fee_open) return float(buy_trade + fees) + def recalc_open_trade_price(self) -> None: + """ + Recalculate open_trade_price. + Must be called whenever open_rate or fee_open is changed. + """ + self.open_trade_price = self._calc_open_trade_price() + def calc_close_trade_price(self, rate: Optional[float] = None, fee: Optional[float] = None) -> float: """ Calculate the close_rate including fee :param fee: fee to use on the close rate (optional). - If rate is not set self.fee will be used + If rate is not set self.fee will be used :param rate: rate to compare with (optional). - If rate is not set self.close_rate will be used + If rate is not set self.close_rate will be used :return: Price in BTC of the open trade """ if rate is None and not self.close_rate: return 0.0 - sell_trade = (Decimal(self.amount) * Decimal(rate or self.close_rate)) + sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) fees = sell_trade * Decimal(fee or self.fee_close) return float(sell_trade - fees) @@ -364,34 +379,32 @@ class Trade(_DECL_BASE): """ Calculate the absolute profit in stake currency between Close and Open trade :param fee: fee to use on the close rate (optional). - If rate is not set self.fee will be used + If rate is not set self.fee will be used :param rate: close rate to compare with (optional). - If rate is not set self.close_rate will be used + If rate is not set self.close_rate will be used :return: profit in stake currency as float """ - open_trade_price = self.calc_open_trade_price() close_trade_price = self.calc_close_trade_price( rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) - profit = close_trade_price - open_trade_price + profit = close_trade_price - self.open_trade_price return float(f"{profit:.8f}") - def calc_profit_percent(self, rate: Optional[float] = None, - fee: Optional[float] = None) -> float: + def calc_profit_ratio(self, rate: Optional[float] = None, + fee: Optional[float] = None) -> float: """ - Calculates the profit in percentage (including fee). + Calculates the profit as ratio (including fee). :param rate: rate to compare with (optional). - If rate is not set self.close_rate will be used + If rate is not set self.close_rate will be used :param fee: fee to use on the close rate (optional). - :return: profit in percentage as float + :return: profit ratio as float """ - open_trade_price = self.calc_open_trade_price() close_trade_price = self.calc_close_trade_price( rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) - profit_percent = (close_trade_price / open_trade_price) - 1 + profit_percent = (close_trade_price / self.open_trade_price) - 1 return float(f"{profit_percent:.8f}") @staticmethod diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 57a02dd6b..85089af9c 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -121,7 +121,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: ) # Create description for sell summarizing the trade desc = trades.apply(lambda row: f"{round(row['profitperc'], 3)}%, {row['sell_reason']}, " - f"{row['duration']}min", + f"{row['duration']} min", axis=1) trade_sells = go.Scatter( x=trades["close_time"], diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 4cebe646e..3b4b7570a 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -123,7 +123,7 @@ class RPC: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) except DependencyException: current_rate = NAN - current_profit = trade.calc_profit_percent(current_rate) + current_profit = trade.calc_profit_ratio(current_rate) fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%' if trade.close_profit else None) trade_dict = trade.to_json() @@ -151,7 +151,7 @@ class RPC: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) except DependencyException: current_rate = NAN - trade_perc = (100 * trade.calc_profit_percent(current_rate)) + trade_perc = (100 * trade.calc_profit_ratio(current_rate)) trade_profit = trade.calc_profit(current_rate) profit_str = f'{trade_perc:.2f}%' if self._fiat_converter: @@ -240,7 +240,7 @@ class RPC: durations.append((trade.close_date - trade.open_date).total_seconds()) if not trade.is_open: - profit_percent = trade.calc_profit_percent() + profit_percent = trade.calc_profit_ratio() profit_closed_coin.append(trade.calc_profit()) profit_closed_perc.append(profit_percent) else: @@ -249,7 +249,7 @@ class RPC: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) except DependencyException: current_rate = NAN - profit_percent = trade.calc_profit_percent(rate=current_rate) + profit_percent = trade.calc_profit_ratio(rate=current_rate) profit_all_coin.append( trade.calc_profit(rate=trade.close_rate or current_rate) @@ -348,6 +348,7 @@ class RPC: 'total': total, 'symbol': symbol, 'value': value, + 'note': 'Simulated balances' if self._freqtrade.config.get('dry_run', False) else '' } def _rpc_start(self) -> Dict[str, str]: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 51736968b..e0e2afd7b 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -331,7 +331,15 @@ class Telegram(RPC): try: result = self._rpc_balance(self._config['stake_currency'], self._config.get('fiat_display_currency', '')) + output = '' + if self._config['dry_run']: + output += ( + f"*Warning:*Simulated balances in Dry Mode.\n" + "This mode is still experimental!\n" + "Starting capital: " + f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" + ) for currency in result['currencies']: if currency['est_stake'] > 0.0001: curr_output = "*{currency}:*\n" \ @@ -587,14 +595,25 @@ class Telegram(RPC): :return: None """ val = self._rpc_show_config() + if val['trailing_stop']: + sl_info = ( + f"*Initial Stoploss:* `{val['stoploss']}`\n" + f"*Trailing stop positive:* `{val['trailing_stop_positive']}`\n" + f"*Trailing stop offset:* `{val['trailing_stop_positive_offset']}`\n" + f"*Only trail above offset:* `{val['trailing_only_offset_is_reached']}`\n" + ) + + else: + sl_info = f"*Stoploss:* `{val['stoploss']}`\n" + self._send_msg( f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n" f"*Exchange:* `{val['exchange']}`\n" f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n" f"*Minimum ROI:* `{val['minimal_roi']}`\n" - f"*{'Trailing ' if val['trailing_stop'] else ''}Stoploss:* `{val['stoploss']}`\n" + f"{sl_info}" f"*Ticker Interval:* `{val['ticker_interval']}`\n" - f"*Strategy:* `{val['strategy']}`'" + f"*Strategy:* `{val['strategy']}`" ) def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index e208138e7..985ff37de 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -296,7 +296,7 @@ class IStrategy(ABC): """ # Set current rate to low for backtesting sell current_rate = low or rate - current_profit = trade.calc_profit_percent(current_rate) + current_profit = trade.calc_profit_ratio(current_rate) trade.adjust_min_max_rates(high or current_rate) @@ -311,7 +311,7 @@ class IStrategy(ABC): # Set current rate to high for backtesting sell current_rate = high or rate - current_profit = trade.calc_profit_percent(current_rate) + current_profit = trade.calc_profit_ratio(current_rate) config_ask_strategy = self.config.get('ask_strategy', {}) if buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False): @@ -360,7 +360,7 @@ class IStrategy(ABC): sl_offset = self.trailing_stop_positive_offset # Make sure current_profit is calculated using high for backtesting. - high_profit = current_profit if not high else trade.calc_profit_percent(high) + high_profit = current_profit if not high else trade.calc_profit_ratio(high) # Don't update stoploss if trailing_only_offset_is_reached is true. if not (self.trailing_only_offset_is_reached and high_profit < sl_offset): @@ -394,7 +394,7 @@ class IStrategy(ABC): return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) - def min_roi_reached_entry(self, trade_dur: int) -> Optional[float]: + def min_roi_reached_entry(self, trade_dur: int) -> Tuple[Optional[int], Optional[float]]: """ Based on trade duration defines the ROI entry that may have been reached. :param trade_dur: trade duration in minutes @@ -403,9 +403,9 @@ class IStrategy(ABC): # Get highest entry in ROI dict where key <= trade-duration roi_list = list(filter(lambda x: x <= trade_dur, self.minimal_roi.keys())) if not roi_list: - return None + return None, None roi_entry = max(roi_list) - return self.minimal_roi[roi_entry] + return roi_entry, self.minimal_roi[roi_entry] def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: """ @@ -415,7 +415,7 @@ class IStrategy(ABC): """ # Check if time matches and current rate is above threshold trade_dur = int((current_time.timestamp() - trade.open_date.timestamp()) // 60) - roi = self.min_roi_reached_entry(trade_dur) + _, roi = self.min_roi_reached_entry(trade_dur) if roi is None: return False else: diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 230fcf268..9e01c7ea6 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -213,7 +213,7 @@ def start_download_data(args: Dict[str, Any]) -> None: else: pairs_not_available = refresh_backtest_ohlcv_data( exchange, pairs=config["pairs"], timeframes=config["timeframes"], - dl_path=Path(config['datadir']), timerange=timerange, erase=config.get("erase")) + datadir=Path(config['datadir']), timerange=timerange, erase=config.get("erase")) except KeyboardInterrupt: sys.exit("SIGINT received, aborting ...") diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index c674b5286..dd706438f 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -4,7 +4,7 @@ import logging from typing import Dict, NamedTuple, Any from freqtrade.exchange import Exchange -from freqtrade import constants +from freqtrade.persistence import Trade logger = logging.getLogger(__name__) @@ -23,14 +23,12 @@ class Wallets: self._config = config self._exchange = exchange self._wallets: Dict[str, Wallet] = {} + self.start_cap = config['dry_run_wallet'] self.update() def get_free(self, currency) -> float: - if self._config['dry_run']: - return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET) - balance = self._wallets.get(currency) if balance and balance.free: return balance.free @@ -39,9 +37,6 @@ class Wallets: def get_used(self, currency) -> float: - if self._config['dry_run']: - return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET) - balance = self._wallets.get(currency) if balance and balance.used: return balance.used @@ -50,16 +45,42 @@ class Wallets: def get_total(self, currency) -> float: - if self._config['dry_run']: - return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET) - balance = self._wallets.get(currency) if balance and balance.total: return balance.total else: return 0 - def update(self) -> None: + def _update_dry(self) -> None: + """ + Update from database in dry-run mode + - Apply apply profits of closed trades on top of stake amount + - Subtract currently tied up stake_amount in open trades + - update balances for currencies currently in trades + """ + closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all() + open_trades = Trade.get_trades(Trade.is_open.is_(True)).all() + tot_profit = sum([trade.calc_profit() for trade in closed_trades]) + tot_in_trades = sum([trade.stake_amount for trade in open_trades]) + + current_stake = self.start_cap + tot_profit - tot_in_trades + self._wallets[self._config['stake_currency']] = Wallet( + self._config['stake_currency'], + current_stake, + 0, + current_stake + ) + + for trade in open_trades: + curr = trade.pair.split('/')[0] + self._wallets[curr] = Wallet( + curr, + trade.amount, + 0, + trade.amount + ) + + def _update_live(self) -> None: balances = self._exchange.get_balances() @@ -71,6 +92,11 @@ class Wallets: balances[currency].get('total', None) ) + def update(self) -> None: + if self._config['dry_run']: + self._update_dry() + else: + self._update_live() logger.info('Wallets synced.') def get_all_balances(self) -> Dict[str, Any]: diff --git a/requirements-common.txt b/requirements-common.txt index f00425e5b..a6d9a6f5e 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,10 +1,10 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.20.46 +ccxt==1.20.84 SQLAlchemy==1.3.11 python-telegram-bot==12.2.0 arrow==0.15.4 -cachetools==3.1.1 +cachetools==4.0.0 requests==2.22.0 urllib3==1.25.7 wrapt==1.11.2 diff --git a/requirements-dev.txt b/requirements-dev.txt index f073ece6e..fe5b4e369 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.7.9 flake8-type-annotations==0.1.0 flake8-tidy-imports==3.1.0 mypy==0.750 -pytest==5.3.1 +pytest==5.3.2 pytest-asyncio==0.10.0 pytest-cov==2.8.1 pytest-mock==1.13.0 diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 2317cdf3e..b2428e37d 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -6,4 +6,4 @@ scipy==1.3.3 scikit-learn==0.22 scikit-optimize==0.5.2 filelock==3.0.12 -joblib==0.14.0 +joblib==0.14.1 diff --git a/requirements-plot.txt b/requirements-plot.txt index 87d5553b6..415d4b888 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.3.0 +plotly==4.4.1 diff --git a/tests/conftest.py b/tests/conftest.py index 020c29eb1..82111528e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1418,4 +1418,129 @@ def open_trade(): @pytest.fixture def hyperopt_results(): - return [{'loss': 0.4366182531161519, 'params_dict': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, 'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, 'results_metrics': {'trade_count': 2, 'avg_profit': -1.254995, 'total_profit': -0.00125625, 'profit': -2.50999, 'duration': 3930.0}, 'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 mins.', 'total_profit': -0.00125625, 'current_epoch': 1, 'is_initial_point': True, 'is_best': True}, {'loss': 20.0, 'params_dict': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, 'stoploss': {'stoploss': -0.338070047333259}}, 'results_metrics': {'trade_count': 1, 'avg_profit': 0.12357, 'total_profit': 6.185e-05, 'profit': 0.12357, 'duration': 1200.0}, 'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 mins.', 'total_profit': 6.185e-05, 'current_epoch': 2, 'is_initial_point': True, 'is_best': False}, {'loss': 14.241196856510731, 'params_dict': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 889, 'roi_t2': 533, 'roi_t3': 263, 'roi_p1': 0.04759065393663096, 'roi_p2': 0.1488819964638463, 'roi_p3': 0.4102801822104605, 'stoploss': -0.05394588767607611}, 'params_details': {'buy': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.6067528326109377, 263: 0.19647265040047726, 796: 0.04759065393663096, 1685: 0}, 'stoploss': {'stoploss': -0.05394588767607611}}, 'results_metrics': {'trade_count': 621, 'avg_profit': -0.43883302093397747, 'total_profit': -0.13639474, 'profit': -272.515306, 'duration': 1691.207729468599}, 'results_explanation': ' 621 trades. Avg profit -0.44%. Total profit -0.13639474 BTC (-272.52Σ%). Avg duration 1691.2 mins.', 'total_profit': -0.13639474, 'current_epoch': 3, 'is_initial_point': True, 'is_best': False}, {'loss': 100000, 'params_dict': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1402, 'roi_t2': 676, 'roi_t3': 215, 'roi_p1': 0.06264755784937427, 'roi_p2': 0.14258587851894644, 'roi_p3': 0.20671291201040828, 'stoploss': -0.11818343570194478}, 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan mins.', 'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_best': False}, {'loss': 0.22195522184191518, 'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3077646493708299, 444: 0.16227697603830155, 1045: 0.07280999507931168, 2314: 0}, 'stoploss': {'stoploss': -0.18181041180901014}}, 'results_metrics': {'trade_count': 14, 'avg_profit': -0.3539515, 'total_profit': -0.002480140000000001, 'profit': -4.955321, 'duration': 3402.8571428571427}, 'results_explanation': ' 14 trades. Avg profit -0.35%. Total profit -0.00248014 BTC ( -4.96Σ%). Avg duration 3402.9 mins.', 'total_profit': -0.002480140000000001, 'current_epoch': 5, 'is_initial_point': True, 'is_best': True}, {'loss': 0.545315889154162, 'params_dict': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower', 'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 319, 'roi_t2': 556, 'roi_t3': 216, 'roi_p1': 0.06251955472249589, 'roi_p2': 0.11659519602202795, 'roi_p3': 0.0953744132197762, 'stoploss': -0.024551752215582423}, 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.2744891639643, 216: 0.17911475074452382, 772: 0.06251955472249589, 1091: 0}, 'stoploss': {'stoploss': -0.024551752215582423}}, 'results_metrics': {'trade_count': 39, 'avg_profit': -0.21400679487179478, 'total_profit': -0.0041773, 'profit': -8.346264999999997, 'duration': 636.9230769230769}, 'results_explanation': ' 39 trades. Avg profit -0.21%. Total profit -0.00417730 BTC ( -8.35Σ%). Avg duration 636.9 mins.', 'total_profit': -0.0041773, 'current_epoch': 6, 'is_initial_point': True, 'is_best': False}, {'loss': 4.713497421432944, 'params_dict': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 771, 'roi_t2': 620, 'roi_t3': 145, 'roi_p1': 0.0586919200378493, 'roi_p2': 0.04984118697312542, 'roi_p3': 0.37521058680247044, 'stoploss': -0.14613268022709905}, 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, 'stoploss': {'stoploss': -0.14613268022709905}}, 'results_metrics': {'trade_count': 318, 'avg_profit': -0.39833954716981146, 'total_profit': -0.06339929, 'profit': -126.67197600000004, 'duration': 3140.377358490566}, 'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 mins.', 'total_profit': -0.06339929, 'current_epoch': 7, 'is_initial_point': True, 'is_best': False}, {'loss': 20.0, 'params_dict': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal', 'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 1149, 'roi_t2': 375, 'roi_t3': 289, 'roi_p1': 0.05571820757172588, 'roi_p2': 0.0606240398618907, 'roi_p3': 0.1729012220156157, 'stoploss': -0.1588514289110401}, 'params_details': {'buy': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.2892434694492323, 289: 0.11634224743361658, 664: 0.05571820757172588, 1813: 0}, 'stoploss': {'stoploss': -0.1588514289110401}}, 'results_metrics': {'trade_count': 1, 'avg_profit': 0.0, 'total_profit': 0.0, 'profit': 0.0, 'duration': 5340.0}, 'results_explanation': ' 1 trades. Avg profit 0.00%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration 5340.0 mins.', 'total_profit': 0.0, 'current_epoch': 8, 'is_initial_point': True, 'is_best': False}, {'loss': 2.4731817780991223, 'params_dict': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1012, 'roi_t2': 584, 'roi_t3': 422, 'roi_p1': 0.036764323603472565, 'roi_p2': 0.10335480573205287, 'roi_p3': 0.10322347377503042, 'stoploss': -0.2780610808108503}, 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.2433426031105559, 422: 0.14011912933552545, 1006: 0.036764323603472565, 2018: 0}, 'stoploss': {'stoploss': -0.2780610808108503}}, 'results_metrics': {'trade_count': 229, 'avg_profit': -0.38433433624454144, 'total_profit': -0.044050070000000004, 'profit': -88.01256299999999, 'duration': 6505.676855895196}, 'results_explanation': ' 229 trades. Avg profit -0.38%. Total profit -0.04405007 BTC ( -88.01Σ%). Avg duration 6505.7 mins.', 'total_profit': -0.044050070000000004, 'current_epoch': 9, 'is_initial_point': True, 'is_best': False}, {'loss': -0.2604606005845212, 'params_dict': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 792, 'roi_t2': 464, 'roi_t3': 215, 'roi_p1': 0.04594053535385903, 'roi_p2': 0.09623192684243963, 'roi_p3': 0.04428219070850663, 'stoploss': -0.16992287161634415}, 'params_details': {'buy': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.18645465290480528, 215: 0.14217246219629864, 679: 0.04594053535385903, 1471: 0}, 'stoploss': {'stoploss': -0.16992287161634415}}, 'results_metrics': {'trade_count': 4, 'avg_profit': 0.1080385, 'total_profit': 0.00021629, 'profit': 0.432154, 'duration': 2850.0}, 'results_explanation': ' 4 trades. Avg profit 0.11%. Total profit 0.00021629 BTC ( 0.43Σ%). Avg duration 2850.0 mins.', 'total_profit': 0.00021629, 'current_epoch': 10, 'is_initial_point': True, 'is_best': True}, {'loss': 4.876465945994304, 'params_dict': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 579, 'roi_t2': 614, 'roi_t3': 273, 'roi_p1': 0.05307643172744114, 'roi_p2': 0.1352282078262871, 'roi_p3': 0.1913307406325751, 'stoploss': -0.25728526022513887}, 'params_details': {'buy': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3796353801863034, 273: 0.18830463955372825, 887: 0.05307643172744114, 1466: 0}, 'stoploss': {'stoploss': -0.25728526022513887}}, 'results_metrics': {'trade_count': 117, 'avg_profit': -1.2698609145299145, 'total_profit': -0.07436117, 'profit': -148.573727, 'duration': 4282.5641025641025}, 'results_explanation': ' 117 trades. Avg profit -1.27%. Total profit -0.07436117 BTC (-148.57Σ%). Avg duration 4282.6 mins.', 'total_profit': -0.07436117, 'current_epoch': 11, 'is_initial_point': True, 'is_best': False}, {'loss': 100000, 'params_dict': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1156, 'roi_t2': 581, 'roi_t3': 408, 'roi_p1': 0.06860454019988212, 'roi_p2': 0.12473718444931989, 'roi_p3': 0.2896360635226823, 'stoploss': -0.30889015124682806}, 'params_details': {'buy': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4829777881718843, 408: 0.19334172464920202, 989: 0.06860454019988212, 2145: 0}, 'stoploss': {'stoploss': -0.30889015124682806}}, 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan mins.', 'total_profit': 0, 'current_epoch': 12, 'is_initial_point': True, 'is_best': False}] # noqa: E501 + return [ + { + 'loss': 0.4366182531161519, + 'params_dict': { + 'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501 + 'results_metrics': {'trade_count': 2, 'avg_profit': -1.254995, 'total_profit': -0.00125625, 'profit': -2.50999, 'duration': 3930.0}, # noqa: E501 + 'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501 + 'total_profit': -0.00125625, + 'current_epoch': 1, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 20.0, + 'params_dict': { + 'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, # noqa: E501 + 'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501 + 'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501 + 'stoploss': {'stoploss': -0.338070047333259}}, + 'results_metrics': {'trade_count': 1, 'avg_profit': 0.12357, 'total_profit': 6.185e-05, 'profit': 0.12357, 'duration': 1200.0}, # noqa: E501 + 'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 min.', # noqa: E501 + 'total_profit': 6.185e-05, + 'current_epoch': 2, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 14.241196856510731, + 'params_dict': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 889, 'roi_t2': 533, 'roi_t3': 263, 'roi_p1': 0.04759065393663096, 'roi_p2': 0.1488819964638463, 'roi_p3': 0.4102801822104605, 'stoploss': -0.05394588767607611}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.6067528326109377, 263: 0.19647265040047726, 796: 0.04759065393663096, 1685: 0}, 'stoploss': {'stoploss': -0.05394588767607611}}, # noqa: E501 + 'results_metrics': {'trade_count': 621, 'avg_profit': -0.43883302093397747, 'total_profit': -0.13639474, 'profit': -272.515306, 'duration': 1691.207729468599}, # noqa: E501 + 'results_explanation': ' 621 trades. Avg profit -0.44%. Total profit -0.13639474 BTC (-272.52Σ%). Avg duration 1691.2 min.', # noqa: E501 + 'total_profit': -0.13639474, + 'current_epoch': 3, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 100000, + 'params_dict': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1402, 'roi_t2': 676, 'roi_t3': 215, 'roi_p1': 0.06264755784937427, 'roi_p2': 0.14258587851894644, 'roi_p3': 0.20671291201040828, 'stoploss': -0.11818343570194478}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501 + 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501 + 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 + 'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_best': False + }, { + 'loss': 0.22195522184191518, + 'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3077646493708299, 444: 0.16227697603830155, 1045: 0.07280999507931168, 2314: 0}, 'stoploss': {'stoploss': -0.18181041180901014}}, # noqa: E501 + 'results_metrics': {'trade_count': 14, 'avg_profit': -0.3539515, 'total_profit': -0.002480140000000001, 'profit': -4.955321, 'duration': 3402.8571428571427}, # noqa: E501 + 'results_explanation': ' 14 trades. Avg profit -0.35%. Total profit -0.00248014 BTC ( -4.96Σ%). Avg duration 3402.9 min.', # noqa: E501 + 'total_profit': -0.002480140000000001, + 'current_epoch': 5, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 0.545315889154162, + 'params_dict': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower', 'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 319, 'roi_t2': 556, 'roi_t3': 216, 'roi_p1': 0.06251955472249589, 'roi_p2': 0.11659519602202795, 'roi_p3': 0.0953744132197762, 'stoploss': -0.024551752215582423}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.2744891639643, 216: 0.17911475074452382, 772: 0.06251955472249589, 1091: 0}, 'stoploss': {'stoploss': -0.024551752215582423}}, # noqa: E501 + 'results_metrics': {'trade_count': 39, 'avg_profit': -0.21400679487179478, 'total_profit': -0.0041773, 'profit': -8.346264999999997, 'duration': 636.9230769230769}, # noqa: E501 + 'results_explanation': ' 39 trades. Avg profit -0.21%. Total profit -0.00417730 BTC ( -8.35Σ%). Avg duration 636.9 min.', # noqa: E501 + 'total_profit': -0.0041773, + 'current_epoch': 6, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 4.713497421432944, + 'params_dict': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 771, 'roi_t2': 620, 'roi_t3': 145, 'roi_p1': 0.0586919200378493, 'roi_p2': 0.04984118697312542, 'roi_p3': 0.37521058680247044, 'stoploss': -0.14613268022709905}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501 + 'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501 + 'results_metrics': {'trade_count': 318, 'avg_profit': -0.39833954716981146, 'total_profit': -0.06339929, 'profit': -126.67197600000004, 'duration': 3140.377358490566}, # noqa: E501 + 'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501 + 'total_profit': -0.06339929, + 'current_epoch': 7, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 20.0, # noqa: E501 + 'params_dict': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal', 'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 1149, 'roi_t2': 375, 'roi_t3': 289, 'roi_p1': 0.05571820757172588, 'roi_p2': 0.0606240398618907, 'roi_p3': 0.1729012220156157, 'stoploss': -0.1588514289110401}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.2892434694492323, 289: 0.11634224743361658, 664: 0.05571820757172588, 1813: 0}, 'stoploss': {'stoploss': -0.1588514289110401}}, # noqa: E501 + 'results_metrics': {'trade_count': 1, 'avg_profit': 0.0, 'total_profit': 0.0, 'profit': 0.0, 'duration': 5340.0}, # noqa: E501 + 'results_explanation': ' 1 trades. Avg profit 0.00%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration 5340.0 min.', # noqa: E501 + 'total_profit': 0.0, + 'current_epoch': 8, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 2.4731817780991223, + 'params_dict': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1012, 'roi_t2': 584, 'roi_t3': 422, 'roi_p1': 0.036764323603472565, 'roi_p2': 0.10335480573205287, 'roi_p3': 0.10322347377503042, 'stoploss': -0.2780610808108503}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.2433426031105559, 422: 0.14011912933552545, 1006: 0.036764323603472565, 2018: 0}, 'stoploss': {'stoploss': -0.2780610808108503}}, # noqa: E501 + 'results_metrics': {'trade_count': 229, 'avg_profit': -0.38433433624454144, 'total_profit': -0.044050070000000004, 'profit': -88.01256299999999, 'duration': 6505.676855895196}, # noqa: E501 + 'results_explanation': ' 229 trades. Avg profit -0.38%. Total profit -0.04405007 BTC ( -88.01Σ%). Avg duration 6505.7 min.', # noqa: E501 + 'total_profit': -0.044050070000000004, # noqa: E501 + 'current_epoch': 9, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': -0.2604606005845212, # noqa: E501 + 'params_dict': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 792, 'roi_t2': 464, 'roi_t3': 215, 'roi_p1': 0.04594053535385903, 'roi_p2': 0.09623192684243963, 'roi_p3': 0.04428219070850663, 'stoploss': -0.16992287161634415}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.18645465290480528, 215: 0.14217246219629864, 679: 0.04594053535385903, 1471: 0}, 'stoploss': {'stoploss': -0.16992287161634415}}, # noqa: E501 + 'results_metrics': {'trade_count': 4, 'avg_profit': 0.1080385, 'total_profit': 0.00021629, 'profit': 0.432154, 'duration': 2850.0}, # noqa: E501 + 'results_explanation': ' 4 trades. Avg profit 0.11%. Total profit 0.00021629 BTC ( 0.43Σ%). Avg duration 2850.0 min.', # noqa: E501 + 'total_profit': 0.00021629, + 'current_epoch': 10, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 4.876465945994304, # noqa: E501 + 'params_dict': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 579, 'roi_t2': 614, 'roi_t3': 273, 'roi_p1': 0.05307643172744114, 'roi_p2': 0.1352282078262871, 'roi_p3': 0.1913307406325751, 'stoploss': -0.25728526022513887}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3796353801863034, 273: 0.18830463955372825, 887: 0.05307643172744114, 1466: 0}, 'stoploss': {'stoploss': -0.25728526022513887}}, # noqa: E501 + 'results_metrics': {'trade_count': 117, 'avg_profit': -1.2698609145299145, 'total_profit': -0.07436117, 'profit': -148.573727, 'duration': 4282.5641025641025}, # noqa: E501 + 'results_explanation': ' 117 trades. Avg profit -1.27%. Total profit -0.07436117 BTC (-148.57Σ%). Avg duration 4282.6 min.', # noqa: E501 + 'total_profit': -0.07436117, + 'current_epoch': 11, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 100000, + 'params_dict': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1156, 'roi_t2': 581, 'roi_t3': 408, 'roi_p1': 0.06860454019988212, 'roi_p2': 0.12473718444931989, 'roi_p3': 0.2896360635226823, 'stoploss': -0.30889015124682806}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4829777881718843, 408: 0.19334172464920202, 989: 0.06860454019988212, 2145: 0}, 'stoploss': {'stoploss': -0.30889015124682806}}, # noqa: E501 + 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501 + 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 + 'total_profit': 0, + 'current_epoch': 12, + 'is_initial_point': True, + 'is_best': False + } + ] diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 8184167b3..414551c95 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -2,7 +2,7 @@ import logging from freqtrade.data.converter import parse_ticker_dataframe, ohlcv_fill_up_missing_data -from freqtrade.data.history import load_pair_history, validate_backtest_data, get_timeframe +from freqtrade.data.history import load_pair_history, validate_backtest_data, get_timerange from tests.conftest import log_has @@ -36,7 +36,7 @@ def test_ohlcv_fill_up_missing_data(testdatadir, caplog): f"{len(data)} - after: {len(data2)}", caplog) # Test fillup actually fixes invalid backtest data - min_date, max_date = get_timeframe({'UNITTEST/BTC': data}) + min_date, max_date = get_timerange({'UNITTEST/BTC': data}) assert validate_backtest_data(data, 'UNITTEST/BTC', min_date, max_date, 1) assert not validate_backtest_data(data2, 'UNITTEST/BTC', min_date, max_date, 1) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 6d89ab7c5..7b3143db9 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -7,21 +7,21 @@ from shutil import copyfile from unittest.mock import MagicMock, PropertyMock import arrow -import pytest from pandas import DataFrame -from freqtrade import OperationalException from freqtrade.configuration import TimeRange -from freqtrade.data import history -from freqtrade.data.history import (_load_cached_data_for_updating, - convert_trades_to_ohlcv, - download_pair_history, - download_trades_history, +from freqtrade.data.history import (_download_pair_history, + _download_trades_history, + _load_cached_data_for_updating, + convert_trades_to_ohlcv, get_timerange, + load_data, load_pair_history, load_tickerdata_file, pair_data_filename, pair_trades_filename, refresh_backtest_ohlcv_data, refresh_backtest_trades_data, - trim_tickerlist) + refresh_data, + trim_dataframe, trim_tickerlist, + validate_backtest_data) from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import file_dump_json from freqtrade.strategy.default_strategy import DefaultStrategy @@ -64,7 +64,7 @@ def _clean_test_file(file: Path) -> None: def test_load_data_30min_ticker(mocker, caplog, default_conf, testdatadir) -> None: - ld = history.load_pair_history(pair='UNITTEST/BTC', timeframe='30m', datadir=testdatadir) + ld = load_pair_history(pair='UNITTEST/BTC', timeframe='30m', datadir=testdatadir) assert isinstance(ld, DataFrame) assert not log_has( 'Download history data for pair: "UNITTEST/BTC", timeframe: 30m ' @@ -73,7 +73,7 @@ def test_load_data_30min_ticker(mocker, caplog, default_conf, testdatadir) -> No def test_load_data_7min_ticker(mocker, caplog, default_conf, testdatadir) -> None: - ld = history.load_pair_history(pair='UNITTEST/BTC', timeframe='7m', datadir=testdatadir) + ld = load_pair_history(pair='UNITTEST/BTC', timeframe='7m', datadir=testdatadir) assert isinstance(ld, DataFrame) assert ld.empty assert log_has( @@ -86,7 +86,7 @@ def test_load_data_1min_ticker(ticker_history, mocker, caplog, testdatadir) -> N mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history) file = testdatadir / 'UNITTEST_BTC-1m.json' _backup_file(file, copy_file=True) - history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC']) + load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC']) assert file.is_file() assert not log_has( 'Download history data for pair: "UNITTEST/BTC", interval: 1m ' @@ -99,10 +99,9 @@ def test_load_data_startup_candles(mocker, caplog, default_conf, testdatadir) -> ltfmock = mocker.patch('freqtrade.data.history.load_tickerdata_file', MagicMock(return_value=None)) timerange = TimeRange('date', None, 1510639620, 0) - history.load_pair_history(pair='UNITTEST/BTC', timeframe='1m', - datadir=testdatadir, timerange=timerange, - startup_candles=20, - ) + load_pair_history(pair='UNITTEST/BTC', timeframe='1m', + datadir=testdatadir, timerange=timerange, + startup_candles=20,) assert ltfmock.call_count == 1 assert ltfmock.call_args_list[0][1]['timerange'] != timerange @@ -121,9 +120,7 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, _backup_file(file) # do not download a new pair if refresh_pairs isn't set - history.load_pair_history(datadir=testdatadir, - timeframe='1m', - pair='MEME/BTC') + load_pair_history(datadir=testdatadir, timeframe='1m', pair='MEME/BTC') assert not file.is_file() assert log_has( 'No history data for pair: "MEME/BTC", timeframe: 1m. ' @@ -131,22 +128,14 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, ) # download a new pair if refresh_pairs is set - history.load_pair_history(datadir=testdatadir, - timeframe='1m', - refresh_pairs=True, - exchange=exchange, - pair='MEME/BTC') + refresh_data(datadir=testdatadir, timeframe='1m', pairs=['MEME/BTC'], + exchange=exchange) + load_pair_history(datadir=testdatadir, timeframe='1m', pair='MEME/BTC') assert file.is_file() assert log_has_re( 'Download history data for pair: "MEME/BTC", timeframe: 1m ' 'and store in .*', caplog ) - with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'): - history.load_pair_history(datadir=testdatadir, - timeframe='1m', - refresh_pairs=True, - exchange=None, - pair='MEME/BTC') _clean_test_file(file) @@ -267,12 +256,12 @@ def test_download_pair_history(ticker_history_list, mocker, default_conf, testda assert not file1_1.is_file() assert not file2_1.is_file() - assert download_pair_history(datadir=testdatadir, exchange=exchange, - pair='MEME/BTC', - timeframe='1m') - assert download_pair_history(datadir=testdatadir, exchange=exchange, - pair='CFI/BTC', - timeframe='1m') + assert _download_pair_history(datadir=testdatadir, exchange=exchange, + pair='MEME/BTC', + timeframe='1m') + assert _download_pair_history(datadir=testdatadir, exchange=exchange, + pair='CFI/BTC', + timeframe='1m') assert not exchange._pairs_last_refresh_time assert file1_1.is_file() assert file2_1.is_file() @@ -284,12 +273,12 @@ def test_download_pair_history(ticker_history_list, mocker, default_conf, testda assert not file1_5.is_file() assert not file2_5.is_file() - assert download_pair_history(datadir=testdatadir, exchange=exchange, - pair='MEME/BTC', - timeframe='5m') - assert download_pair_history(datadir=testdatadir, exchange=exchange, - pair='CFI/BTC', - timeframe='5m') + assert _download_pair_history(datadir=testdatadir, exchange=exchange, + pair='MEME/BTC', + timeframe='5m') + assert _download_pair_history(datadir=testdatadir, exchange=exchange, + pair='CFI/BTC', + timeframe='5m') assert not exchange._pairs_last_refresh_time assert file1_5.is_file() assert file2_5.is_file() @@ -307,8 +296,8 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None: json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None) mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick) exchange = get_patched_exchange(mocker, default_conf) - download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='1m') - download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='3m') + _download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='1m') + _download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='3m') assert json_dump_mock.call_count == 2 @@ -324,9 +313,9 @@ def test_download_backtesting_data_exception(ticker_history, mocker, caplog, _backup_file(file1_1) _backup_file(file1_5) - assert not download_pair_history(datadir=testdatadir, exchange=exchange, - pair='MEME/BTC', - timeframe='1m') + assert not _download_pair_history(datadir=testdatadir, exchange=exchange, + pair='MEME/BTC', + timeframe='1m') # clean files freshly downloaded _clean_test_file(file1_1) _clean_test_file(file1_5) @@ -351,10 +340,8 @@ def test_load_partial_missing(testdatadir, caplog) -> None: # Make sure we start fresh - test missing data at start start = arrow.get('2018-01-01T00:00:00') end = arrow.get('2018-01-11T00:00:00') - tickerdata = history.load_data(testdatadir, '5m', ['UNITTEST/BTC'], - startup_candles=20, - timerange=TimeRange('date', 'date', - start.timestamp, end.timestamp)) + tickerdata = load_data(testdatadir, '5m', ['UNITTEST/BTC'], startup_candles=20, + timerange=TimeRange('date', 'date', start.timestamp, end.timestamp)) assert log_has( 'Using indicator startup period: 20 ...', caplog ) @@ -369,10 +356,8 @@ def test_load_partial_missing(testdatadir, caplog) -> None: caplog.clear() start = arrow.get('2018-01-10T00:00:00') end = arrow.get('2018-02-20T00:00:00') - tickerdata = history.load_data(datadir=testdatadir, timeframe='5m', - pairs=['UNITTEST/BTC'], - timerange=TimeRange('date', 'date', - start.timestamp, end.timestamp)) + tickerdata = load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], + timerange=TimeRange('date', 'date', start.timestamp, end.timestamp)) # timedifference in 5 minutes td = ((end - start).total_seconds() // 60 // 5) + 1 assert td != len(tickerdata['UNITTEST/BTC']) @@ -384,12 +369,24 @@ def test_load_partial_missing(testdatadir, caplog) -> None: def test_init(default_conf, mocker) -> None: - exchange = get_patched_exchange(mocker, default_conf) - assert {} == history.load_data( + assert {} == load_data( + datadir='', + pairs=[], + timeframe=default_conf['ticker_interval'] + ) + + +def test_init_with_refresh(default_conf, mocker) -> None: + exchange = get_patched_exchange(mocker, default_conf) + refresh_data( + datadir='', + pairs=[], + timeframe=default_conf['ticker_interval'], + exchange=exchange + ) + assert {} == load_data( datadir='', - exchange=exchange, pairs=[], - refresh_pairs=True, timeframe=default_conf['ticker_interval'] ) @@ -447,7 +444,7 @@ def test_trim_tickerlist(testdatadir) -> None: def test_trim_dataframe(testdatadir) -> None: - data = history.load_data( + data = load_data( datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'] @@ -458,7 +455,7 @@ def test_trim_dataframe(testdatadir) -> None: # Remove first 30 minutes (1800 s) tr = TimeRange('date', None, min_date + 1800, 0) - data_modify = history.trim_dataframe(data_modify, tr) + data_modify = trim_dataframe(data_modify, tr) assert not data_modify.equals(data) assert len(data_modify) < len(data) assert len(data_modify) == len(data) - 30 @@ -468,7 +465,7 @@ def test_trim_dataframe(testdatadir) -> None: data_modify = data.copy() # Remove last 30 minutes (1800 s) tr = TimeRange(None, 'date', 0, max_date - 1800) - data_modify = history.trim_dataframe(data_modify, tr) + data_modify = trim_dataframe(data_modify, tr) assert not data_modify.equals(data) assert len(data_modify) < len(data) assert len(data_modify) == len(data) - 30 @@ -478,7 +475,7 @@ def test_trim_dataframe(testdatadir) -> None: data_modify = data.copy() # Remove first 25 and last 30 minutes (1800 s) tr = TimeRange('date', 'date', min_date + 1500, max_date - 1800) - data_modify = history.trim_dataframe(data_modify, tr) + data_modify = trim_dataframe(data_modify, tr) assert not data_modify.equals(data) assert len(data_modify) < len(data) assert len(data_modify) == len(data) - 55 @@ -510,18 +507,18 @@ def test_file_dump_json_tofile(testdatadir) -> None: _clean_test_file(file) -def test_get_timeframe(default_conf, mocker, testdatadir) -> None: +def test_get_timerange(default_conf, mocker, testdatadir) -> None: patch_exchange(mocker) strategy = DefaultStrategy(default_conf) data = strategy.tickerdata_to_dataframe( - history.load_data( + load_data( datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'] ) ) - min_date, max_date = history.get_timeframe(data) + min_date, max_date = get_timerange(data) assert min_date.isoformat() == '2017-11-04T23:02:00+00:00' assert max_date.isoformat() == '2017-11-14T22:58:00+00:00' @@ -531,17 +528,17 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) strategy = DefaultStrategy(default_conf) data = strategy.tickerdata_to_dataframe( - history.load_data( + load_data( datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'], fill_up_missing=False ) ) - min_date, max_date = history.get_timeframe(data) + min_date, max_date = get_timerange(data) caplog.clear() - assert history.validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC', - min_date, max_date, timeframe_to_minutes('1m')) + assert validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC', + min_date, max_date, timeframe_to_minutes('1m')) assert len(caplog.record_tuples) == 1 assert log_has( "UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values", @@ -554,7 +551,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No timerange = TimeRange('index', 'index', 200, 250) data = strategy.tickerdata_to_dataframe( - history.load_data( + load_data( datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], @@ -562,15 +559,15 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No ) ) - min_date, max_date = history.get_timeframe(data) + min_date, max_date = get_timerange(data) caplog.clear() - assert not history.validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC', - min_date, max_date, timeframe_to_minutes('5m')) + assert not validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC', + min_date, max_date, timeframe_to_minutes('5m')) assert len(caplog.record_tuples) == 0 def test_refresh_backtest_ohlcv_data(mocker, default_conf, markets, caplog, testdatadir): - dl_mock = mocker.patch('freqtrade.data.history.download_pair_history', MagicMock()) + dl_mock = mocker.patch('freqtrade.data.history._download_pair_history', MagicMock()) mocker.patch( 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) ) @@ -580,7 +577,7 @@ def test_refresh_backtest_ohlcv_data(mocker, default_conf, markets, caplog, test ex = get_patched_exchange(mocker, default_conf) timerange = TimeRange.parse_timerange("20190101-20190102") refresh_backtest_ohlcv_data(exchange=ex, pairs=["ETH/BTC", "XRP/BTC"], - timeframes=["1m", "5m"], dl_path=testdatadir, + timeframes=["1m", "5m"], datadir=testdatadir, timerange=timerange, erase=True ) @@ -591,7 +588,7 @@ def test_refresh_backtest_ohlcv_data(mocker, default_conf, markets, caplog, test def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir): - dl_mock = mocker.patch('freqtrade.data.history.download_pair_history', MagicMock()) + dl_mock = mocker.patch('freqtrade.data.history._download_pair_history', MagicMock()) ex = get_patched_exchange(mocker, default_conf) mocker.patch( @@ -600,7 +597,7 @@ def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir): timerange = TimeRange.parse_timerange("20190101-20190102") unav_pairs = refresh_backtest_ohlcv_data(exchange=ex, pairs=["BTT/BTC", "LTC/USDT"], timeframes=["1m", "5m"], - dl_path=testdatadir, + datadir=testdatadir, timerange=timerange, erase=False ) @@ -611,7 +608,7 @@ def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir): def test_refresh_backtest_trades_data(mocker, default_conf, markets, caplog, testdatadir): - dl_mock = mocker.patch('freqtrade.data.history.download_trades_history', MagicMock()) + dl_mock = mocker.patch('freqtrade.data.history._download_trades_history', MagicMock()) mocker.patch( 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) ) @@ -646,8 +643,8 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad assert not file1.is_file() - assert download_trades_history(datadir=testdatadir, exchange=exchange, - pair='ETH/BTC') + assert _download_trades_history(datadir=testdatadir, exchange=exchange, + pair='ETH/BTC') assert log_has("New Amount of trades: 5", caplog) assert file1.is_file() @@ -657,8 +654,8 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad mocker.patch('freqtrade.exchange.Exchange.get_historic_trades', MagicMock(side_effect=ValueError)) - assert not download_trades_history(datadir=testdatadir, exchange=exchange, - pair='ETH/BTC') + assert not _download_trades_history(datadir=testdatadir, exchange=exchange, + pair='ETH/BTC') assert log_has_re('Failed to download historic trades for pair: "ETH/BTC".*', caplog) @@ -668,12 +665,8 @@ def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog): file1 = testdatadir / 'XRP_ETH-1m.json' file5 = testdatadir / 'XRP_ETH-5m.json' # Compare downloaded dataset with converted dataset - dfbak_1m = history.load_pair_history(datadir=testdatadir, - timeframe="1m", - pair=pair) - dfbak_5m = history.load_pair_history(datadir=testdatadir, - timeframe="5m", - pair=pair) + dfbak_1m = load_pair_history(datadir=testdatadir, timeframe="1m", pair=pair) + dfbak_5m = load_pair_history(datadir=testdatadir, timeframe="5m", pair=pair) _backup_file(file1, copy_file=True) _backup_file(file5) @@ -685,12 +678,8 @@ def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog): assert log_has("Deleting existing data for pair XRP/ETH, interval 1m.", caplog) # Load new data - df_1m = history.load_pair_history(datadir=testdatadir, - timeframe="1m", - pair=pair) - df_5m = history.load_pair_history(datadir=testdatadir, - timeframe="5m", - pair=pair) + df_1m = load_pair_history(datadir=testdatadir, timeframe="1m", pair=pair) + df_5m = load_pair_history(datadir=testdatadir, timeframe="5m", pair=pair) assert df_1m.equals(dfbak_1m) assert df_5m.equals(dfbak_5m) diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index 001dc9591..3a866c0a8 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -255,8 +255,8 @@ def test_edge_heartbeat_calculate(mocker, edge_conf): assert edge.calculate() is False -def mocked_load_data(datadir, pairs=[], timeframe='0m', refresh_pairs=False, - timerange=None, exchange=None, *args, **kwargs): +def mocked_load_data(datadir, pairs=[], timeframe='0m', + timerange=None, *args, **kwargs): hz = 0.1 base = 0.001 @@ -290,6 +290,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m', refresh_pairs=False, def test_edge_process_downloaded_data(mocker, edge_conf): freqtrade = get_patched_freqtradebot(mocker, edge_conf) mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch('freqtrade.data.history.refresh_data', MagicMock()) mocker.patch('freqtrade.data.history.load_data', mocked_load_data) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) @@ -301,6 +302,7 @@ def test_edge_process_downloaded_data(mocker, edge_conf): def test_edge_process_no_data(mocker, edge_conf, caplog): freqtrade = get_patched_freqtradebot(mocker, edge_conf) mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch('freqtrade.data.history.refresh_data', MagicMock()) mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={})) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) @@ -313,6 +315,7 @@ def test_edge_process_no_data(mocker, edge_conf, caplog): def test_edge_process_no_trades(mocker, edge_conf, caplog): freqtrade = get_patched_freqtradebot(mocker, edge_conf) mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch('freqtrade.data.history.refresh_data', MagicMock()) mocker.patch('freqtrade.data.history.load_data', mocked_load_data) # Return empty mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', MagicMock(return_value=[])) @@ -334,7 +337,7 @@ def test_process_expectancy(mocker, edge_conf): edge_conf['edge']['min_trade_number'] = 2 freqtrade = get_patched_freqtradebot(mocker, edge_conf) - def get_fee(): + def get_fee(*args, **kwargs): return 0.001 freqtrade.exchange.get_fee = get_fee diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index a21a5f3ac..629f99aa2 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -876,6 +876,7 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): def test_get_balance_dry_run(default_conf, mocker): default_conf['dry_run'] = True + default_conf['dry_run_wallet'] = 999.9 exchange = get_patched_exchange(mocker, default_conf) assert exchange.get_balance(currency='BTC') == 999.9 @@ -1646,10 +1647,10 @@ def test_get_fee(default_conf, mocker, exchange_name): }) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - assert exchange.get_fee() == 0.025 + assert exchange.get_fee('ETH/BTC') == 0.025 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - 'get_fee', 'calculate_fee') + 'get_fee', 'calculate_fee', symbol="ETH/BTC") def test_stoploss_limit_order_unsupported_exchange(default_conf, mocker): diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 3f6cc8c9a..47cb9f353 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock import pytest -from freqtrade.data.history import get_timeframe +from freqtrade.data.history import get_timerange from freqtrade.optimize.backtesting import Backtesting from freqtrade.strategy.interface import SellType from tests.conftest import patch_exchange @@ -250,7 +250,7 @@ tc15 = BTContainer(data=[ BTrade(sell_reason=SellType.STOP_LOSS, open_tick=2, close_tick=2)] ) -# Test 16: Buy, hold for 65 mins, then forcesell using roi=-1 +# Test 16: Buy, hold for 65 min, then forcesell using roi=-1 # Causes negative profit even though sell-reason is ROI. # stop-loss: 10%, ROI: 10% (should not apply), -100% after 65 minutes (limits trade duration) tc16 = BTContainer(data=[ @@ -265,6 +265,69 @@ tc16 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] ) +# Test 17: Buy, hold for 120 mins, then forcesell using roi=-1 +# Causes negative profit even though sell-reason is ROI. +# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration) +# Uses open as sell-rate (special case) - since the roi-time is a multiple of the ticker interval. +tc17 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], + [2, 4987, 5300, 4950, 5050, 6172, 0, 0], + [3, 4980, 5000, 4940, 4962, 6172, 0, 0], # ForceSell on ROI (roi=-1) + [4, 4962, 4987, 4972, 4950, 6172, 0, 0], + [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10, "120": -1}, profit_perc=-0.004, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] +) + + +# Test 18: Buy, hold for 120 mins, then drop ROI to 1%, causing a sell in candle 3. +# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration) +# uses open_rate as sell-price +tc18 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], + [2, 4987, 5300, 4950, 5200, 6172, 0, 0], + [3, 5200, 5220, 4940, 4962, 6172, 0, 0], # Sell on ROI (sells on open) + [4, 4962, 4987, 4972, 4950, 6172, 0, 0], + [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10, "120": 0.01}, profit_perc=0.04, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] +) + +# Test 19: Buy, hold for 119 mins, then drop ROI to 1%, causing a sell in candle 3. +# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration) +# uses calculated ROI (1%) as sell rate, otherwise identical to tc18 +tc19 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], + [2, 4987, 5300, 4950, 5200, 6172, 0, 0], + [3, 5000, 5300, 4940, 4962, 6172, 0, 0], # Sell on ROI + [4, 4962, 4987, 4972, 4950, 6172, 0, 0], + [5, 4550, 4975, 4925, 4950, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10, "120": 0.01}, profit_perc=0.01, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] +) + +# Test 20: Buy, hold for 119 mins, then drop ROI to 1%, causing a sell in candle 3. +# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration) +# uses calculated ROI (1%) as sell rate, otherwise identical to tc18 +tc20 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], + [2, 4987, 5300, 4950, 5200, 6172, 0, 0], + [3, 5200, 5300, 4940, 4962, 6172, 0, 0], # Sell on ROI + [4, 4962, 4987, 4972, 4950, 6172, 0, 0], + [5, 4550, 4975, 4925, 4950, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10, "119": 0.01}, profit_perc=0.01, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] +) + + TESTS = [ tc0, tc1, @@ -283,6 +346,10 @@ TESTS = [ tc14, tc15, tc16, + tc17, + tc18, + tc19, + tc20, ] @@ -313,7 +380,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: pair = "UNITTEST/BTC" # Dummy data as we mock the analyze functions data_processed = {pair: frame.copy()} - min_date, max_date = get_timeframe({pair: frame}) + min_date, max_date = get_timerange({pair: frame}) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 5086891a6..0ea35e41f 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -16,7 +16,7 @@ from freqtrade.data import history from freqtrade.data.btanalysis import evaluate_result_multi from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.dataprovider import DataProvider -from freqtrade.data.history import get_timeframe +from freqtrade.data.history import get_timerange from freqtrade.optimize import setup_configuration, start_backtesting from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode @@ -100,7 +100,7 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None: data = load_data_test(contour, testdatadir) processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = get_timeframe(processed) + min_date, max_date = get_timerange(processed) assert isinstance(processed, dict) results = backtesting.backtest( { @@ -116,8 +116,8 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None: assert len(results) == num_results -def mocked_load_data(datadir, pairs=[], timeframe='0m', refresh_pairs=False, - timerange=None, exchange=None, live=False, *args, **kwargs): +def mocked_load_data(datadir, pairs=[], timeframe='0m', + timerange=None, *args, **kwargs): tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange) pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata, '1m', pair="UNITTEST/BTC", fill_missing=True)} @@ -138,7 +138,7 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC', record= patch_exchange(mocker) backtesting = Backtesting(conf) processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = get_timeframe(processed) + min_date, max_date = get_timerange(processed) return { 'stake_amount': conf['stake_amount'], 'processed': processed, @@ -458,11 +458,11 @@ def test_generate_text_table_strategyn(default_conf, mocker): def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: - def get_timeframe(input1): + def get_timerange(input1): return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) mocker.patch('freqtrade.data.history.load_data', mocked_load_data) - mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe) + mocker.patch('freqtrade.data.history.get_timerange', get_timerange) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) mocker.patch.multiple( @@ -491,11 +491,11 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None: - def get_timeframe(input1): + def get_timerange(input1): return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) mocker.patch('freqtrade.data.history.load_pair_history', MagicMock(return_value=pd.DataFrame())) - mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe) + mocker.patch('freqtrade.data.history.get_timerange', get_timerange) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) mocker.patch.multiple( @@ -525,7 +525,7 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None: data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], timerange=timerange) data_processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = get_timeframe(data_processed) + min_date, max_date = get_timerange(data_processed) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], @@ -581,7 +581,7 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker, testdatadir) - data = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'], timerange=timerange) processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = get_timeframe(processed) + min_date, max_date = get_timerange(processed) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], @@ -701,7 +701,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) backtesting.strategy.advise_sell = _trend_alternate_hold # Override data_processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = get_timeframe(data_processed) + min_date, max_date = get_timerange(data_processed) backtest_conf = { 'stake_amount': default_conf['stake_amount'], 'processed': data_processed, diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 60b445323..29b8b5b16 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -251,7 +251,7 @@ def test_start_no_data(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.data.history.load_pair_history', MagicMock(return_value=pd.DataFrame)) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -427,7 +427,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -602,7 +602,7 @@ def test_generate_optimizer(mocker, default_conf) -> None: MagicMock(return_value=backtest_result) ) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13))) ) patch_exchange(mocker) @@ -642,7 +642,7 @@ def test_generate_optimizer(mocker, default_conf) -> None: response_expected = { 'loss': 1.9840569076926293, 'results_explanation': (' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC ' - '( 2.31\N{GREEK CAPITAL LETTER SIGMA}%). Avg duration 100.0 mins.' + '( 2.31\N{GREEK CAPITAL LETTER SIGMA}%). Avg duration 100.0 min.' ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8'), 'params_details': {'buy': {'adx-enabled': False, 'adx-value': 0, @@ -726,7 +726,7 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None: mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -769,7 +769,7 @@ def test_print_json_spaces_default(mocker, default_conf, caplog, capsys) -> None mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -811,7 +811,7 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -851,7 +851,7 @@ def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys) mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -899,7 +899,7 @@ def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) - mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -930,7 +930,7 @@ def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None: mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -977,7 +977,7 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -1030,7 +1030,7 @@ def test_simplified_interface_failed(mocker, default_conf, caplog, capsys, metho mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 699f2d962..0a8c1cabd 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -398,7 +398,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): get_valid_pair_combination=MagicMock( side_effect=lambda a, b: f"{b}/{a}" if a == "USDT" else f"{a}/{b}") ) - + default_conf['dry_run'] = False freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 555fcdc81..f1e3421c5 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -230,6 +230,7 @@ def test_api_stopbuy(botclient): def test_api_balance(botclient, mocker, rpc_balance): ftbot, client = botclient + ftbot.config['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', side_effect=lambda a, b: f"{a}/{b}") @@ -380,7 +381,7 @@ def test_api_performance(botclient, mocker, ticker, fee): close_rate=0.265441, ) - trade.close_profit = trade.calc_profit_percent() + trade.close_profit = trade.calc_profit_ratio() Trade.session.add(trade) trade = Trade( @@ -395,7 +396,7 @@ def test_api_performance(botclient, mocker, ticker, fee): fee_open=fee.return_value, close_rate=0.391 ) - trade.close_profit = trade.calc_profit_percent() + trade.close_profit = trade.calc_profit_ratio() Trade.session.add(trade) Trade.session.flush() diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7c4a8f0d6..b02f11394 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -462,7 +462,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None: - + default_conf['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', @@ -494,6 +494,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick def test_balance_handle_empty_response(default_conf, update, mocker) -> None: + default_conf['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) msg_mock = MagicMock() @@ -533,7 +534,8 @@ def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 - assert "Running in Dry Run, balances are not available." in result + assert "*Warning:*Simulated balances in Dry Mode." in result + assert "Starting capital: `1000` BTC" in result def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: @@ -1177,6 +1179,16 @@ def test_show_config_handle(default_conf, update, mocker) -> None: assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] assert '*Exchange:* `bittrex`' in msg_mock.call_args_list[0][0][0] assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0] + assert '*Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] + + msg_mock.reset_mock() + freqtradebot.config['trailing_stop'] = True + telegram._show_config(update=update, context=MagicMock()) + assert msg_mock.call_count == 1 + assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] + assert '*Exchange:* `bittrex`' in msg_mock.call_args_list[0][0][0] + assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0] + assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] def test_send_msg_buy_notification(default_conf, mocker) -> None: diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 5519b1a34..605622b8f 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -125,6 +125,7 @@ def test_min_roi_reached(default_conf, fee) -> None: trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, @@ -162,6 +163,7 @@ def test_min_roi_reached2(default_conf, fee) -> None: trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, @@ -195,6 +197,7 @@ def test_min_roi_reached3(default_conf, fee) -> None: trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 89ca74afa..292d53315 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -8,9 +8,9 @@ from pathlib import Path from unittest.mock import MagicMock import pytest -from jsonschema import Draft4Validator, ValidationError, validate +from jsonschema import ValidationError -from freqtrade import OperationalException, constants +from freqtrade import OperationalException from freqtrade.configuration import (Arguments, Configuration, check_exchange, remove_credentials, validate_config_consistency) @@ -718,7 +718,8 @@ def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None: def test_validate_default_conf(default_conf) -> None: - validate(default_conf, constants.CONF_SCHEMA, Draft4Validator) + # Validate via our validator - we allow setting defaults! + validate_config_schema(default_conf) def test_validate_tsl(default_conf): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 9c2fd9ddc..300d0ad32 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -211,6 +211,7 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) patch_edge(mocker) + edge_conf['dry_run_wallet'] = 999.9 freqtrade = FreqtradeBot(edge_conf) assert freqtrade._get_trade_stake_amount('NEO/BTC') == (999.9 * 0.5 * 0.01) / 0.20 @@ -786,7 +787,10 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - pair = 'NOCLUE/BTC' + pair = 'BLK/BTC' + # Ensure the pair is not in the whitelist! + assert pair not in default_conf['exchange']['pair_whitelist'] + # create open trade not in whitelist Trade.session.add(Trade( pair=pair, @@ -1335,6 +1339,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, patch_exchange(mocker) patch_edge(mocker) edge_conf['max_open_trades'] = float('inf') + edge_conf['dry_run_wallet'] = 999.9 mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=MagicMock(return_value={ @@ -1507,13 +1512,15 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=limit_buy_order['amount']) - trade = Trade() - # Mock session away - Trade.session = MagicMock() - trade.open_order_id = '123' - trade.open_fee = 0.001 + trade = Trade( + open_order_id=123, + fee_open=0.001, + fee_close=0.001, + open_rate=0.01, + open_date=arrow.utcnow().datetime, + amount=11, + ) # Add datetime explicitly since sqlalchemy defaults apply only once written to database - trade.open_date = arrow.utcnow().datetime freqtrade.update_trade_state(trade) # Test amount not modified by fee-logic assert not log_has_re(r'Applying fee to .*', caplog) @@ -1536,7 +1543,8 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No assert log_has_re('Found open order for.*', caplog) -def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, mocker): +def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee, + mocker): mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) # get_order should not be called!! mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) @@ -1549,6 +1557,8 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, open_order_id="123456", is_open=True, ) @@ -1557,7 +1567,7 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ assert trade.amount == limit_buy_order['amount'] -def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_order, +def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_order, fee, limit_buy_order, mocker, caplog): trades_for_order[0]['amount'] = limit_buy_order['amount'] + 1e-14 mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) @@ -1572,6 +1582,8 @@ def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_ amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, open_order_id="123456", is_open=True, open_date=arrow.utcnow().datetime, @@ -3039,7 +3051,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, assert trade.sell_reason == SellType.STOP_LOSS.value -def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, caplog, mocker): +def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, fee, caplog, mocker): mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) patch_RPCManager(mocker) patch_exchange(mocker) @@ -3049,6 +3061,8 @@ def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, ca amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, open_order_id="123456" ) freqtrade = FreqtradeBot(default_conf) @@ -3061,7 +3075,7 @@ def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, ca caplog) -def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker): +def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker, fee): mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) patch_RPCManager(mocker) @@ -3072,6 +3086,8 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker): amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, open_order_id="123456" ) freqtrade = FreqtradeBot(default_conf) @@ -3084,7 +3100,7 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker): caplog) -def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mocker): +def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, fee, mocker): trades_for_order[0]['fee']['currency'] = 'ETH' patch_RPCManager(mocker) @@ -3095,6 +3111,8 @@ def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mo pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3105,7 +3123,8 @@ def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mo assert freqtrade.get_real_amount(trade, buy_order_fee) == amount -def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_order_fee, mocker): +def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_order_fee, + fee, mocker): limit_buy_order = deepcopy(buy_order_fee) limit_buy_order['fee'] = {'cost': 0.004, 'currency': None} @@ -3119,6 +3138,8 @@ def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_ pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3129,7 +3150,7 @@ def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_ assert freqtrade.get_real_amount(trade, limit_buy_order) == amount -def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mocker): +def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, fee, mocker): trades_for_order[0]['fee']['currency'] = 'BNB' trades_for_order[0]['fee']['cost'] = 0.00094518 @@ -3141,6 +3162,8 @@ def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mock pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3151,7 +3174,7 @@ def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mock assert freqtrade.get_real_amount(trade, buy_order_fee) == amount -def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, caplog, mocker): +def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, caplog, fee, mocker): patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order2) @@ -3160,6 +3183,8 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3173,7 +3198,8 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c caplog) -def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee, caplog, mocker): +def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee, fee, + caplog, mocker): limit_buy_order = deepcopy(buy_order_fee) limit_buy_order['fee'] = {'cost': 0.004, 'currency': 'LTC'} @@ -3186,6 +3212,8 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3199,7 +3227,7 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee caplog) -def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order_fee, mocker): +def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order_fee, fee, mocker): limit_buy_order = deepcopy(buy_order_fee) limit_buy_order['fee'] = {'cost': 0.004} @@ -3211,6 +3239,8 @@ def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3221,7 +3251,7 @@ def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order assert freqtrade.get_real_amount(trade, limit_buy_order) == amount -def test_get_real_amount_wrong_amount(default_conf, trades_for_order, buy_order_fee, mocker): +def test_get_real_amount_wrong_amount(default_conf, trades_for_order, buy_order_fee, fee, mocker): limit_buy_order = deepcopy(buy_order_fee) limit_buy_order['amount'] = limit_buy_order['amount'] - 0.001 @@ -3234,6 +3264,8 @@ def test_get_real_amount_wrong_amount(default_conf, trades_for_order, buy_order_ amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, open_order_id="123456" ) freqtrade = FreqtradeBot(default_conf) @@ -3244,7 +3276,7 @@ def test_get_real_amount_wrong_amount(default_conf, trades_for_order, buy_order_ freqtrade.get_real_amount(trade, limit_buy_order) -def test_get_real_amount_wrong_amount_rounding(default_conf, trades_for_order, buy_order_fee, +def test_get_real_amount_wrong_amount_rounding(default_conf, trades_for_order, buy_order_fee, fee, mocker): # Floats should not be compared directly. limit_buy_order = deepcopy(buy_order_fee) @@ -3258,6 +3290,8 @@ def test_get_real_amount_wrong_amount_rounding(default_conf, trades_for_order, b pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3269,7 +3303,7 @@ def test_get_real_amount_wrong_amount_rounding(default_conf, trades_for_order, b abs_tol=MATH_CLOSE_PREC,) -def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, mocker): +def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, fee, mocker): # Remove "Currency" from fee dict trades_for_order[0]['fee'] = {'cost': 0.008} @@ -3282,6 +3316,9 @@ def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_order_id="123456" ) freqtrade = FreqtradeBot(default_conf) @@ -3290,7 +3327,7 @@ def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, assert freqtrade.get_real_amount(trade, buy_order_fee) == amount -def test_get_real_amount_open_trade(default_conf, mocker): +def test_get_real_amount_open_trade(default_conf, fee, mocker): patch_RPCManager(mocker) patch_exchange(mocker) amount = 12345 @@ -3299,6 +3336,8 @@ def test_get_real_amount_open_trade(default_conf, mocker): amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, open_order_id="123456" ) order = { @@ -3552,3 +3591,32 @@ def test_process_i_am_alive(default_conf, mocker, caplog): ftbot.process() assert log_has_re(message, caplog) + + +@pytest.mark.usefixtures("init_persistence") +def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order): + default_conf['dry_run'] = True + # Initialize to 2 times stake amount + default_conf['dry_run_wallet'] = 0.002 + default_conf['max_open_trades'] = 2 + patch_exchange(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker, + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + get_fee=fee, + ) + + bot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(bot) + assert bot.wallets.get_free('BTC') == 0.002 + + bot.create_trades() + trades = Trade.query.all() + assert len(trades) == 2 + + bot.config['max_open_trades'] = 3 + with pytest.raises( + DependencyException, + match=r"Available balance \(0 BTC\) is lower than stake amount \(0.001 BTC\)"): + bot.create_trades() diff --git a/tests/test_integration.py b/tests/test_integration.py index 228ed8468..728e96d55 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -71,6 +71,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, ) mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock()) + mocker.patch("freqtrade.wallets.Wallets.get_free", MagicMock(return_value=1)) freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade.strategy.order_types['stoploss_on_exchange'] = True diff --git a/tests/test_main.py b/tests/test_main.py index 4e97c375d..03e6a7ce9 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -79,6 +79,7 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=KeyboardInterrupt)) patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) + mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) args = ['trade', '-c', 'config.json.example'] @@ -98,6 +99,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None: MagicMock(side_effect=OperationalException('Oh snap!')) ) patched_configuration_load_config_file(mocker, default_conf) + mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) @@ -120,6 +122,7 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None: OperationalException("Oh snap!")]) mocker.patch('freqtrade.worker.Worker._worker', worker_mock) patched_configuration_load_config_file(mocker, default_conf) + mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) reconfigure_mock = mocker.patch('freqtrade.worker.Worker._reconfigure', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) @@ -143,6 +146,7 @@ def test_reconfigure(mocker, default_conf) -> None: 'freqtrade.worker.Worker._worker', MagicMock(side_effect=OperationalException('Oh snap!')) ) + mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 231a1d2e2..25ad8b6a7 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -136,12 +136,13 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog): id=2, pair='ETH/BTC', stake_amount=0.001, + open_rate=0.01, + amount=5, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', ) assert trade.open_order_id is None - assert trade.open_rate is None assert trade.close_profit is None assert trade.close_date is None @@ -173,6 +174,8 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): id=1, pair='ETH/BTC', stake_amount=0.001, + amount=5, + open_rate=0.01, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -205,6 +208,8 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + open_rate=0.01, + amount=5, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -212,7 +217,7 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): trade.open_order_id = 'something' trade.update(limit_buy_order) - assert trade.calc_open_trade_price() == 0.0010024999999225068 + assert trade._calc_open_trade_price() == 0.0010024999999225068 trade.update(limit_sell_order) assert trade.calc_close_trade_price() == 0.0010646656050132426 @@ -221,7 +226,7 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): assert trade.calc_profit() == 0.00006217 # Profit in percent - assert trade.calc_profit_percent() == 0.06201058 + assert trade.calc_profit_ratio() == 0.06201058 @pytest.mark.usefixtures("init_persistence") @@ -229,6 +234,8 @@ def test_calc_close_trade_price_exception(limit_buy_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + open_rate=0.1, + amount=5, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -244,13 +251,14 @@ def test_update_open_order(limit_buy_order): trade = Trade( pair='ETH/BTC', stake_amount=1.00, + open_rate=0.01, + amount=5, fee_open=0.1, fee_close=0.1, exchange='bittrex', ) assert trade.open_order_id is None - assert trade.open_rate is None assert trade.close_profit is None assert trade.close_date is None @@ -258,7 +266,6 @@ def test_update_open_order(limit_buy_order): trade.update(limit_buy_order) assert trade.open_order_id is None - assert trade.open_rate is None assert trade.close_profit is None assert trade.close_date is None @@ -268,6 +275,8 @@ def test_update_invalid_order(limit_buy_order): trade = Trade( pair='ETH/BTC', stake_amount=1.00, + amount=5, + open_rate=0.001, fee_open=0.1, fee_close=0.1, exchange='bittrex', @@ -282,6 +291,8 @@ def test_calc_open_trade_price(limit_buy_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, + open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -290,10 +301,10 @@ def test_calc_open_trade_price(limit_buy_order, fee): trade.update(limit_buy_order) # Buy @ 0.00001099 # Get the open rate price with the standard fee rate - assert trade.calc_open_trade_price() == 0.0010024999999225068 - + assert trade._calc_open_trade_price() == 0.0010024999999225068 + trade.fee_open = 0.003 # Get the open rate price with a custom fee rate - assert trade.calc_open_trade_price(fee=0.003) == 0.001002999999922468 + assert trade._calc_open_trade_price() == 0.001002999999922468 @pytest.mark.usefixtures("init_persistence") @@ -301,6 +312,8 @@ def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, + open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -324,6 +337,8 @@ def test_calc_profit(limit_buy_order, limit_sell_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, + open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -352,10 +367,12 @@ def test_calc_profit(limit_buy_order, limit_sell_order, fee): @pytest.mark.usefixtures("init_persistence") -def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee): +def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, + open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -364,17 +381,17 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee): trade.update(limit_buy_order) # Buy @ 0.00001099 # Get percent of profit with a custom rate (Higher than open rate) - assert trade.calc_profit_percent(rate=0.00001234) == 0.11723875 + assert trade.calc_profit_ratio(rate=0.00001234) == 0.11723875 # Get percent of profit with a custom rate (Lower than open rate) - assert trade.calc_profit_percent(rate=0.00000123) == -0.88863828 + assert trade.calc_profit_ratio(rate=0.00000123) == -0.88863828 # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 trade.update(limit_sell_order) - assert trade.calc_profit_percent() == 0.06201058 + assert trade.calc_profit_ratio() == 0.06201058 # Test with a custom fee rate on the close trade - assert trade.calc_profit_percent(fee=0.003) == 0.06147824 + assert trade.calc_profit_ratio(fee=0.003) == 0.06147824 @pytest.mark.usefixtures("init_persistence") @@ -481,6 +498,7 @@ def test_migrate_old(mocker, default_conf, fee): assert trade.max_rate == 0.0 assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 + assert trade.open_trade_price == trade._calc_open_trade_price() def test_migrate_new(mocker, default_conf, fee, caplog): @@ -563,6 +581,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): 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 trade.open_trade_price == trade._calc_open_trade_price() def test_migrate_mid_state(mocker, default_conf, fee, caplog): @@ -622,6 +641,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): assert trade.max_rate == 0.0 assert trade.stop_loss == 0.0 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) @@ -630,6 +650,7 @@ def test_adjust_stop_loss(fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -681,6 +702,7 @@ def test_adjust_min_max_rates(fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', diff --git a/tests/test_utils.py b/tests/test_utils.py index feba1ed59..40ca9ac02 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -444,6 +444,9 @@ def test_create_datadir_failed(caplog): def test_create_datadir(caplog, mocker): + # Ensure that caplog is empty before starting ... + # Should prevent random failures. + caplog.clear() cud = mocker.patch("freqtrade.utils.create_userdata_dir", MagicMock()) csf = mocker.patch("freqtrade.utils.copy_sample_files", MagicMock()) args = [ diff --git a/tests/test_wallets.py b/tests/test_wallets.py index ae2810a2d..3177edc05 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -1,7 +1,8 @@ # pragma pylint: disable=missing-docstring -from tests.conftest import get_patched_freqtradebot from unittest.mock import MagicMock +from tests.conftest import get_patched_freqtradebot + def test_sync_wallet_at_boot(mocker, default_conf): default_conf['dry_run'] = False