From 0587256733a1f9df8deb4ac4c6f63a2ded10e274 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Thu, 5 Mar 2020 21:57:01 +0100 Subject: [PATCH 01/79] minor create_trade() optimization --- freqtrade/freqtradebot.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4cbacdb1e..e5ae9043a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -246,7 +246,7 @@ class FreqtradeBot: if 'use_order_book' in bid_strategy and bid_strategy.get('use_order_book', False): logger.info( f"Getting price from order book {bid_strategy['price_side'].capitalize()} side." - ) + ) order_book_top = bid_strategy.get('order_book_top', 1) order_book = self.exchange.get_order_book(pair, order_book_top) logger.debug('order_book %s', order_book) @@ -394,21 +394,21 @@ class FreqtradeBot: logger.info(f"Pair {pair} is currently locked.") return False + if not self.get_free_open_trades(): + logger.debug(f"Can't open a new trade for {pair}: max number of trades is reached.") + return False + + stake_amount = self.get_trade_stake_amount(pair) + if not stake_amount: + logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") + return False + # running get_signal on historical data fetched (buy, sell) = self.strategy.get_signal( pair, self.strategy.ticker_interval, self.dataprovider.ohlcv(pair, self.strategy.ticker_interval)) if buy and not sell: - if not self.get_free_open_trades(): - logger.debug("Can't open a new trade: max number of trades is reached.") - return False - - stake_amount = self.get_trade_stake_amount(pair) - if not stake_amount: - logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") - return False - logger.info(f"Buy signal found: about create a new trade with stake_amount: " f"{stake_amount} ...") From 3208faf7ede6be0898951b8d0bffd2966071f1b7 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 8 Mar 2020 13:35:31 +0300 Subject: [PATCH 02/79] Do not use ticker where it's not a ticker --- docs/backtesting.md | 12 +-- docs/configuration.md | 8 +- docs/data-download.md | 12 +-- docs/developer.md | 6 +- docs/edge.md | 2 +- docs/exchanges.md | 4 +- docs/hyperopt.md | 17 ++-- docs/strategy-customization.md | 17 ++-- docs/utils.md | 6 +- freqtrade/commands/arguments.py | 4 +- freqtrade/commands/build_config_commands.py | 2 +- freqtrade/commands/cli_options.py | 2 +- freqtrade/configuration/timerange.py | 2 +- freqtrade/data/btanalysis.py | 10 +- freqtrade/data/converter.py | 55 +++++------ freqtrade/data/dataprovider.py | 14 +-- freqtrade/data/history/history_utils.py | 22 ++--- freqtrade/data/history/idatahandler.py | 25 +++-- freqtrade/data/history/jsondatahandler.py | 4 +- freqtrade/edge/edge_positioning.py | 16 +-- freqtrade/exchange/exchange.py | 79 +++++++-------- freqtrade/freqtradebot.py | 8 +- freqtrade/misc.py | 12 +-- freqtrade/optimize/backtesting.py | 48 ++++----- freqtrade/optimize/hyperopt.py | 12 +-- freqtrade/plot/plotting.py | 24 ++--- freqtrade/strategy/interface.py | 30 +++--- freqtrade/templates/base_strategy.py.j2 | 2 +- freqtrade/templates/sample_strategy.py | 2 +- tests/conftest.py | 23 ++--- tests/data/test_btanalysis.py | 11 +-- tests/data/test_converter.py | 28 +++--- tests/data/test_dataprovider.py | 36 +++---- tests/data/test_history.py | 48 ++++----- tests/edge/test_edge.py | 26 ++--- tests/exchange/test_exchange.py | 102 ++++++++++---------- tests/optimize/__init__.py | 10 +- tests/optimize/test_backtesting.py | 44 ++++----- tests/optimize/test_hyperopt.py | 44 ++++----- tests/strategy/strats/default_strategy.py | 2 +- tests/strategy/test_interface.py | 52 +++++----- tests/test_misc.py | 8 +- tests/test_plotting.py | 20 ++-- 43 files changed, 459 insertions(+), 452 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 79bfa2350..3d08d5332 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -11,8 +11,8 @@ Now you have good Buy and Sell strategies and some historic data, you want to te real data. This is what we call [backtesting](https://en.wikipedia.org/wiki/Backtesting). -Backtesting will use the crypto-currencies (pairs) from your config file and load ticker data from `user_data/data/` by default. -If no data is available for the exchange / pair / ticker interval combination, backtesting will ask you to download them first using `freqtrade download-data`. +Backtesting will use the crypto-currencies (pairs) from your config file and load historical candle (OHCLV) data from `user_data/data/` by default. +If no data is available for the exchange / pair / timeframe (ticker interval) combination, backtesting will ask you to download them first using `freqtrade download-data`. For details on downloading, please refer to the [Data Downloading](data-download.md) section in the documentation. The result of backtesting will confirm if your bot has better odds of making a profit than a loss. @@ -22,19 +22,19 @@ The result of backtesting will confirm if your bot has better odds of making a p ### Run a backtesting against the currencies listed in your config file -#### With 5 min tickers (Per default) +#### With 5 min candle (OHLCV) data (per default) ```bash freqtrade backtesting ``` -#### With 1 min tickers +#### With 1 min candle (OHLCV) data ```bash freqtrade backtesting --ticker-interval 1m ``` -#### Using a different on-disk ticker-data source +#### Using a different on-disk historical candle (OHLCV) data source Assume you downloaded the history data from the Bittrex exchange and kept it in the `user_data/data/bittrex-20180101` directory. You can then use this data for backtesting as follows: @@ -223,7 +223,7 @@ You can then load the trades to perform further analysis as shown in our [data a To compare multiple strategies, a list of Strategies can be provided to backtesting. -This is limited to 1 ticker-interval per run, however, data is only loaded once from disk so if you have multiple +This is limited to 1 timeframe (ticker interval) value per run. However, data is only loaded once from disk so if you have multiple strategies you'd like to compare, this will give a nice runtime boost. All listed Strategies need to be in the same directory. diff --git a/docs/configuration.md b/docs/configuration.md index 5580b9c68..42018a499 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 | `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade).
*Defaults to `false`.*
**Datatype:** Boolean | `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade).
*Defaults to `0.5`.*
**Datatype:** Float (as ratio) | `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals.
*Defaults to `0.05` (5%).*
**Datatype:** Positive Float as ratio. -| `ticker_interval` | The ticker interval to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** String +| `ticker_interval` | The timeframe (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` | 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 @@ -113,8 +113,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details.
**Datatype:** Boolean | `logfile` | Specifies logfile name. Uses a rolling strategy for log file rotation for 10 files with the 1MB limit per file.
**Datatype:** String | `user_data_dir` | Directory containing user data.
*Defaults to `./user_data/`*.
**Datatype:** String -| `dataformat_ohlcv` | Data format to use to store OHLCV historic data.
*Defaults to `json`*.
**Datatype:** String -| `dataformat_trades` | Data format to use to store trades historic data.
*Defaults to `jsongz`*.
**Datatype:** String +| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data.
*Defaults to `json`*.
**Datatype:** String +| `dataformat_trades` | Data format to use to store historical trades data.
*Defaults to `jsongz`*.
**Datatype:** String ### Parameters in the strategy @@ -413,7 +413,7 @@ Advanced options can be configured using the `_ft_has_params` setting, which wil Available options are listed in the exchange-class as `_ft_has_default`. -For example, to test the order type `FOK` with Kraken, and modify candle_limit to 200 (so you only get 200 candles per call): +For example, to test the order type `FOK` with Kraken, and modify candle limit to 200 (so you only get 200 candles per API call): ```json "exchange": { diff --git a/docs/data-download.md b/docs/data-download.md index 76e22f4ea..903d62854 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -33,7 +33,7 @@ optional arguments: Specify which tickers to download. Space-separated list. Default: `1m 5m`. --erase Clean all existing data for the selected exchange/pairs/timeframes. --data-format-ohlcv {json,jsongz} - Storage format for downloaded ohlcv data. (default: `json`). + Storage format for downloaded candle (OHLCV) data. (default: `json`). --data-format-trades {json,jsongz} Storage format for downloaded trades data. (default: `jsongz`). @@ -105,7 +105,7 @@ Common arguments: ##### Example converting data -The following command will convert all ohlcv (candle) data available in `~/.freqtrade/data/binance` from json to jsongz, saving diskspace in the process. +The following command will convert all candle (OHLCV) data available in `~/.freqtrade/data/binance` from json to jsongz, saving diskspace in the process. It'll also remove original json data files (`--erase` parameter). ``` bash @@ -192,15 +192,15 @@ Then run: freqtrade download-data --exchange binance ``` -This will download ticker data for all the currency pairs you defined in `pairs.json`. +This will download historical candle (OHLCV) data for all the currency pairs you defined in `pairs.json`. ### Other Notes - To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`. -- To change the exchange used to download the tickers, please use a different configuration file (you'll probably need to adjust ratelimits etc.) +- To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust ratelimits etc.) - To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`. -- To download ticker data for only 10 days, use `--days 10` (defaults to 30 days). -- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers. +- To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days). +- Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data. - To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options. ### Trades (tick) data diff --git a/docs/developer.md b/docs/developer.md index ef9232a59..34b2f1ba5 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -165,7 +165,7 @@ Since CCXT does not provide unification for Stoploss On Exchange yet, we'll need ### Incomplete candles -While fetching OHLCV data, we're may end up getting incomplete candles (Depending on the exchange). +While fetching candle (OHLCV) data, we may end up getting incomplete candles (depending on the exchange). To demonstrate this, we'll use daily candles (`"1d"`) to keep things simple. We query the api (`ct.fetch_ohlcv()`) for the timeframe and look at the date of the last entry. If this entry changes or shows the date of a "incomplete" candle, then we should drop this since having incomplete candles is problematic because indicators assume that only complete candles are passed to them, and will generate a lot of false buy signals. By default, we're therefore removing the last candle assuming it's incomplete. @@ -174,14 +174,14 @@ To check how the new exchange behaves, you can use the following snippet: ``` python import ccxt from datetime import datetime -from freqtrade.data.converter import parse_ticker_dataframe +from freqtrade.data.converter import ohlcv_to_dataframe ct = ccxt.binance() timeframe = "1d" pair = "XLM/BTC" # Make sure to use a pair that exists on that exchange! raw = ct.fetch_ohlcv(pair, timeframe=timeframe) # convert to dataframe -df1 = parse_ticker_dataframe(raw, timeframe, pair=pair, drop_incomplete=False) +df1 = ohlcv_to_dataframe(raw, timeframe, pair=pair, drop_incomplete=False) print(df1.tail(1)) print(datetime.utcnow()) diff --git a/docs/edge.md b/docs/edge.md index 6a301b044..721f570c7 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -156,7 +156,7 @@ Edge module has following configuration options: | `minimum_winrate` | It filters out pairs which don't have at least minimum_winrate.
This comes handy if you want to be conservative and don't comprise win rate in favour of risk reward ratio.
*Defaults to `0.60`.*
**Datatype:** Float | `minimum_expectancy` | It filters out pairs which have the expectancy lower than this number.
Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ return.
*Defaults to `0.20`.*
**Datatype:** Float | `min_trade_number` | When calculating *W*, *R* and *E* (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable.
Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something.
*Defaults to `10` (it is highly recommended not to decrease this number).*
**Datatype:** Integer -| `max_trade_duration_minute` | Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign.
**NOTICE:** While configuring this value, you should take into consideration your ticker interval. As an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. Default value is set assuming your strategy interval is relatively small (1m or 5m, etc.).
*Defaults to `1440` (one day).*
**Datatype:** Integer +| `max_trade_duration_minute` | Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign.
**NOTICE:** While configuring this value, you should take into consideration your timeframe (ticker interval). As an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. Default value is set assuming your strategy interval is relatively small (1m or 5m, etc.).
*Defaults to `1440` (one day).*
**Datatype:** Integer | `remove_pumps` | Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off.
*Defaults to `false`.*
**Datatype:** Boolean ## Running Edge independently diff --git a/docs/exchanges.md b/docs/exchanges.md index 70dae0aa5..66a0e96da 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -76,8 +76,8 @@ $ 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. +Most exchanges return current incomplete candle via their OHLCV/klines API interface. +By default, Freqtrade assumes that incomplete candle is fetched from the exchange and removes the last candle assuming it's the 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. diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 9bc5888ce..abd7aa7ce 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -103,9 +103,10 @@ Place the corresponding settings into the following methods The configuration and rules are the same than for buy signals. To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`. -#### Using ticker-interval as part of the Strategy +#### Using timeframe as a part of the Strategy -The Strategy exposes the ticker-interval as `self.ticker_interval`. The same value is available as class-attribute `HyperoptName.ticker_interval`. +The Strategy class exposes the timeframe (ticker interval) value as the `self.ticker_interval` attribute. +The same value is available as class-attribute `HyperoptName.ticker_interval`. In the case of the linked sample-value this would be `SampleHyperOpt.ticker_interval`. ## Solving a Mystery @@ -222,11 +223,11 @@ The `--spaces all` option determines that all possible parameters should be opti !!! Warning When switching parameters or changing configuration options, make sure to not use the argument `--continue` so temporary results can be removed. -### Execute Hyperopt with Different Ticker-Data Source +### Execute Hyperopt with different historical data source -If you would like to hyperopt parameters using an alternate ticker data that -you have on-disk, use the `--datadir PATH` option. Default hyperopt will -use data from directory `user_data/data`. +If you would like to hyperopt parameters using an alternate historical data set that +you have on-disk, use the `--datadir PATH` option. By default, hyperopt +uses data from directory `user_data/data`. ### Running Hyperopt with Smaller Testset @@ -380,7 +381,7 @@ As stated in the comment, you can also use it as the value of the `minimal_roi` #### Default ROI Search Space -If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the ticker_interval used. By default the values vary in the following ranges (for some of the most used ticker intervals, values are rounded to 5 digits after the decimal point): +If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the ticker_interval used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 5 digits after the decimal point): | # step | 1m | | 5m | | 1h | | 1d | | | ------ | ------ | ----------------- | -------- | ----------- | ---------- | ----------------- | ------------ | ----------------- | @@ -389,7 +390,7 @@ If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace f | 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 | | 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 | -These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the ticker interval used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the ticker interval used. +These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe (ticker interval) used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used. If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 4aacd3af6..7793ea148 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -84,7 +84,7 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame Performance Note: For the best performance be frugal on the number of indicators you are using. Let uncomment only the indicator you are using in your strategies or your hyperopt configuration, otherwise you will waste your memory and CPU usage. - :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() + :param dataframe: Dataframe with data from the exchange :param metadata: Additional information, like the currently traded pair :return: a Dataframe with all mandatory indicators for the strategies """ @@ -284,13 +284,14 @@ If your exchange supports it, it's recommended to also set `"stoploss_on_exchang For more information on order_types please look [here](configuration.md#understand-order_types). -### Ticker interval +### Timeframe (ticker interval) This is the set of candles the bot should download and use for the analysis. Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported by your exchange should work. -Please note that the same buy/sell signals may work with one interval, but not the other. -This setting is accessible within the strategy by using `self.ticker_interval`. +Please note that the same buy/sell signals may work well with one timeframe, but not with the others. + +This setting is accessible within the strategy methods as the `self.ticker_interval` attribute. ### Metadata dict @@ -335,14 +336,14 @@ Please always check the mode of operation to select the correct method to get da #### Possible options for DataProvider - `available_pairs` - Property with tuples listing cached pairs with their intervals (pair, interval). -- `ohlcv(pair, timeframe)` - Currently cached ticker data for the pair, returns DataFrame or empty DataFrame. +- `ohlcv(pair, timeframe)` - Currently cached candle (OHLCV) data for the pair, returns DataFrame or empty DataFrame. - `historic_ohlcv(pair, timeframe)` - Returns historical data stored on disk. - `get_pair_dataframe(pair, timeframe)` - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes). - `orderbook(pair, maximum)` - Returns latest orderbook data for the pair, a dict with bids/asks with a total of `maximum` entries. - `market(pair)` - Returns market data for the pair: fees, limits, precisions, activity flag, etc. See [ccxt documentation](https://github.com/ccxt/ccxt/wiki/Manual#markets) for more details on Market data structure. - `runmode` - Property containing the current runmode. -#### Example: fetch live ohlcv / historic data for the first informative pair +#### Example: fetch live / historical candle (OHLCV) data for the first informative pair ``` python if self.dp: @@ -377,8 +378,8 @@ if self.dp: ``` python if self.dp: - for pair, ticker in self.dp.available_pairs: - print(f"available {pair}, {ticker}") + for pair, timeframe in self.dp.available_pairs: + print(f"available {pair}, {timeframe}") ``` #### Get data for non-tradeable pairs diff --git a/docs/utils.md b/docs/utils.md index cdf0c31af..ec9ceeac9 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -61,8 +61,8 @@ $ freqtrade new-config --config config_binance.json ? Do you want to enable Dry-run (simulated trades)? Yes ? Please insert your stake currency: BTC ? Please insert your stake amount: 0.05 -? Please insert max_open_trades (Integer or 'unlimited'): 5 -? Please insert your ticker interval: 15m +? Please insert max_open_trades (Integer or 'unlimited'): 3 +? Please insert your timeframe (ticker interval): 5m ? Please insert your display Currency (for reporting): USD ? Select exchange binance ? Do you want to enable Telegram? No @@ -258,7 +258,7 @@ All exchanges supported by the ccxt library: _1btcxe, acx, adara, allcoin, anxpr ## List Timeframes -Use the `list-timeframes` subcommand to see the list of ticker intervals (timeframes) available for the exchange. +Use the `list-timeframes` subcommand to see the list of timeframes (ticker intervals) available for the exchange. ``` usage: freqtrade list-timeframes [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [--exchange EXCHANGE] [-1] diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 73e77d69d..06acea69e 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -296,7 +296,7 @@ class Arguments: # Add convert-data subcommand convert_data_cmd = subparsers.add_parser( 'convert-data', - help='Convert OHLCV data from one format to another.', + help='Convert candle (OHLCV) data from one format to another.', parents=[_common_parser], ) convert_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=True)) @@ -305,7 +305,7 @@ class Arguments: # Add convert-trade-data subcommand convert_trade_data_cmd = subparsers.add_parser( 'convert-trade-data', - help='Convert trade-data from one format to another.', + help='Convert trade data from one format to another.', parents=[_common_parser], ) convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False)) diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index 1598fa2ae..58ac6ec27 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -76,7 +76,7 @@ def ask_user_config() -> Dict[str, Any]: { "type": "text", "name": "ticker_interval", - "message": "Please insert your ticker interval:", + "message": "Please insert your timeframe (ticker interval):", "default": "5m", }, { diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index ef674c5c2..e92acef30 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -348,7 +348,7 @@ AVAILABLE_CLI_OPTIONS = { ), "dataformat_ohlcv": Arg( '--data-format-ohlcv', - help='Storage format for downloaded ohlcv data. (default: `%(default)s`).', + help='Storage format for downloaded candle (OHLCV) data. (default: `%(default)s`).', choices=constants.AVAILABLE_DATAHANDLERS, default='json' ), diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index 3db5f6217..151003999 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -45,7 +45,7 @@ class TimeRange: """ Adjust startts by candles. Applies only if no startup-candles have been available. - :param timeframe_secs: Ticker timeframe in seconds e.g. `timeframe_to_seconds('5m')` + :param timeframe_secs: Timeframe in seconds e.g. `timeframe_to_seconds('5m')` :param startup_candles: Number of candles to move start-date forward :param min_date: Minimum data date loaded. Key kriterium to decide if start-time has to be moved diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 7972c6333..681bf6734 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -151,17 +151,17 @@ def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> p return trades -def combine_tickers_with_mean(tickers: Dict[str, pd.DataFrame], - column: str = "close") -> pd.DataFrame: +def combine_dataframes_with_mean(data: Dict[str, pd.DataFrame], + column: str = "close") -> pd.DataFrame: """ Combine multiple dataframes "column" - :param tickers: Dict of Dataframes, dict key should be pair. + :param data: Dict of Dataframes, dict key should be pair. :param column: Column in the original dataframes to use :return: DataFrame with the column renamed to the dict key, and a column named mean, containing the mean of all pairs. """ - df_comb = pd.concat([tickers[pair].set_index('date').rename( - {column: pair}, axis=1)[pair] for pair in tickers], axis=1) + df_comb = pd.concat([data[pair].set_index('date').rename( + {column: pair}, axis=1)[pair] for pair in data], axis=1) df_comb['mean'] = df_comb.mean(axis=1) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 49a2a25bc..77371bf27 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -13,12 +13,12 @@ from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS logger = logging.getLogger(__name__) -def parse_ticker_dataframe(ticker: list, timeframe: str, pair: str, *, - fill_missing: bool = True, - drop_incomplete: bool = True) -> DataFrame: +def ohlcv_to_dataframe(ohlcv: list, timeframe: str, pair: str, *, + fill_missing: bool = True, drop_incomplete: bool = True) -> DataFrame: """ - Converts a ticker-list (format ccxt.fetch_ohlcv) to a Dataframe - :param ticker: ticker list, as returned by exchange.async_get_candle_history + Converts a list with candle (OHLCV) data (in format returned by ccxt.fetch_ohlcv) + to a Dataframe + :param ohlcv: list with candle (OHLCV) data, as returned by exchange.async_get_candle_history :param timeframe: timeframe (e.g. 5m). Used to fill up eventual missing data :param pair: Pair this data is for (used to warn if fillup was necessary) :param fill_missing: fill up missing candles with 0 candles @@ -26,21 +26,18 @@ def parse_ticker_dataframe(ticker: list, timeframe: str, pair: str, *, :param drop_incomplete: Drop the last candle of the dataframe, assuming it's incomplete :return: DataFrame """ - logger.debug("Parsing tickerlist to dataframe") + logger.debug(f"Converting candle (OHLCV) data to dataframe for pair {pair}.") cols = DEFAULT_DATAFRAME_COLUMNS - frame = DataFrame(ticker, columns=cols) + df = DataFrame(ohlcv, columns=cols) - frame['date'] = to_datetime(frame['date'], - unit='ms', - utc=True, - infer_datetime_format=True) + df['date'] = to_datetime(df['date'], unit='ms', utc=True, infer_datetime_format=True) - # Some exchanges return int values for volume and even for ohlc. + # Some exchanges return int values for Volume and even for OHLC. # Convert them since TA-LIB indicators used in the strategy assume floats # and fail with exception... - frame = frame.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float', - 'volume': 'float'}) - return clean_ohlcv_dataframe(frame, timeframe, pair, + df = df.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float', + 'volume': 'float'}) + return clean_ohlcv_dataframe(df, timeframe, pair, fill_missing=fill_missing, drop_incomplete=drop_incomplete) @@ -49,11 +46,11 @@ def clean_ohlcv_dataframe(data: DataFrame, timeframe: str, pair: str, *, fill_missing: bool = True, drop_incomplete: bool = True) -> DataFrame: """ - Clense a ohlcv dataframe by + Clense a OHLCV dataframe by * Grouping it by date (removes duplicate tics) * dropping last candles if requested * Filling up missing data (if requested) - :param data: DataFrame containing ohlcv data. + :param data: DataFrame containing candle (OHLCV) data. :param timeframe: timeframe (e.g. 5m). Used to fill up eventual missing data :param pair: Pair this data is for (used to warn if fillup was necessary) :param fill_missing: fill up missing candles with 0 candles @@ -88,16 +85,16 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) """ from freqtrade.exchange import timeframe_to_minutes - ohlc_dict = { + ohlcv_dict = { 'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', 'volume': 'sum' } - ticker_minutes = timeframe_to_minutes(timeframe) + timeframe_minutes = timeframe_to_minutes(timeframe) # Resample to create "NAN" values - df = dataframe.resample(f'{ticker_minutes}min', on='date').agg(ohlc_dict) + df = dataframe.resample(f'{timeframe_minutes}min', on='date').agg(ohlcv_dict) # Forwardfill close for missing columns df['close'] = df['close'].fillna(method='ffill') @@ -159,20 +156,20 @@ def order_book_to_dataframe(bids: list, asks: list) -> DataFrame: def trades_to_ohlcv(trades: list, timeframe: str) -> DataFrame: """ - Converts trades list to ohlcv list + Converts trades list to OHLCV list TODO: This should get a dedicated test :param trades: List of trades, as returned by ccxt.fetch_trades. - :param timeframe: Ticker timeframe to resample data to - :return: ohlcv Dataframe. + :param timeframe: Timeframe to resample data to + :return: OHLCV Dataframe. """ from freqtrade.exchange import timeframe_to_minutes - ticker_minutes = timeframe_to_minutes(timeframe) + timeframe_minutes = timeframe_to_minutes(timeframe) df = pd.DataFrame(trades) df['datetime'] = pd.to_datetime(df['datetime']) df = df.set_index('datetime') - df_new = df['price'].resample(f'{ticker_minutes}min').ohlc() - df_new['volume'] = df['amount'].resample(f'{ticker_minutes}min').sum() + df_new = df['price'].resample(f'{timeframe_minutes}min').ohlc() + df_new['volume'] = df['amount'].resample(f'{timeframe_minutes}min').sum() df_new['date'] = df_new.index # Drop 0 volume rows df_new = df_new.dropna() @@ -206,7 +203,7 @@ def convert_trades_format(config: Dict[str, Any], convert_from: str, convert_to: def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to: str, erase: bool): """ - Convert ohlcv from one format to another format. + Convert OHLCV from one format to another :param config: Config dictionary :param convert_from: Source format :param convert_to: Target format @@ -216,7 +213,7 @@ def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to: src = get_datahandler(config['datadir'], convert_from) trg = get_datahandler(config['datadir'], convert_to) timeframes = config.get('timeframes', [config.get('ticker_interval')]) - logger.info(f"Converting OHLCV for timeframe {timeframes}") + logger.info(f"Converting candle (OHLCV) for timeframe {timeframes}") if 'pairs' not in config: config['pairs'] = [] @@ -224,7 +221,7 @@ def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to: for timeframe in timeframes: config['pairs'].extend(src.ohlcv_get_pairs(config['datadir'], timeframe)) - logger.info(f"Converting OHLCV for {config['pairs']}") + logger.info(f"Converting candle (OHLCV) data for {config['pairs']}") for timeframe in timeframes: for pair in config['pairs']: diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 2964d1cb7..1df710152 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -1,7 +1,7 @@ """ Dataprovider Responsible to provide data to the bot -including Klines, tickers, historic data +including ticker and orderbook data, live and historical candle (OHLCV) data Common Interface for bot and strategy to access data. """ import logging @@ -43,10 +43,10 @@ class DataProvider: def ohlcv(self, pair: str, timeframe: str = None, copy: bool = True) -> DataFrame: """ - Get ohlcv data for the given pair as DataFrame + Get candle (OHLCV) data for the given pair as DataFrame Please use the `available_pairs` method to verify which pairs are currently cached. :param pair: pair to get the data for - :param timeframe: Ticker timeframe to get data for + :param timeframe: Timeframe to get data for :param copy: copy dataframe before returning if True. Use False only for read-only operations (where the dataframe is not modified) """ @@ -58,7 +58,7 @@ class DataProvider: def historic_ohlcv(self, pair: str, timeframe: str = None) -> DataFrame: """ - Get stored historic ohlcv data + Get stored historical candle (OHLCV) data :param pair: pair to get the data for :param timeframe: timeframe to get data for """ @@ -69,17 +69,17 @@ class DataProvider: def get_pair_dataframe(self, pair: str, timeframe: str = None) -> DataFrame: """ - Return pair ohlcv data, either live or cached historical -- depending + Return pair candle (OHLCV) data, either live or cached historical -- depending on the runmode. :param pair: pair to get the data for :param timeframe: timeframe to get data for :return: Dataframe for this pair """ if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): - # Get live ohlcv data. + # Get live OHLCV data. data = self.ohlcv(pair=pair, timeframe=timeframe) else: - # Get historic ohlcv data (cached on disk). + # Get historical OHLCV data (cached on disk). data = self.historic_ohlcv(pair=pair, timeframe=timeframe) if len(data) == 0: logger.warning(f"No data found for ({pair}, {timeframe}).") diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 5f9a7da20..89d29d33b 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -9,7 +9,7 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS -from freqtrade.data.converter import parse_ticker_dataframe, trades_to_ohlcv +from freqtrade.data.converter import ohlcv_to_dataframe, trades_to_ohlcv from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler from freqtrade.exceptions import OperationalException from freqtrade.exchange import Exchange @@ -28,10 +28,10 @@ def load_pair_history(pair: str, data_handler: IDataHandler = None, ) -> DataFrame: """ - Load cached ticker history for the given pair. + Load cached ohlcv history for the given pair. :param pair: Pair to load data for - :param timeframe: Ticker timeframe (e.g. "5m") + :param timeframe: Timeframe (e.g. "5m") :param datadir: Path to the data storage location. :param data_format: Format of the data. Ignored if data_handler is set. :param timerange: Limit data to be loaded to this timerange @@ -63,10 +63,10 @@ def load_data(datadir: Path, data_format: str = 'json', ) -> Dict[str, DataFrame]: """ - Load ticker history data for a list of pairs. + Load ohlcv history data for a list of pairs. :param datadir: Path to the data storage location. - :param timeframe: Ticker Timeframe (e.g. "5m") + :param timeframe: Timeframe (e.g. "5m") :param pairs: List of pairs to load :param timerange: Limit data to be loaded to this timerange :param fill_up_missing: Fill missing values with "No action"-candles @@ -104,10 +104,10 @@ def refresh_data(datadir: Path, timerange: Optional[TimeRange] = None, ) -> None: """ - Refresh ticker history data for a list of pairs. + Refresh ohlcv history data for a list of pairs. :param datadir: Path to the data storage location. - :param timeframe: Ticker Timeframe (e.g. "5m") + :param timeframe: 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 @@ -165,7 +165,7 @@ def _download_pair_history(datadir: Path, Based on @Rybolov work: https://github.com/rybolov/freqtrade-data :param pair: pair to download - :param timeframe: Ticker Timeframe (e.g 5m) + :param timeframe: Timeframe (e.g "5m") :param timerange: range of time to download :return: bool with success state """ @@ -194,8 +194,8 @@ def _download_pair_history(datadir: Path, days=-30).float_timestamp) * 1000 ) # TODO: Maybe move parsing to exchange class (?) - new_dataframe = parse_ticker_dataframe(new_data, timeframe, pair, - fill_missing=False, drop_incomplete=True) + new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair, + fill_missing=False, drop_incomplete=True) if data.empty: data = new_dataframe else: @@ -362,7 +362,7 @@ 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_min: ticker Timeframe in minutes + :param timeframe_min: Timeframe in minutes """ # total difference in minutes / timeframe-minutes expected_frames = int((max_date - min_date).total_seconds() // 60 // timeframe_min) diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index df03e7713..b08292604 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -55,7 +55,7 @@ class IDataHandler(ABC): Implements the loading and conversion to a Pandas dataframe. Timerange trimming and dataframe validation happens outside of this method. :param pair: Pair to load data - :param timeframe: Ticker timeframe (e.g. "5m") + :param timeframe: Timeframe (e.g. "5m") :param timerange: Limit data to be loaded to this timerange. Optionally implemented by subclasses to avoid loading all data where possible. @@ -67,7 +67,7 @@ class IDataHandler(ABC): """ Remove data for this pair :param pair: Delete data for this pair. - :param timeframe: Ticker timeframe (e.g. "5m") + :param timeframe: Timeframe (e.g. "5m") :return: True when deleted, false if file did not exist. """ @@ -129,10 +129,10 @@ class IDataHandler(ABC): warn_no_data: bool = True ) -> DataFrame: """ - Load cached ticker history for the given pair. + Load cached candle (OHLCV) data for the given pair. :param pair: Pair to load data for - :param timeframe: Ticker timeframe (e.g. "5m") + :param timeframe: Timeframe (e.g. "5m") :param timerange: Limit data to be loaded to this timerange :param fill_missing: Fill missing values with "No action"-candles :param drop_incomplete: Drop last candle assuming it may be incomplete. @@ -145,28 +145,27 @@ class IDataHandler(ABC): if startup_candles > 0 and timerange_startup: timerange_startup.subtract_start(timeframe_to_seconds(timeframe) * startup_candles) - pairdf = self._ohlcv_load(pair, timeframe, - timerange=timerange_startup) - if pairdf.empty: + df = self._ohlcv_load(pair, timeframe, timerange=timerange_startup) + if df.empty: if warn_no_data: logger.warning( f'No history data for pair: "{pair}", timeframe: {timeframe}. ' 'Use `freqtrade download-data` to download the data' ) - return pairdf + return df else: - enddate = pairdf.iloc[-1]['date'] + enddate = df.iloc[-1]['date'] if timerange_startup: - self._validate_pairdata(pair, pairdf, timerange_startup) - pairdf = trim_dataframe(pairdf, timerange_startup) + self._validate_pairdata(pair, df, timerange_startup) + df = trim_dataframe(df, timerange_startup) # incomplete candles should only be dropped if we didn't trim the end beforehand. - return clean_ohlcv_dataframe(pairdf, timeframe, + return clean_ohlcv_dataframe(df, timeframe, pair=pair, fill_missing=fill_missing, drop_incomplete=(drop_incomplete and - enddate == pairdf.iloc[-1]['date'])) + enddate == df.iloc[-1]['date'])) def _validate_pairdata(self, pair, pairdata: DataFrame, timerange: TimeRange): """ diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/jsondatahandler.py index 2b738a94a..363b03958 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/jsondatahandler.py @@ -60,7 +60,7 @@ class JsonDataHandler(IDataHandler): Implements the loading and conversion to a Pandas dataframe. Timerange trimming and dataframe validation happens outside of this method. :param pair: Pair to load data - :param timeframe: Ticker timeframe (e.g. "5m") + :param timeframe: Timeframe (e.g. "5m") :param timerange: Limit data to be loaded to this timerange. Optionally implemented by subclasses to avoid loading all data where possible. @@ -83,7 +83,7 @@ class JsonDataHandler(IDataHandler): """ Remove data for this pair :param pair: Delete data for this pair. - :param timeframe: Ticker timeframe (e.g. "5m") + :param timeframe: Timeframe (e.g. "5m") :return: True when deleted, false if file did not exist. """ filename = self._pair_data_filename(self._datadir, pair, timeframe) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 57a8f4a7c..a24e29efb 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -119,7 +119,7 @@ class Edge: logger.critical("No data found. Edge is stopped ...") return False - preprocessed = self.strategy.tickerdata_to_dataframe(data) + preprocessed = self.strategy.ohlcvdata_to_dataframe(data) # Print timeframe min_date, max_date = history.get_timerange(preprocessed) @@ -137,10 +137,10 @@ class Edge: pair_data = pair_data.sort_values(by=['date']) pair_data = pair_data.reset_index(drop=True) - ticker_data = self.strategy.advise_sell( + dataframe = self.strategy.advise_sell( self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() - trades += self._find_trades_for_stoploss_range(ticker_data, pair, self._stoploss_range) + trades += self._find_trades_for_stoploss_range(dataframe, pair, self._stoploss_range) # If no trade found then exit if len(trades) == 0: @@ -359,11 +359,11 @@ class Edge: # Returning a list of pairs in order of "expectancy" return final - def _find_trades_for_stoploss_range(self, ticker_data, pair, stoploss_range): - buy_column = ticker_data['buy'].values - sell_column = ticker_data['sell'].values - date_column = ticker_data['date'].values - ohlc_columns = ticker_data[['open', 'high', 'low', 'close']].values + def _find_trades_for_stoploss_range(self, dataframe, pair, stoploss_range): + buy_column = dataframe['buy'].values + sell_column = dataframe['sell'].values + date_column = dataframe['date'].values + ohlc_columns = dataframe[['open', 'high', 'low', 'close']].values result: list = [] for stoploss in stoploss_range: diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 522b4e40e..f4c94a1ca 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, TICK_SIZE, TRUNCATE, decimal_to_precision) from pandas import DataFrame -from freqtrade.data.converter import parse_ticker_dataframe +from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.exceptions import (DependencyException, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async @@ -351,7 +351,7 @@ class Exchange: def validate_timeframes(self, timeframe: Optional[str]) -> None: """ - Checks if ticker interval from config is a supported timeframe on the exchange + Check if timeframe from config is a supported timeframe on the exchange """ if not hasattr(self._api, "timeframes") or self._api.timeframes is None: # If timeframes attribute is missing (or is None), the exchange probably @@ -364,7 +364,7 @@ class Exchange: if timeframe and (timeframe not in self.timeframes): raise OperationalException( - f"Invalid ticker interval '{timeframe}'. This exchange supports: {self.timeframes}") + f"Invalid timeframe '{timeframe}'. This exchange supports: {self.timeframes}") if timeframe and timeframe_to_minutes(timeframe) < 1: raise OperationalException( @@ -599,7 +599,7 @@ class Exchange: return self._api.fetch_tickers() except ccxt.NotSupported as e: raise OperationalException( - f'Exchange {self._api.name} does not support fetching tickers in batch.' + f'Exchange {self._api.name} does not support fetching tickers in batch. ' f'Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( @@ -623,13 +623,13 @@ class Exchange: def get_historic_ohlcv(self, pair: str, timeframe: str, since_ms: int) -> List: """ - Gets candle history using asyncio and returns the list of candles. - Handles all async doing. - Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call. + Get candle history using asyncio and returns the list of candles. + Handles all async work for this. + Async over one pair, assuming we get `self._ohlcv_candle_limit` candles per call. :param pair: Pair to download - :param timeframe: Ticker Timeframe to get + :param timeframe: Timeframe to get data for :param since_ms: Timestamp in milliseconds to get history from - :returns List of tickers + :returns List with candle (OHLCV) data """ return asyncio.get_event_loop().run_until_complete( self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe, @@ -649,26 +649,27 @@ class Exchange: pair, timeframe, since) for since in range(since_ms, arrow.utcnow().timestamp * 1000, one_call)] - tickers = await asyncio.gather(*input_coroutines, return_exceptions=True) + results = await asyncio.gather(*input_coroutines, return_exceptions=True) - # Combine tickers + # Combine gathered results data: List = [] - for p, timeframe, ticker in tickers: + for p, timeframe, res in results: if p == pair: - data.extend(ticker) + data.extend(res) # Sort data again after extending the result - above calls return in "async order" data = sorted(data, key=lambda x: x[0]) - logger.info("downloaded %s with length %s.", pair, len(data)) + logger.info("Downloaded data for %s with length %s.", pair, len(data)) return data def refresh_latest_ohlcv(self, pair_list: List[Tuple[str, str]]) -> List[Tuple[str, List]]: """ - Refresh in-memory ohlcv asynchronously and set `_klines` with the result + Refresh in-memory OHLCV asynchronously and set `_klines` with the result Loops asynchronously over pair_list and downloads all pairs async (semi-parallel). + Only used in the dataprovider.refresh() method. :param pair_list: List of 2 element tuples containing pair, interval to refresh - :return: Returns a List of ticker-dataframes. + :return: TODO: return value is only used in the tests, get rid of it """ - logger.debug("Refreshing ohlcv data for %d pairs", len(pair_list)) + logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list)) input_coroutines = [] @@ -679,15 +680,15 @@ class Exchange: input_coroutines.append(self._async_get_candle_history(pair, timeframe)) else: logger.debug( - "Using cached ohlcv data for pair %s, timeframe %s ...", + "Using cached candle (OHLCV) data for pair %s, timeframe %s ...", pair, timeframe ) - tickers = asyncio.get_event_loop().run_until_complete( + results = asyncio.get_event_loop().run_until_complete( asyncio.gather(*input_coroutines, return_exceptions=True)) # handle caching - for res in tickers: + for res in results: if isinstance(res, Exception): logger.warning("Async code raised an exception: %s", res.__class__.__name__) continue @@ -698,13 +699,14 @@ class Exchange: if ticks: self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000 # keeping parsed dataframe in cache - self._klines[(pair, timeframe)] = parse_ticker_dataframe( + self._klines[(pair, timeframe)] = ohlcv_to_dataframe( ticks, timeframe, pair=pair, fill_missing=True, drop_incomplete=self._ohlcv_partial_candle) - return tickers + + return results def _now_is_time_to_refresh(self, pair: str, timeframe: str) -> bool: - # Calculating ticker interval in seconds + # Timeframe in seconds interval_in_sec = timeframe_to_seconds(timeframe) return not ((self._pairs_last_refresh_time.get((pair, timeframe), 0) @@ -714,11 +716,11 @@ class Exchange: async def _async_get_candle_history(self, pair: str, timeframe: str, since_ms: Optional[int] = None) -> Tuple[str, str, List]: """ - Asynchronously gets candle histories using fetch_ohlcv + Asynchronously get candle history data using fetch_ohlcv returns tuple: (pair, timeframe, ohlcv_list) """ try: - # fetch ohlcv asynchronously + # Fetch OHLCV asynchronously s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else '' logger.debug( "Fetching pair %s, interval %s, since %s %s...", @@ -728,9 +730,9 @@ class Exchange: data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, since=since_ms) - # Because some exchange sort Tickers ASC and other DESC. - # Ex: Bittrex returns a list of tickers ASC (oldest first, newest last) - # when GDAX returns a list of tickers DESC (newest first, oldest last) + # Some exchanges sort OHLCV in ASC order and others in DESC. + # Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last) + # while GDAX returns the list of OHLCV in DESC order (newest first, oldest last) # Only sort if necessary to save computing time try: if data and data[0][0] > data[-1][0]: @@ -743,14 +745,15 @@ class Exchange: except ccxt.NotSupported as e: raise OperationalException( - f'Exchange {self._api.name} does not support fetching historical candlestick data.' - f'Message: {e}') from e + f'Exchange {self._api.name} does not support fetching historical ' + f'candle (OHLCV) data. Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError(f'Could not load ticker history for pair {pair} due to ' - f'{e.__class__.__name__}. Message: {e}') from e + raise TemporaryError(f'Could not fetch historical candle (OHLCV) data ' + f'for pair {pair} due to {e.__class__.__name__}. ' + f'Message: {e}') from e except ccxt.BaseError as e: - raise OperationalException(f'Could not fetch ticker data for pair {pair}. ' - f'Msg: {e}') from e + raise OperationalException(f'Could not fetch historical candle (OHLCV) data ' + f'for pair {pair}. Message: {e}') from e @retrier_async async def _async_fetch_trades(self, pair: str, @@ -883,14 +886,14 @@ class Exchange: until: Optional[int] = None, from_id: Optional[str] = None) -> Tuple[str, List]: """ - Gets candle history using asyncio and returns the list of candles. - Handles all async doing. - Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call. + Get trade history data using asyncio. + Handles all async work and returns the list of candles. + Async over one pair, assuming we get `self._ohlcv_candle_limit` candles per call. :param pair: Pair to download :param since: Timestamp in milliseconds to get history from :param until: Timestamp in milliseconds. Defaults to current timestamp if not defined. :param from_id: Download data starting with ID (if id is known) - :returns List of tickers + :returns List of trade data """ if not self.exchange_has("fetchTrades"): raise OperationalException("This exchange does not suport downloading Trades.") diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 914b8d9cd..9897b39b4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -172,8 +172,8 @@ class FreqtradeBot: _whitelist = self.edge.adjust(_whitelist) if trades: - # Extend active-pair whitelist with pairs from open trades - # It ensures that tickers are downloaded for open trades + # Extend active-pair whitelist with pairs of open trades + # It ensures that candle (OHLCV) data are downloaded for open trades as well _whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist]) return _whitelist @@ -628,7 +628,7 @@ class FreqtradeBot: def get_sell_rate(self, pair: str, refresh: bool) -> float: """ - Get sell rate - either using get-ticker bid or first bid based on orderbook + Get sell rate - either using ticker bid or first bid based on orderbook The orderbook portion is only used for rpc messaging, which would otherwise fail for BitMex (has no bid/ask in fetch_ticker) or remain static in any other case since it's not updating. @@ -1043,7 +1043,7 @@ class FreqtradeBot: """ profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_trade = trade.calc_profit(rate=profit_rate) - # Use cached ticker here - it was updated seconds ago. + # Use cached rates here - it was updated seconds ago. current_rate = self.get_sell_rate(trade.pair, False) profit_ratio = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_ratio > 0 else "loss" diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 96bac28d8..1f52b75ec 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -81,13 +81,13 @@ def file_load_json(file): gzipfile = file # Try gzip file first, otherwise regular json file. if gzipfile.is_file(): - logger.debug('Loading ticker data from file %s', gzipfile) - with gzip.open(gzipfile) as tickerdata: - pairdata = json_load(tickerdata) + logger.debug(f"Loading historical data from file {gzipfile}") + with gzip.open(gzipfile) as datafile: + pairdata = json_load(datafile) elif file.is_file(): - logger.debug('Loading ticker data from file %s', file) - with open(file) as tickerdata: - pairdata = json_load(tickerdata) + logger.debug(f"Loading historical data from file {file}") + with open(file) as datafile: + pairdata = json_load(datafile) else: return None return pairdata diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 94441ce24..949c072c5 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -88,8 +88,8 @@ class Backtesting: validate_config_consistency(self.config) if "ticker_interval" not in self.config: - raise OperationalException("Ticker-interval needs to be set in either configuration " - "or as cli argument `--ticker-interval 5m`") + raise OperationalException("Timeframe (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_min = timeframe_to_minutes(self.timeframe) @@ -151,32 +151,33 @@ class Backtesting: logger.info(f'Dumping backtest results to {recordfilename}') file_dump_json(recordfilename, records) - def _get_ticker_list(self, processed: Dict) -> Dict[str, DataFrame]: + def _get_ohlcv_as_lists(self, processed: Dict) -> Dict[str, DataFrame]: """ - Helper function to convert a processed tickerlist into a list for performance reasons. + Helper function to convert a processed dataframes into lists for performance reasons. Used by backtest() - so keep this optimized for performance. """ headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] - ticker: Dict = {} - # Create ticker dict + data: Dict = {} + # Create dict with data for pair, pair_data in processed.items(): pair_data.loc[:, 'buy'] = 0 # cleanup from previous run pair_data.loc[:, 'sell'] = 0 # cleanup from previous run - ticker_data = self.strategy.advise_sell( + dataframe = self.strategy.advise_sell( self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() - # to avoid using data from future, we buy/sell with signal from previous candle - ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1) - ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1) + # To avoid using data from future, we use buy/sell signals shifted + # from the previous candle + dataframe.loc[:, 'buy'] = dataframe['buy'].shift(1) + dataframe.loc[:, 'sell'] = dataframe['sell'].shift(1) - ticker_data.drop(ticker_data.head(1).index, inplace=True) + dataframe.drop(dataframe.head(1).index, inplace=True) # Convert from Pandas to list for performance reasons # (Looping Pandas is slow.) - ticker[pair] = [x for x in ticker_data.itertuples()] - return ticker + data[pair] = [x for x in dataframe.itertuples()] + return data def _get_close_rate(self, sell_row, trade: Trade, sell: SellCheckTuple, trade_dur: int) -> float: @@ -220,7 +221,7 @@ class Backtesting: def _get_sell_trade_entry( self, pair: str, buy_row: DataFrame, - partial_ticker: List, trade_count_lock: Dict, + partial_ohlcv: List, trade_count_lock: Dict, stake_amount: float, max_open_trades: int) -> Optional[BacktestResult]: trade = Trade( @@ -235,7 +236,7 @@ class Backtesting: ) logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.") # calculate win/lose forwards from buy point - for sell_row in partial_ticker: + for sell_row in partial_ohlcv: if max_open_trades > 0: # Increase trade_count_lock for every iteration trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1 @@ -259,9 +260,9 @@ class Backtesting: close_rate=closerate, sell_reason=sell.sell_type ) - if partial_ticker: + if partial_ohlcv: # no sell condition found - trade stil open at end of backtest period - sell_row = partial_ticker[-1] + sell_row = partial_ohlcv[-1] bt_res = BacktestResult(pair=pair, profit_percent=trade.calc_profit_ratio(rate=sell_row.open), profit_abs=trade.calc_profit(rate=sell_row.open), @@ -308,8 +309,9 @@ class Backtesting: trades = [] trade_count_lock: Dict = {} - # Dict of ticker-lists for performance (looping lists is a lot faster than dataframes) - ticker: Dict = self._get_ticker_list(processed) + # Use dict of lists with data for performance + # (looping lists is a lot faster than pandas DataFrames) + data: Dict = self._get_ohlcv_as_lists(processed) lock_pair_until: Dict = {} # Indexes per pair, so some pairs are allowed to have a missing start. @@ -319,12 +321,12 @@ class Backtesting: # Loop timerange and get candle for each pair at that point in time while tmp < end_date: - for i, pair in enumerate(ticker): + for i, pair in enumerate(data): if pair not in indexes: indexes[pair] = 0 try: - row = ticker[pair][indexes[pair]] + row = data[pair][indexes[pair]] except IndexError: # missing Data for one pair at the end. # Warnings for this are shown during data loading @@ -352,7 +354,7 @@ class Backtesting: # since indexes has been incremented before, we need to go one step back to # also check the buying candle for sell conditions. - trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][indexes[pair]-1:], + trade_entry = self._get_sell_trade_entry(pair, row, data[pair][indexes[pair]-1:], trade_count_lock, stake_amount, max_open_trades) @@ -395,7 +397,7 @@ class Backtesting: self._set_strategy(strat) # need to reprocess data every time to populate signals - preprocessed = self.strategy.tickerdata_to_dataframe(data) + preprocessed = self.strategy.ohlcvdata_to_dataframe(data) # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index e9ab469f4..ed58db977 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -75,8 +75,8 @@ class Hyperopt: self.trials_file = (self.config['user_data_dir'] / 'hyperopt_results' / 'hyperopt_results.pickle') - self.tickerdata_pickle = (self.config['user_data_dir'] / - 'hyperopt_results' / 'hyperopt_tickerdata.pkl') + self.data_pickle_file = (self.config['user_data_dir'] / + 'hyperopt_results' / 'hyperopt_data.pkl') self.total_epochs = config.get('epochs', 0) self.current_best_loss = 100 @@ -130,7 +130,7 @@ class Hyperopt: """ Remove hyperopt pickle files to restart hyperopt. """ - for f in [self.tickerdata_pickle, self.trials_file]: + for f in [self.data_pickle_file, self.trials_file]: p = Path(f) if p.is_file(): logger.info(f"Removing `{p}`.") @@ -454,7 +454,7 @@ class Hyperopt: self.backtesting.strategy.trailing_only_offset_is_reached = \ d['trailing_only_offset_is_reached'] - processed = load(self.tickerdata_pickle) + processed = load(self.data_pickle_file) min_date, max_date = get_timerange(processed) @@ -570,7 +570,7 @@ class Hyperopt: self.hyperopt_table_header = -1 data, timerange = self.backtesting.load_bt_data() - preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data) + preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): @@ -581,7 +581,7 @@ class Hyperopt: 'Hyperopting with data from %s up to %s (%s days)..', min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days ) - dump(preprocessed, self.tickerdata_pickle) + dump(preprocessed, self.data_pickle_file) # We don't need exchange instance anymore while running hyperopt self.backtesting.exchange = None # type: ignore diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index d979a40e0..cfbda6714 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -6,7 +6,7 @@ import pandas as pd from freqtrade.configuration import TimeRange from freqtrade.data.btanalysis import (calculate_max_drawdown, - combine_tickers_with_mean, + combine_dataframes_with_mean, create_cum_profit, extract_trades_of_period, load_trades) from freqtrade.data.converter import trim_dataframe @@ -29,7 +29,7 @@ except ImportError: def init_plotscript(config): """ Initialize objects needed for plotting - :return: Dict with tickers, trades and pairs + :return: Dict with candle (OHLCV) data, trades and pairs """ if "pairs" in config: @@ -40,7 +40,7 @@ def init_plotscript(config): # Set timerange to use timerange = TimeRange.parse_timerange(config.get("timerange")) - tickers = load_data( + data = load_data( datadir=config.get("datadir"), pairs=pairs, timeframe=config.get('ticker_interval', '5m'), @@ -53,7 +53,7 @@ def init_plotscript(config): exportfilename=config.get('exportfilename'), ) trades = trim_dataframe(trades, timerange, 'open_time') - return {"tickers": tickers, + return {"ohlcv": data, "trades": trades, "pairs": pairs, } @@ -368,10 +368,10 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra return fig -def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], +def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame], trades: pd.DataFrame, timeframe: str) -> go.Figure: # Combine close-values for all pairs, rename columns to "pair" - df_comb = combine_tickers_with_mean(tickers, "close") + df_comb = combine_dataframes_with_mean(data, "close") # Add combined cumulative profit df_comb = create_cum_profit(df_comb, trades, 'cum_profit', timeframe) @@ -439,7 +439,7 @@ def load_and_plot_trades(config: Dict[str, Any]): """ From configuration provided - Initializes plot-script - - Get tickers data + - Get candle (OHLCV) data - Generate Dafaframes populated with indicators and signals based on configured strategy - Load trades excecuted during the selected period - Generate Plotly plot objects @@ -451,13 +451,13 @@ def load_and_plot_trades(config: Dict[str, Any]): plot_elements = init_plotscript(config) trades = plot_elements['trades'] pair_counter = 0 - for pair, data in plot_elements["tickers"].items(): + for pair, data in plot_elements["ohlcv"].items(): pair_counter += 1 logger.info("analyse pair %s", pair) - tickers = {} - tickers[pair] = data + ohlcv = {} + ohlcv[pair] = data - dataframe = strategy.analyze_ticker(tickers[pair], {'pair': pair}) + dataframe = strategy.analyze_ticker(ohlcv[pair], {'pair': pair}) trades_pair = trades.loc[trades['pair'] == pair] trades_pair = extract_trades_of_period(dataframe, trades_pair) @@ -494,7 +494,7 @@ def plot_profit(config: Dict[str, Any]) -> None: # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend - fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"], + fig = generate_profit_graph(plot_elements["pairs"], plot_elements["ohlcv"], trades, config.get('ticker_interval', '5m')) store_plot_file(fig, filename='freqtrade-profit-plot.html', directory=config['user_data_dir'] / "plot", auto_open=True) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d23af3f6e..696d2b2d2 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -59,7 +59,7 @@ class IStrategy(ABC): Attributes you can use: minimal_roi -> Dict: Minimal ROI designed for the strategy stoploss -> float: optimal stoploss designed for the strategy - ticker_interval -> str: value of the ticker interval to use for the strategy + ticker_interval -> str: value of the timeframe (ticker interval) to use with the strategy """ # Strategy interface version # Default to version 2 @@ -125,7 +125,7 @@ class IStrategy(ABC): def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Populate indicators that will be used in the Buy and Sell strategy - :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() + :param dataframe: DataFrame with data from the exchange :param metadata: Additional information, like the currently traded pair :return: a Dataframe with all mandatory indicators for the strategies """ @@ -200,11 +200,11 @@ class IStrategy(ABC): def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ - Parses the given ticker history and returns a populated DataFrame + Parses the given candle (OHLCV) data and returns a populated DataFrame add several TA indicators and buy signal to it - :param dataframe: Dataframe containing ticker data + :param dataframe: Dataframe containing data from exchange :param metadata: Metadata dictionary with additional data (e.g. 'pair') - :return: DataFrame with ticker data and indicator data + :return: DataFrame of candle (OHLCV) data with indicator data and signals added """ logger.debug("TA Analysis Launched") dataframe = self.advise_indicators(dataframe, metadata) @@ -214,12 +214,12 @@ class IStrategy(ABC): def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ - Parses the given ticker history and returns a populated DataFrame + Parses the given candle (OHLCV) data and returns a populated DataFrame add several TA indicators and buy signal to it WARNING: Used internally only, may skip analysis if `process_only_new_candles` is set. - :param dataframe: Dataframe containing ticker data + :param dataframe: Dataframe containing data from exchange :param metadata: Metadata dictionary with additional data (e.g. 'pair') - :return: DataFrame with ticker data and indicator data + :return: DataFrame of candle (OHLCV) data with indicator data and signals added """ pair = str(metadata.get('pair')) @@ -251,21 +251,21 @@ class IStrategy(ABC): :return: (Buy, Sell) A bool-tuple indicating buy/sell signal """ if not isinstance(dataframe, DataFrame) or dataframe.empty: - logger.warning('Empty ticker history for pair %s', pair) + logger.warning('Empty candle (OHLCV) data for pair %s', pair) return False, False try: dataframe = self._analyze_ticker_internal(dataframe, {'pair': pair}) except ValueError as error: logger.warning( - 'Unable to analyze ticker for pair %s: %s', + 'Unable to analyze candle (OHLCV) data for pair %s: %s', pair, str(error) ) return False, False except Exception as error: logger.exception( - 'Unexpected error when analyzing ticker for pair %s: %s', + 'Unexpected error when analyzing candle (OHLCV) data for pair %s: %s', pair, str(error) ) @@ -440,19 +440,19 @@ class IStrategy(ABC): else: return current_profit > roi - def tickerdata_to_dataframe(self, tickerdata: Dict[str, DataFrame]) -> Dict[str, DataFrame]: + def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]: """ - Creates a dataframe and populates indicators for given ticker data + Creates a dataframe and populates indicators for given candle (OHLCV) data Used by optimize operations only, not during dry / live runs. """ return {pair: self.advise_indicators(pair_data, {'pair': pair}) - for pair, pair_data in tickerdata.items()} + for pair, pair_data in data.items()} def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Populate indicators that will be used in the Buy and Sell strategy This method should not be overridden. - :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() + :param dataframe: Dataframe with data from the exchange :param metadata: Additional information, like the currently traded pair :return: a Dataframe with all mandatory indicators for the strategies """ diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index fbf083387..97a189ff4 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -99,7 +99,7 @@ class {{ strategy }}(IStrategy): Performance Note: For the best performance be frugal on the number of indicators you are using. Let uncomment only the indicator you are using in your strategies or your hyperopt configuration, otherwise you will waste your memory and CPU usage. - :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() + :param dataframe: Dataframe with data from the exchange :param metadata: Additional information, like the currently traded pair :return: a Dataframe with all mandatory indicators for the strategies """ diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 17372e1e0..f78489173 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -116,7 +116,7 @@ class SampleStrategy(IStrategy): Performance Note: For the best performance be frugal on the number of indicators you are using. Let uncomment only the indicator you are using in your strategies or your hyperopt configuration, otherwise you will waste your memory and CPU usage. - :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() + :param dataframe: Dataframe with data from the exchange :param metadata: Additional information, like the currently traded pair :return: a Dataframe with all mandatory indicators for the strategies """ diff --git a/tests/conftest.py b/tests/conftest.py index e8e3fe9e3..64d0cd5ee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,7 +15,7 @@ from telegram import Chat, Message, Update from freqtrade import constants, persistence from freqtrade.commands import Arguments -from freqtrade.data.converter import parse_ticker_dataframe +from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot @@ -849,15 +849,15 @@ def order_book_l2(): @pytest.fixture -def ticker_history_list(): +def ohlcv_history_list(): return [ [ 1511686200000, # unix timestamp ms - 8.794e-05, # open - 8.948e-05, # high - 8.794e-05, # low - 8.88e-05, # close - 0.0877869, # volume (in quote currency) + 8.794e-05, # open + 8.948e-05, # high + 8.794e-05, # low + 8.88e-05, # close + 0.0877869, # volume (in quote currency) ], [ 1511686500000, @@ -879,8 +879,9 @@ def ticker_history_list(): @pytest.fixture -def ticker_history(ticker_history_list): - return parse_ticker_dataframe(ticker_history_list, "5m", pair="UNITTEST/BTC", fill_missing=True) +def ohlcv_history(ohlcv_history_list): + return ohlcv_to_dataframe(ohlcv_history_list, "5m", pair="UNITTEST/BTC", + fill_missing=True) @pytest.fixture @@ -1195,8 +1196,8 @@ def tickers(): @pytest.fixture def result(testdatadir): with (testdatadir / 'UNITTEST_BTC-1m.json').open('r') as data_file: - return parse_ticker_dataframe(json.load(data_file), '1m', pair="UNITTEST/BTC", - fill_missing=True) + return ohlcv_to_dataframe(json.load(data_file), '1m', pair="UNITTEST/BTC", + fill_missing=True) @pytest.fixture(scope="function") diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 7e3c1f077..da5d225b9 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -8,7 +8,7 @@ from freqtrade.configuration import TimeRange from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelism, calculate_max_drawdown, - combine_tickers_with_mean, + combine_dataframes_with_mean, create_cum_profit, extract_trades_of_period, load_backtest_data, load_trades, @@ -120,13 +120,10 @@ def test_load_trades(default_conf, mocker): assert bt_mock.call_count == 1 -def test_combine_tickers_with_mean(testdatadir): +def test_combine_dataframes_with_mean(testdatadir): pairs = ["ETH/BTC", "ADA/BTC"] - tickers = load_data(datadir=testdatadir, - pairs=pairs, - timeframe='5m' - ) - df = combine_tickers_with_mean(tickers) + data = load_data(datadir=testdatadir, pairs=pairs, timeframe='5m') + df = combine_dataframes_with_mean(data) assert isinstance(df, DataFrame) assert "ETH/BTC" in df.columns assert "ADA/BTC" in df.columns diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index a0ec2f46f..7dff520e0 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -5,9 +5,12 @@ from freqtrade.configuration.timerange import TimeRange from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_format, ohlcv_fill_up_missing_data, - parse_ticker_dataframe, trim_dataframe) -from freqtrade.data.history import (get_timerange, load_data, - load_pair_history, validate_backtest_data) + ohlcv_to_dataframe, + trim_dataframe) +from freqtrade.data.history import (get_timerange, + load_data, + load_pair_history, + validate_backtest_data) from tests.conftest import log_has from tests.data.test_history import _backup_file, _clean_test_file @@ -16,15 +19,15 @@ def test_dataframe_correct_columns(result): assert result.columns.tolist() == ['date', 'open', 'high', 'low', 'close', 'volume'] -def test_parse_ticker_dataframe(ticker_history_list, caplog): +def test_ohlcv_to_dataframe(ohlcv_history_list, caplog): columns = ['date', 'open', 'high', 'low', 'close', 'volume'] caplog.set_level(logging.DEBUG) # Test file with BV data - dataframe = parse_ticker_dataframe(ticker_history_list, '5m', - pair="UNITTEST/BTC", fill_missing=True) + dataframe = ohlcv_to_dataframe(ohlcv_history_list, '5m', pair="UNITTEST/BTC", + fill_missing=True) assert dataframe.columns.tolist() == columns - assert log_has('Parsing tickerlist to dataframe', caplog) + assert log_has('Converting candle (OHLCV) data to dataframe for pair UNITTEST/BTC.', caplog) def test_ohlcv_fill_up_missing_data(testdatadir, caplog): @@ -84,7 +87,8 @@ def test_ohlcv_fill_up_missing_data2(caplog): ] # Generate test-data without filling missing - data = parse_ticker_dataframe(ticks, timeframe, pair="UNITTEST/BTC", fill_missing=False) + data = ohlcv_to_dataframe(ticks, timeframe, pair="UNITTEST/BTC", + fill_missing=False) assert len(data) == 3 caplog.set_level(logging.DEBUG) data2 = ohlcv_fill_up_missing_data(data, timeframe, "UNITTEST/BTC") @@ -140,14 +144,14 @@ def test_ohlcv_drop_incomplete(caplog): ] ] caplog.set_level(logging.DEBUG) - data = parse_ticker_dataframe(ticks, timeframe, pair="UNITTEST/BTC", - fill_missing=False, drop_incomplete=False) + data = ohlcv_to_dataframe(ticks, timeframe, pair="UNITTEST/BTC", + fill_missing=False, drop_incomplete=False) assert len(data) == 4 assert not log_has("Dropping last candle", caplog) # Drop last candle - data = parse_ticker_dataframe(ticks, timeframe, pair="UNITTEST/BTC", - fill_missing=False, drop_incomplete=True) + data = ohlcv_to_dataframe(ticks, timeframe, pair="UNITTEST/BTC", + fill_missing=False, drop_incomplete=True) assert len(data) == 3 assert log_has("Dropping last candle", caplog) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index 1dbe20936..2b3dda188 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -7,19 +7,19 @@ from freqtrade.state import RunMode from tests.conftest import get_patched_exchange -def test_ohlcv(mocker, default_conf, ticker_history): +def test_ohlcv(mocker, default_conf, ohlcv_history): default_conf["runmode"] = RunMode.DRY_RUN timeframe = default_conf["ticker_interval"] exchange = get_patched_exchange(mocker, default_conf) - exchange._klines[("XRP/BTC", timeframe)] = ticker_history - exchange._klines[("UNITTEST/BTC", timeframe)] = ticker_history + exchange._klines[("XRP/BTC", timeframe)] = ohlcv_history + exchange._klines[("UNITTEST/BTC", timeframe)] = ohlcv_history dp = DataProvider(default_conf, exchange) assert dp.runmode == RunMode.DRY_RUN - assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", timeframe)) + assert ohlcv_history.equals(dp.ohlcv("UNITTEST/BTC", timeframe)) assert isinstance(dp.ohlcv("UNITTEST/BTC", timeframe), DataFrame) - assert dp.ohlcv("UNITTEST/BTC", timeframe) is not ticker_history - assert dp.ohlcv("UNITTEST/BTC", timeframe, copy=False) is ticker_history + assert dp.ohlcv("UNITTEST/BTC", timeframe) is not ohlcv_history + assert dp.ohlcv("UNITTEST/BTC", timeframe, copy=False) is ohlcv_history assert not dp.ohlcv("UNITTEST/BTC", timeframe).empty assert dp.ohlcv("NONESENSE/AAA", timeframe).empty @@ -37,8 +37,8 @@ def test_ohlcv(mocker, default_conf, ticker_history): assert dp.ohlcv("UNITTEST/BTC", timeframe).empty -def test_historic_ohlcv(mocker, default_conf, ticker_history): - historymock = MagicMock(return_value=ticker_history) +def test_historic_ohlcv(mocker, default_conf, ohlcv_history): + historymock = MagicMock(return_value=ohlcv_history) mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock) dp = DataProvider(default_conf, None) @@ -48,18 +48,18 @@ def test_historic_ohlcv(mocker, default_conf, ticker_history): assert historymock.call_args_list[0][1]["timeframe"] == "5m" -def test_get_pair_dataframe(mocker, default_conf, ticker_history): +def test_get_pair_dataframe(mocker, default_conf, ohlcv_history): default_conf["runmode"] = RunMode.DRY_RUN ticker_interval = default_conf["ticker_interval"] exchange = get_patched_exchange(mocker, default_conf) - exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history - exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history + exchange._klines[("XRP/BTC", ticker_interval)] = ohlcv_history + exchange._klines[("UNITTEST/BTC", ticker_interval)] = ohlcv_history dp = DataProvider(default_conf, exchange) assert dp.runmode == RunMode.DRY_RUN - assert ticker_history.equals(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval)) + assert ohlcv_history.equals(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval)) assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame) - assert dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval) is not ticker_history + assert dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval) is not ohlcv_history assert not dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval).empty assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty @@ -73,7 +73,7 @@ def test_get_pair_dataframe(mocker, default_conf, ticker_history): assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame) assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty - historymock = MagicMock(return_value=ticker_history) + historymock = MagicMock(return_value=ohlcv_history) mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock) default_conf["runmode"] = RunMode.BACKTEST dp = DataProvider(default_conf, exchange) @@ -82,11 +82,11 @@ def test_get_pair_dataframe(mocker, default_conf, ticker_history): # assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty -def test_available_pairs(mocker, default_conf, ticker_history): +def test_available_pairs(mocker, default_conf, ohlcv_history): exchange = get_patched_exchange(mocker, default_conf) ticker_interval = default_conf["ticker_interval"] - exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history - exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history + exchange._klines[("XRP/BTC", ticker_interval)] = ohlcv_history + exchange._klines[("UNITTEST/BTC", ticker_interval)] = ohlcv_history dp = DataProvider(default_conf, exchange) assert len(dp.available_pairs) == 2 @@ -96,7 +96,7 @@ def test_available_pairs(mocker, default_conf, ticker_history): ] -def test_refresh(mocker, default_conf, ticker_history): +def test_refresh(mocker, default_conf, ohlcv_history): refresh_mock = MagicMock() mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 9c9af9acd..12390538a 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -12,7 +12,7 @@ from pandas import DataFrame from pandas.testing import assert_frame_equal from freqtrade.configuration import TimeRange -from freqtrade.data.converter import parse_ticker_dataframe +from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.data.history.history_utils import ( _download_pair_history, _download_trades_history, _load_cached_data_for_updating, convert_trades_to_ohlcv, get_timerange, @@ -63,7 +63,7 @@ def _clean_test_file(file: Path) -> None: file_swp.rename(file) -def test_load_data_30min_ticker(mocker, caplog, default_conf, testdatadir) -> None: +def test_load_data_30min_timeframe(mocker, caplog, default_conf, testdatadir) -> None: ld = load_pair_history(pair='UNITTEST/BTC', timeframe='30m', datadir=testdatadir) assert isinstance(ld, DataFrame) assert not log_has( @@ -72,7 +72,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: +def test_load_data_7min_timeframe(mocker, caplog, default_conf, testdatadir) -> None: ld = load_pair_history(pair='UNITTEST/BTC', timeframe='7m', datadir=testdatadir) assert isinstance(ld, DataFrame) assert ld.empty @@ -82,8 +82,8 @@ def test_load_data_7min_ticker(mocker, caplog, default_conf, testdatadir) -> Non ) -def test_load_data_1min_ticker(ticker_history, mocker, caplog, testdatadir) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history) +def test_load_data_1min_timeframe(ohlcv_history, mocker, caplog, testdatadir) -> None: + mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history) file = testdatadir / 'UNITTEST_BTC-1m.json' _backup_file(file, copy_file=True) load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC']) @@ -110,12 +110,12 @@ def test_load_data_startup_candles(mocker, caplog, default_conf, testdatadir) -> assert ltfmock.call_args_list[0][1]['timerange'].startts == timerange.startts - 20 * 60 -def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, +def test_load_data_with_new_pair_1min(ohlcv_history_list, mocker, caplog, default_conf, testdatadir) -> None: """ - Test load_pair_history() with 1 min ticker + Test load_pair_history() with 1 min timeframe """ - mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history_list) + mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history_list) exchange = get_patched_exchange(mocker, default_conf) file = testdatadir / 'MEME_BTC-1m.json' @@ -188,8 +188,8 @@ def test_load_cached_data_for_updating(mocker, testdatadir) -> None: with open(test_filename, "rt") as file: test_data = json.load(file) - test_data_df = parse_ticker_dataframe(test_data, '1m', 'UNITTEST/BTC', - fill_missing=False, drop_incomplete=False) + test_data_df = ohlcv_to_dataframe(test_data, '1m', 'UNITTEST/BTC', + fill_missing=False, drop_incomplete=False) # now = last cached item + 1 hour now_ts = test_data[-1][0] / 1000 + 60 * 60 mocker.patch('arrow.utcnow', return_value=arrow.get(now_ts)) @@ -230,8 +230,8 @@ def test_load_cached_data_for_updating(mocker, testdatadir) -> None: assert start_ts is None -def test_download_pair_history(ticker_history_list, mocker, default_conf, testdatadir) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history_list) +def test_download_pair_history(ohlcv_history_list, mocker, default_conf, testdatadir) -> None: + mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history_list) exchange = get_patched_exchange(mocker, default_conf) file1_1 = testdatadir / 'MEME_BTC-1m.json' file1_5 = testdatadir / 'MEME_BTC-5m.json' @@ -293,7 +293,7 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None: assert json_dump_mock.call_count == 2 -def test_download_backtesting_data_exception(ticker_history, mocker, caplog, +def test_download_backtesting_data_exception(ohlcv_history, mocker, caplog, default_conf, testdatadir) -> None: mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', side_effect=Exception('File Error')) @@ -321,15 +321,15 @@ 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 = load_data(testdatadir, '5m', ['UNITTEST/BTC'], startup_candles=20, - timerange=TimeRange('date', 'date', start.timestamp, end.timestamp)) + data = 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 ) # timedifference in 5 minutes td = ((end - start).total_seconds() // 60 // 5) + 1 - assert td != len(tickerdata['UNITTEST/BTC']) - start_real = tickerdata['UNITTEST/BTC'].iloc[0, 0] + assert td != len(data['UNITTEST/BTC']) + start_real = data['UNITTEST/BTC'].iloc[0, 0] assert log_has(f'Missing data at start for pair ' f'UNITTEST/BTC, data starts at {start_real.strftime("%Y-%m-%d %H:%M:%S")}', caplog) @@ -337,14 +337,14 @@ 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 = load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], - timerange=TimeRange('date', 'date', start.timestamp, end.timestamp)) + data = 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']) + assert td != len(data['UNITTEST/BTC']) # Shift endtime with +5 - as last candle is dropped (partial candle) - end_real = arrow.get(tickerdata['UNITTEST/BTC'].iloc[-1, 0]).shift(minutes=5) + end_real = arrow.get(data['UNITTEST/BTC'].iloc[-1, 0]).shift(minutes=5) assert log_has(f'Missing data at end for pair ' f'UNITTEST/BTC, data ends at {end_real.strftime("%Y-%m-%d %H:%M:%S")}', caplog) @@ -403,7 +403,7 @@ def test_get_timerange(default_conf, mocker, testdatadir) -> None: default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) - data = strategy.tickerdata_to_dataframe( + data = strategy.ohlcvdata_to_dataframe( load_data( datadir=testdatadir, timeframe='1m', @@ -421,7 +421,7 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) - data = strategy.tickerdata_to_dataframe( + data = strategy.ohlcvdata_to_dataframe( load_data( datadir=testdatadir, timeframe='1m', @@ -446,7 +446,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No strategy = StrategyResolver.load_strategy(default_conf) timerange = TimeRange('index', 'index', 200, 250) - data = strategy.tickerdata_to_dataframe( + data = strategy.ohlcvdata_to_dataframe( load_data( datadir=testdatadir, timeframe='5m', diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index 2a0d19128..c68ac477c 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -11,7 +11,7 @@ import pytest from pandas import DataFrame, to_datetime from freqtrade.exceptions import OperationalException -from freqtrade.data.converter import parse_ticker_dataframe +from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.strategy.interface import SellType from tests.conftest import get_patched_freqtradebot, log_has @@ -26,7 +26,7 @@ from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, # 5) Stoploss and sell are hit. should sell on stoploss #################################################################### -ticker_start_time = arrow.get(2018, 10, 3) +tests_start_time = arrow.get(2018, 10, 3) ticker_interval_in_minute = 60 _ohlc = {'date': 0, 'buy': 1, 'open': 2, 'high': 3, 'low': 4, 'close': 5, 'sell': 6, 'volume': 7} @@ -43,10 +43,10 @@ def _validate_ohlc(buy_ohlc_sell_matrice): def _build_dataframe(buy_ohlc_sell_matrice): _validate_ohlc(buy_ohlc_sell_matrice) - tickers = [] + data = [] for ohlc in buy_ohlc_sell_matrice: - ticker = { - 'date': ticker_start_time.shift( + d = { + 'date': tests_start_time.shift( minutes=( ohlc[0] * ticker_interval_in_minute)).timestamp * @@ -57,9 +57,9 @@ def _build_dataframe(buy_ohlc_sell_matrice): 'low': ohlc[4], 'close': ohlc[5], 'sell': ohlc[6]} - tickers.append(ticker) + data.append(d) - frame = DataFrame(tickers) + frame = DataFrame(data) frame['date'] = to_datetime(frame['date'], unit='ms', utc=True, @@ -69,7 +69,7 @@ def _build_dataframe(buy_ohlc_sell_matrice): def _time_on_candle(number): - return np.datetime64(ticker_start_time.shift( + return np.datetime64(tests_start_time.shift( minutes=(number * ticker_interval_in_minute)).timestamp * 1000, 'ms') @@ -262,7 +262,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m', NEOBTC = [ [ - ticker_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000, + tests_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000, math.sin(x * hz) / 1000 + base, math.sin(x * hz) / 1000 + base + 0.0001, math.sin(x * hz) / 1000 + base - 0.0001, @@ -274,7 +274,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m', base = 0.002 LTCBTC = [ [ - ticker_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000, + tests_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000, math.sin(x * hz) / 1000 + base, math.sin(x * hz) / 1000 + base + 0.0001, math.sin(x * hz) / 1000 + base - 0.0001, @@ -282,8 +282,10 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m', 123.45 ] for x in range(0, 500)] - pairdata = {'NEO/BTC': parse_ticker_dataframe(NEOBTC, '1h', pair="NEO/BTC", fill_missing=True), - 'LTC/BTC': parse_ticker_dataframe(LTCBTC, '1h', pair="LTC/BTC", fill_missing=True)} + pairdata = {'NEO/BTC': ohlcv_to_dataframe(NEOBTC, '1h', pair="NEO/BTC", + fill_missing=True), + 'LTC/BTC': ohlcv_to_dataframe(LTCBTC, '1h', pair="LTC/BTC", + fill_missing=True)} return pairdata diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 6bec53d49..8d8930f66 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -581,7 +581,7 @@ def test_validate_timeframes_failed(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) with pytest.raises(OperationalException, - match=r"Invalid ticker interval '3m'. This exchange supports.*"): + match=r"Invalid timeframe '3m'. This exchange supports.*"): Exchange(default_conf) default_conf["ticker_interval"] = "15s" @@ -1211,7 +1211,7 @@ def test_fetch_ticker(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - tick = [ + ohlcv = [ [ arrow.utcnow().timestamp * 1000, # unix timestamp ms 1, # open @@ -1224,7 +1224,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): pair = 'ETH/BTC' async def mock_candle_hist(pair, timeframe, since_ms): - return pair, timeframe, tick + return pair, timeframe, ohlcv exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) # one_call calculation * 1.8 should do 2 calls @@ -1232,12 +1232,12 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000)) assert exchange._async_get_candle_history.call_count == 2 - # Returns twice the above tick + # Returns twice the above OHLCV data assert len(ret) == 2 def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: - tick = [ + ohlcv = [ [ (arrow.utcnow().timestamp - 1) * 1000, # unix timestamp ms 1, # open @@ -1258,14 +1258,14 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: caplog.set_level(logging.DEBUG) exchange = get_patched_exchange(mocker, default_conf) - exchange._api_async.fetch_ohlcv = get_mock_coro(tick) + exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) pairs = [('IOTA/ETH', '5m'), ('XRP/ETH', '5m')] # empty dicts assert not exchange._klines exchange.refresh_latest_ohlcv(pairs) - assert log_has(f'Refreshing ohlcv data for {len(pairs)} pairs', caplog) + assert log_has(f'Refreshing candle (OHLCV) data for {len(pairs)} pairs', caplog) assert exchange._klines assert exchange._api_async.fetch_ohlcv.call_count == 2 for pair in pairs: @@ -1283,14 +1283,15 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')]) assert exchange._api_async.fetch_ohlcv.call_count == 2 - assert log_has(f"Using cached ohlcv data for pair {pairs[0][0]}, timeframe {pairs[0][1]} ...", + assert log_has(f"Using cached candle (OHLCV) data for pair {pairs[0][0]}, " + f"timeframe {pairs[0][1]} ...", caplog) @pytest.mark.asyncio @pytest.mark.parametrize("exchange_name", EXCHANGES) async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name): - tick = [ + ohlcv = [ [ arrow.utcnow().timestamp * 1000, # unix timestamp ms 1, # open @@ -1304,7 +1305,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ caplog.set_level(logging.DEBUG) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) # Monkey-patch async function - exchange._api_async.fetch_ohlcv = get_mock_coro(tick) + exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) pair = 'ETH/BTC' res = await exchange._async_get_candle_history(pair, "5m") @@ -1312,9 +1313,9 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ assert len(res) == 3 assert res[0] == pair assert res[1] == "5m" - assert res[2] == tick + assert res[2] == ohlcv assert exchange._api_async.fetch_ohlcv.call_count == 1 - assert not log_has(f"Using cached ohlcv data for {pair} ...", caplog) + assert not log_has(f"Using cached candle (OHLCV) data for {pair} ...", caplog) # exchange = Exchange(default_conf) await async_ccxt_exception(mocker, default_conf, MagicMock(), @@ -1322,14 +1323,15 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ pair='ABCD/BTC', timeframe=default_conf['ticker_interval']) api_mock = MagicMock() - with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'): + with pytest.raises(OperationalException, + match=r'Could not fetch historical candle \(OHLCV\) data.*'): api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError("Unknown error")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) await exchange._async_get_candle_history(pair, "5m", (arrow.utcnow().timestamp - 2000) * 1000) with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching ' - r'historical candlestick data\..*'): + r'historical candle \(OHLCV\) data\..*'): api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported("Not supported")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) await exchange._async_get_candle_history(pair, "5m", @@ -1339,7 +1341,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ @pytest.mark.asyncio async def test__async_get_candle_history_empty(default_conf, mocker, caplog): """ Test empty exchange result """ - tick = [] + ohlcv = [] caplog.set_level(logging.DEBUG) exchange = get_patched_exchange(mocker, default_conf) @@ -1353,7 +1355,7 @@ async def test__async_get_candle_history_empty(default_conf, mocker, caplog): assert len(res) == 3 assert res[0] == pair assert res[1] == "5m" - assert res[2] == tick + assert res[2] == ohlcv assert exchange._api_async.fetch_ohlcv.call_count == 1 @@ -1431,8 +1433,8 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na return sorted(data, key=key) # GDAX use-case (real data from GDAX) - # This ticker history is ordered DESC (newest first, oldest last) - tick = [ + # This OHLCV data is ordered DESC (newest first, oldest last) + ohlcv = [ [1527833100000, 0.07666, 0.07671, 0.07666, 0.07668, 16.65244264], [1527832800000, 0.07662, 0.07666, 0.07662, 0.07666, 1.30051526], [1527832500000, 0.07656, 0.07661, 0.07656, 0.07661, 12.034778840000001], @@ -1445,31 +1447,31 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na [1527830400000, 0.07649, 0.07651, 0.07649, 0.07651, 2.5734867] ] exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - exchange._api_async.fetch_ohlcv = get_mock_coro(tick) + exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) sort_mock = mocker.patch('freqtrade.exchange.exchange.sorted', MagicMock(side_effect=sort_data)) - # Test the ticker history sort + # Test the OHLCV data sort res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval']) assert res[0] == 'ETH/BTC' - ticks = res[2] + res_ohlcv = res[2] assert sort_mock.call_count == 1 - assert ticks[0][0] == 1527830400000 - assert ticks[0][1] == 0.07649 - assert ticks[0][2] == 0.07651 - assert ticks[0][3] == 0.07649 - assert ticks[0][4] == 0.07651 - assert ticks[0][5] == 2.5734867 + assert res_ohlcv[0][0] == 1527830400000 + assert res_ohlcv[0][1] == 0.07649 + assert res_ohlcv[0][2] == 0.07651 + assert res_ohlcv[0][3] == 0.07649 + assert res_ohlcv[0][4] == 0.07651 + assert res_ohlcv[0][5] == 2.5734867 - assert ticks[9][0] == 1527833100000 - assert ticks[9][1] == 0.07666 - assert ticks[9][2] == 0.07671 - assert ticks[9][3] == 0.07666 - assert ticks[9][4] == 0.07668 - assert ticks[9][5] == 16.65244264 + assert res_ohlcv[9][0] == 1527833100000 + assert res_ohlcv[9][1] == 0.07666 + assert res_ohlcv[9][2] == 0.07671 + assert res_ohlcv[9][3] == 0.07666 + assert res_ohlcv[9][4] == 0.07668 + assert res_ohlcv[9][5] == 16.65244264 # Bittrex use-case (real data from Bittrex) - # This ticker history is ordered ASC (oldest first, newest last) - tick = [ + # This OHLCV data is ordered ASC (oldest first, newest last) + ohlcv = [ [1527827700000, 0.07659999, 0.0766, 0.07627, 0.07657998, 1.85216924], [1527828000000, 0.07657995, 0.07657995, 0.0763, 0.0763, 26.04051037], [1527828300000, 0.0763, 0.07659998, 0.0763, 0.0764, 10.36434124], @@ -1481,29 +1483,29 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na [1527830100000, 0.076695, 0.07671, 0.07624171, 0.07671, 1.80689244], [1527830400000, 0.07671, 0.07674399, 0.07629216, 0.07655213, 2.31452783] ] - exchange._api_async.fetch_ohlcv = get_mock_coro(tick) + exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) # Reset sort mock sort_mock = mocker.patch('freqtrade.exchange.sorted', MagicMock(side_effect=sort_data)) - # Test the ticker history sort + # Test the OHLCV data sort res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval']) assert res[0] == 'ETH/BTC' assert res[1] == default_conf['ticker_interval'] - ticks = res[2] + res_ohlcv = res[2] # Sorted not called again - data is already in order assert sort_mock.call_count == 0 - assert ticks[0][0] == 1527827700000 - assert ticks[0][1] == 0.07659999 - assert ticks[0][2] == 0.0766 - assert ticks[0][3] == 0.07627 - assert ticks[0][4] == 0.07657998 - assert ticks[0][5] == 1.85216924 + assert res_ohlcv[0][0] == 1527827700000 + assert res_ohlcv[0][1] == 0.07659999 + assert res_ohlcv[0][2] == 0.0766 + assert res_ohlcv[0][3] == 0.07627 + assert res_ohlcv[0][4] == 0.07657998 + assert res_ohlcv[0][5] == 1.85216924 - assert ticks[9][0] == 1527830400000 - assert ticks[9][1] == 0.07671 - assert ticks[9][2] == 0.07674399 - assert ticks[9][3] == 0.07629216 - assert ticks[9][4] == 0.07655213 - assert ticks[9][5] == 2.31452783 + assert res_ohlcv[9][0] == 1527830400000 + assert res_ohlcv[9][1] == 0.07671 + assert res_ohlcv[9][2] == 0.07674399 + assert res_ohlcv[9][3] == 0.07629216 + assert res_ohlcv[9][4] == 0.07655213 + assert res_ohlcv[9][5] == 2.31452783 @pytest.mark.asyncio diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index 13605a38c..8bc66f02c 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -6,7 +6,7 @@ from pandas import DataFrame from freqtrade.exchange import timeframe_to_minutes from freqtrade.strategy.interface import SellType -ticker_start_time = arrow.get(2018, 10, 3) +tests_start_time = arrow.get(2018, 10, 3) tests_timeframe = '1h' @@ -36,14 +36,14 @@ class BTContainer(NamedTuple): def _get_frame_time_from_offset(offset): - return ticker_start_time.shift(minutes=(offset * timeframe_to_minutes(tests_timeframe)) - ).datetime + minutes = offset * timeframe_to_minutes(tests_timeframe) + return tests_start_time.shift(minutes=minutes).datetime -def _build_backtest_dataframe(ticker_with_signals): +def _build_backtest_dataframe(data): columns = ['date', 'open', 'high', 'low', 'close', 'volume', 'buy', 'sell'] - frame = DataFrame.from_records(ticker_with_signals, columns=columns) + frame = DataFrame.from_records(data, columns=columns) frame['date'] = frame['date'].apply(_get_frame_time_from_offset) # Ensure floats are in place for column in ['open', 'high', 'low', 'close', 'volume']: diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 96855dc9d..1b6e23ffa 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -84,7 +84,7 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None: backtesting = Backtesting(config) data = load_data_test(contour, testdatadir) - processed = backtesting.strategy.tickerdata_to_dataframe(data) + processed = backtesting.strategy.ohlcvdata_to_dataframe(data) min_date, max_date = get_timerange(processed) assert isinstance(processed, dict) results = backtesting.backtest( @@ -105,7 +105,7 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'): data = trim_dictlist(data, -201) patch_exchange(mocker) backtesting = Backtesting(conf) - processed = backtesting.strategy.tickerdata_to_dataframe(data) + processed = backtesting.strategy.ohlcvdata_to_dataframe(data) min_date, max_date = get_timerange(processed) return { 'processed': processed, @@ -275,7 +275,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None: backtesting = Backtesting(default_conf) assert backtesting.config == default_conf assert backtesting.timeframe == '5m' - assert callable(backtesting.strategy.tickerdata_to_dataframe) + assert callable(backtesting.strategy.ohlcvdata_to_dataframe) assert callable(backtesting.strategy.advise_buy) assert callable(backtesting.strategy.advise_sell) assert isinstance(backtesting.strategy.dp, DataProvider) @@ -297,7 +297,7 @@ def test_backtesting_init_no_ticker_interval(mocker, default_conf, caplog) -> No "or as cli argument `--ticker-interval 5m`", caplog) -def test_tickerdata_with_fee(default_conf, mocker, testdatadir) -> None: +def test_data_with_fee(default_conf, mocker, testdatadir) -> None: patch_exchange(mocker) default_conf['fee'] = 0.1234 @@ -307,21 +307,21 @@ def test_tickerdata_with_fee(default_conf, mocker, testdatadir) -> None: assert fee_mock.call_count == 0 -def test_tickerdata_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: +def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: patch_exchange(mocker) timerange = TimeRange.parse_timerange('1510694220-1510700340') - tickerlist = history.load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, - fill_up_missing=True) + data = history.load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, + fill_up_missing=True) backtesting = Backtesting(default_conf) - data = backtesting.strategy.tickerdata_to_dataframe(tickerlist) - assert len(data['UNITTEST/BTC']) == 102 + processed = backtesting.strategy.ohlcvdata_to_dataframe(data) + assert len(processed['UNITTEST/BTC']) == 102 # Load strategy to compare the result between Backtesting function and strategy are the same default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) - data2 = strategy.tickerdata_to_dataframe(tickerlist) - assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC']) + processed2 = strategy.ohlcvdata_to_dataframe(data) + assert processed['UNITTEST/BTC'].equals(processed2['UNITTEST/BTC']) def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: @@ -329,7 +329,6 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) mocker.patch('freqtrade.data.history.get_timerange', get_timerange) - mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.generate_text_table', MagicMock(return_value=1)) @@ -360,7 +359,6 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> mocker.patch('freqtrade.data.history.history_utils.load_pair_history', MagicMock(return_value=pd.DataFrame())) mocker.patch('freqtrade.data.history.get_timerange', get_timerange) - mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.generate_text_table', MagicMock(return_value=1)) @@ -385,10 +383,10 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None: timerange = TimeRange('date', None, 1517227800, 0) 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_timerange(data_processed) + processed = backtesting.strategy.ohlcvdata_to_dataframe(data) + min_date, max_date = get_timerange(processed) results = backtesting.backtest( - processed=data_processed, + processed=processed, stake_amount=default_conf['stake_amount'], start_date=min_date, end_date=max_date, @@ -416,7 +414,7 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None: 'sell_reason': [SellType.ROI, SellType.ROI] }) pd.testing.assert_frame_equal(results, expected) - data_pair = data_processed[pair] + data_pair = processed[pair] for _, t in results.iterrows(): ln = data_pair.loc[data_pair["date"] == t["open_time"]] # Check open trade rate alignes to open rate @@ -439,7 +437,7 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker, testdatadir) - timerange = TimeRange.parse_timerange('1510688220-1510700340') data = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'], timerange=timerange) - processed = backtesting.strategy.tickerdata_to_dataframe(data) + processed = backtesting.strategy.ohlcvdata_to_dataframe(data) min_date, max_date = get_timerange(processed) results = backtesting.backtest( processed=processed, @@ -458,7 +456,7 @@ def test_processed(default_conf, mocker, testdatadir) -> None: backtesting = Backtesting(default_conf) dict_of_tickerrows = load_data_test('raise', testdatadir) - dataframes = backtesting.strategy.tickerdata_to_dataframe(dict_of_tickerrows) + dataframes = backtesting.strategy.ohlcvdata_to_dataframe(dict_of_tickerrows) dataframe = dataframes['UNITTEST/BTC'] cols = dataframe.columns # assert the dataframe got some of the indicator columns @@ -557,10 +555,10 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) backtesting.strategy.advise_buy = _trend_alternate_hold # Override backtesting.strategy.advise_sell = _trend_alternate_hold # Override - data_processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = get_timerange(data_processed) + processed = backtesting.strategy.ohlcvdata_to_dataframe(data) + min_date, max_date = get_timerange(processed) backtest_conf = { - 'processed': data_processed, + 'processed': processed, 'stake_amount': default_conf['stake_amount'], 'start_date': min_date, 'end_date': max_date, @@ -576,7 +574,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) assert len(evaluate_result_multi(results, '5m', 3)) == 0 backtest_conf = { - 'processed': data_processed, + 'processed': processed, 'stake_amount': default_conf['stake_amount'], 'start_date': min_date, 'end_date': max_date, diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 0406157f6..eefc6b28a 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -524,7 +524,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: }]) ) patch_exchange(mocker) - # Co-test loading ticker-interval from strategy + # Co-test loading timeframe from strategy del default_conf['ticker_interval'] default_conf.update({'config': 'config.json.example', 'hyperopt': 'DefaultHyperOpt', @@ -534,7 +534,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: 'hyperopt_jobs': 1, }) hyperopt = Hyperopt(default_conf) - hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() @@ -544,7 +544,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: out, err = capsys.readouterr() assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out assert dumper.called - # Should be called twice, once for tickerdata, once to save evaluations + # Should be called twice, once for historical candle data, once to save evaluations assert dumper.call_count == 2 assert hasattr(hyperopt.backtesting.strategy, "advise_sell") assert hasattr(hyperopt.backtesting.strategy, "advise_buy") @@ -630,8 +630,8 @@ def test_has_space(hyperopt, spaces, expected_results): def test_populate_indicators(hyperopt, testdatadir) -> None: - tickerlist = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True) - dataframes = hyperopt.backtesting.strategy.tickerdata_to_dataframe(tickerlist) + data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True) + dataframes = hyperopt.backtesting.strategy.ohlcvdata_to_dataframe(data) dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'}) @@ -642,8 +642,8 @@ def test_populate_indicators(hyperopt, testdatadir) -> None: def test_buy_strategy_generator(hyperopt, testdatadir) -> None: - tickerlist = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True) - dataframes = hyperopt.backtesting.strategy.tickerdata_to_dataframe(tickerlist) + data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True) + dataframes = hyperopt.backtesting.strategy.ohlcvdata_to_dataframe(data) dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'}) @@ -783,7 +783,7 @@ def test_clean_hyperopt(mocker, default_conf, caplog): h = Hyperopt(default_conf) assert unlinkmock.call_count == 2 - assert log_has(f"Removing `{h.tickerdata_pickle}`.", caplog) + assert log_has(f"Removing `{h.data_pickle_file}`.", caplog) def test_continue_hyperopt(mocker, default_conf, caplog): @@ -845,7 +845,7 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None: }) hyperopt = Hyperopt(default_conf) - hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() @@ -859,7 +859,7 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None: ) assert result_str in out # noqa: E501 assert dumper.called - # Should be called twice, once for tickerdata, once to save evaluations + # Should be called twice, once for historical candle data, once to save evaluations assert dumper.call_count == 2 @@ -903,7 +903,7 @@ def test_print_json_spaces_default(mocker, default_conf, caplog, capsys) -> None }) hyperopt = Hyperopt(default_conf) - hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() @@ -913,7 +913,7 @@ def test_print_json_spaces_default(mocker, default_conf, caplog, capsys) -> None out, err = capsys.readouterr() assert '{"params":{"mfi-value":null,"sell-mfi-value":null},"minimal_roi":{},"stoploss":null}' in out # noqa: E501 assert dumper.called - # Should be called twice, once for tickerdata, once to save evaluations + # Should be called twice, once for historical candle data, once to save evaluations assert dumper.call_count == 2 @@ -953,7 +953,7 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> }) hyperopt = Hyperopt(default_conf) - hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() @@ -963,7 +963,7 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> out, err = capsys.readouterr() assert '{"minimal_roi":{},"stoploss":null}' in out assert dumper.called - # Should be called twice, once for tickerdata, once to save evaluations + # Should be called twice, once for historical candle data, once to save evaluations assert dumper.call_count == 2 @@ -1000,7 +1000,7 @@ def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys) 'hyperopt_jobs': 1, }) hyperopt = Hyperopt(default_conf) - hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) del hyperopt.custom_hyperopt.__class__.buy_strategy_generator @@ -1015,7 +1015,7 @@ def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys) out, err = capsys.readouterr() assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out assert dumper.called - # Should be called twice, once for tickerdata, once to save evaluations + # Should be called twice, once for historical candle data, once to save evaluations assert dumper.call_count == 2 assert hasattr(hyperopt.backtesting.strategy, "advise_sell") assert hasattr(hyperopt.backtesting.strategy, "advise_buy") @@ -1043,7 +1043,7 @@ def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) - 'hyperopt_jobs': 1, }) hyperopt = Hyperopt(default_conf) - hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) del hyperopt.custom_hyperopt.__class__.buy_strategy_generator @@ -1088,7 +1088,7 @@ def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None: 'hyperopt_jobs': 1, }) hyperopt = Hyperopt(default_conf) - hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) # TODO: sell_strategy_generator() is actually not called because @@ -1103,7 +1103,7 @@ def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None: out, err = capsys.readouterr() assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out assert dumper.called - # Should be called twice, once for tickerdata, once to save evaluations + # Should be called twice, once for historical candle data, once to save evaluations assert dumper.call_count == 2 assert hasattr(hyperopt.backtesting.strategy, "advise_sell") assert hasattr(hyperopt.backtesting.strategy, "advise_buy") @@ -1145,7 +1145,7 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None 'hyperopt_jobs': 1, }) hyperopt = Hyperopt(default_conf) - hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) # TODO: buy_strategy_generator() is actually not called because @@ -1160,7 +1160,7 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None out, err = capsys.readouterr() assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out assert dumper.called - # Should be called twice, once for tickerdata, once to save evaluations + # Should be called twice, once for historical candle data, once to save evaluations assert dumper.call_count == 2 assert hasattr(hyperopt.backtesting.strategy, "advise_sell") assert hasattr(hyperopt.backtesting.strategy, "advise_buy") @@ -1194,7 +1194,7 @@ def test_simplified_interface_failed(mocker, default_conf, caplog, capsys, metho 'hyperopt_jobs': 1, }) hyperopt = Hyperopt(default_conf) - hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) delattr(hyperopt.custom_hyperopt.__class__, method) diff --git a/tests/strategy/strats/default_strategy.py b/tests/strategy/strats/default_strategy.py index 6c343b477..7ea55d3f9 100644 --- a/tests/strategy/strats/default_strategy.py +++ b/tests/strategy/strats/default_strategy.py @@ -68,7 +68,7 @@ class DefaultStrategy(IStrategy): Performance Note: For the best performance be frugal on the number of indicators you are using. Let uncomment only the indicator you are using in your strategies or your hyperopt configuration, otherwise you will waste your memory and CPU usage. - :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() + :param dataframe: Dataframe with data from the exchange :param metadata: Additional information, like the currently traded pair :return: a Dataframe with all mandatory indicators for the strategies """ diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 86d0738c6..949dda4a0 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -17,69 +17,69 @@ from tests.conftest import get_patched_exchange, log_has _STRATEGY = DefaultStrategy(config={}) -def test_returns_latest_buy_signal(mocker, default_conf, ticker_history): +def test_returns_latest_buy_signal(mocker, default_conf, ohlcv_history): mocker.patch.object( _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}]) ) - assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False) + assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (True, False) mocker.patch.object( _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}]) ) - assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True) + assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, True) -def test_returns_latest_sell_signal(mocker, default_conf, ticker_history): +def test_returns_latest_sell_signal(mocker, default_conf, ohlcv_history): mocker.patch.object( _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}]) ) - assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True) + assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, True) mocker.patch.object( _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}]) ) - assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False) + assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (True, False) def test_get_signal_empty(default_conf, mocker, caplog): assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], DataFrame()) - assert log_has('Empty ticker history for pair foo', caplog) + assert log_has('Empty candle (OHLCV) data for pair foo', caplog) caplog.clear() assert (False, False) == _STRATEGY.get_signal('bar', default_conf['ticker_interval'], []) - assert log_has('Empty ticker history for pair bar', caplog) + assert log_has('Empty candle (OHLCV) data for pair bar', caplog) -def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_history): +def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_history): caplog.set_level(logging.INFO) mocker.patch.object( _STRATEGY, '_analyze_ticker_internal', side_effect=ValueError('xyz') ) assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], - ticker_history) - assert log_has('Unable to analyze ticker for pair foo: xyz', caplog) + ohlcv_history) + assert log_has('Unable to analyze candle (OHLCV) data for pair foo: xyz', caplog) -def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ticker_history): +def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ohlcv_history): caplog.set_level(logging.INFO) mocker.patch.object( _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame([]) ) assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], - ticker_history) + ohlcv_history) assert log_has('Empty dataframe for pair xyz', caplog) -def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history): +def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): caplog.set_level(logging.INFO) # default_conf defines a 5m interval. we check interval * 2 + 5m # this is necessary as the last candle is removed (partial candles) by default @@ -90,7 +90,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history): return_value=DataFrame(ticks) ) assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], - ticker_history) + ohlcv_history) assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) @@ -103,15 +103,15 @@ def test_get_signal_handles_exceptions(mocker, default_conf): assert _STRATEGY.get_signal(exchange, 'ETH/BTC', '5m') == (False, False) -def test_tickerdata_to_dataframe(default_conf, testdatadir) -> None: +def test_ohlcvdata_to_dataframe(default_conf, testdatadir) -> None: default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) timerange = TimeRange.parse_timerange('1510694220-1510700340') - tickerlist = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, - fill_up_missing=True) - data = strategy.tickerdata_to_dataframe(tickerlist) - assert len(data['UNITTEST/BTC']) == 102 # partial candle was removed + data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, + fill_up_missing=True) + processed = strategy.ohlcvdata_to_dataframe(data) + assert len(processed['UNITTEST/BTC']) == 102 # partial candle was removed def test_min_roi_reached(default_conf, fee) -> None: @@ -222,7 +222,7 @@ def test_min_roi_reached3(default_conf, fee) -> None: assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime) -def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None: +def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) buy_mock = MagicMock(side_effect=lambda x, meta: x) @@ -235,7 +235,7 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None: ) strategy = DefaultStrategy({}) - strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'}) + strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'}) assert ind_mock.call_count == 1 assert buy_mock.call_count == 1 assert buy_mock.call_count == 1 @@ -244,7 +244,7 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None: assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) caplog.clear() - strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'}) + strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'}) # No analysis happens as process_only_new_candles is true assert ind_mock.call_count == 2 assert buy_mock.call_count == 2 @@ -253,7 +253,7 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None: assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) -def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) -> None: +def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) buy_mock = MagicMock(side_effect=lambda x, meta: x) @@ -268,7 +268,7 @@ def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) - strategy = DefaultStrategy({}) strategy.process_only_new_candles = True - ret = strategy._analyze_ticker_internal(ticker_history, {'pair': 'ETH/BTC'}) + ret = strategy._analyze_ticker_internal(ohlcv_history, {'pair': 'ETH/BTC'}) assert 'high' in ret.columns assert 'low' in ret.columns assert 'close' in ret.columns @@ -280,7 +280,7 @@ def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) - assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) caplog.clear() - ret = strategy._analyze_ticker_internal(ticker_history, {'pair': 'ETH/BTC'}) + ret = strategy._analyze_ticker_internal(ohlcv_history, {'pair': 'ETH/BTC'}) # No analysis happens as process_only_new_candles is true assert ind_mock.call_count == 1 assert buy_mock.call_count == 1 diff --git a/tests/test_misc.py b/tests/test_misc.py index 83e008466..c1e23926b 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock import pytest -from freqtrade.data.converter import parse_ticker_dataframe +from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json, file_load_json, format_ms_time, pair_to_filename, plural, shorten_date) @@ -18,9 +18,9 @@ def test_shorten_date() -> None: assert shorten_date(str_data) == str_shorten_data -def test_datesarray_to_datetimearray(ticker_history_list): - dataframes = parse_ticker_dataframe(ticker_history_list, "5m", pair="UNITTEST/BTC", - fill_missing=True) +def test_datesarray_to_datetimearray(ohlcv_history_list): + dataframes = ohlcv_to_dataframe(ohlcv_history_list, "5m", pair="UNITTEST/BTC", + fill_missing=True) dates = datesarray_to_datetimearray(dataframes['date']) assert isinstance(dates[0], datetime.datetime) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index dd04035b7..42010ad0d 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -51,15 +51,15 @@ def test_init_plotscript(default_conf, mocker, testdatadir): default_conf["datadir"] = testdatadir default_conf['exportfilename'] = str(testdatadir / "backtest-result_test.json") ret = init_plotscript(default_conf) - assert "tickers" in ret + assert "ohlcv" in ret assert "trades" in ret assert "pairs" in ret default_conf['pairs'] = ["TRX/BTC", "ADA/BTC"] ret = init_plotscript(default_conf) - assert "tickers" in ret - assert "TRX/BTC" in ret["tickers"] - assert "ADA/BTC" in ret["tickers"] + assert "ohlcv" in ret + assert "TRX/BTC" in ret["ohlcv"] + assert "ADA/BTC" in ret["ohlcv"] def test_add_indicators(default_conf, testdatadir, caplog): @@ -269,14 +269,14 @@ def test_generate_profit_graph(testdatadir): pairs = ["TRX/BTC", "ADA/BTC"] trades = trades[trades['close_time'] < pd.Timestamp('2018-01-12', tz='UTC')] - tickers = history.load_data(datadir=testdatadir, - pairs=pairs, - timeframe='5m', - timerange=timerange - ) + data = history.load_data(datadir=testdatadir, + pairs=pairs, + timeframe='5m', + timerange=timerange) + trades = trades[trades['pair'].isin(pairs)] - fig = generate_profit_graph(pairs, tickers, trades, timeframe="5m") + fig = generate_profit_graph(pairs, data, trades, timeframe="5m") assert isinstance(fig, go.Figure) assert fig.layout.title.text == "Freqtrade Profit plot" From 3eaae4661d3256ca27c92329b60661734c3ed716 Mon Sep 17 00:00:00 2001 From: orehunt Date: Mon, 9 Mar 2020 07:39:23 +0100 Subject: [PATCH 03/79] check again for emptiness after trimming dataframe --- freqtrade/data/history/idatahandler.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index df03e7713..87810c95f 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -160,6 +160,13 @@ class IDataHandler(ABC): if timerange_startup: self._validate_pairdata(pair, pairdf, timerange_startup) pairdf = trim_dataframe(pairdf, timerange_startup) + if pairdf.empty: + if warn_no_data: + logger.warning( + f'No history data for pair: "{pair}", timeframe: {timeframe}. ' + 'Use `freqtrade download-data` to download the data' + ) + return pairdf # incomplete candles should only be dropped if we didn't trim the end beforehand. return clean_ohlcv_dataframe(pairdf, timeframe, From 2b1c146940633b50dc0d9a9d225f44cc893bb088 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 10 Mar 2020 16:05:33 +0100 Subject: [PATCH 04/79] Add default volume > 0 filter --- docs/hyperopt.md | 3 +++ freqtrade/templates/base_hyperopt.py.j2 | 6 ++++++ freqtrade/templates/sample_hyperopt.py | 6 ++++++ freqtrade/templates/sample_hyperopt_advanced.py | 6 ++++++ 4 files changed, 21 insertions(+) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 9bc5888ce..1293c7ab4 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -159,6 +159,9 @@ So let's write the buy strategy using these values: dataframe['macd'], dataframe['macdsignal'] )) + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) + if conditions: dataframe.loc[ reduce(lambda x, y: x & y, conditions), diff --git a/freqtrade/templates/base_hyperopt.py.j2 b/freqtrade/templates/base_hyperopt.py.j2 index 08178da4b..ec787cbb6 100644 --- a/freqtrade/templates/base_hyperopt.py.j2 +++ b/freqtrade/templates/base_hyperopt.py.j2 @@ -66,6 +66,9 @@ class {{ hyperopt }}(IHyperOpt): dataframe['close'], dataframe['sar'] )) + # Check that the candle had volume + conditions.append(dataframe['volume'] > 0) + if conditions: dataframe.loc[ reduce(lambda x, y: x & y, conditions), @@ -111,6 +114,9 @@ class {{ hyperopt }}(IHyperOpt): dataframe['sar'], dataframe['close'] )) + # Check that the candle had volume + conditions.append(dataframe['volume'] > 0) + if conditions: dataframe.loc[ reduce(lambda x, y: x & y, conditions), diff --git a/freqtrade/templates/sample_hyperopt.py b/freqtrade/templates/sample_hyperopt.py index 0baa00442..0b6d030db 100644 --- a/freqtrade/templates/sample_hyperopt.py +++ b/freqtrade/templates/sample_hyperopt.py @@ -78,6 +78,9 @@ class SampleHyperOpt(IHyperOpt): dataframe['close'], dataframe['sar'] )) + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) + if conditions: dataframe.loc[ reduce(lambda x, y: x & y, conditions), @@ -138,6 +141,9 @@ class SampleHyperOpt(IHyperOpt): dataframe['sar'], dataframe['close'] )) + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) + if conditions: dataframe.loc[ reduce(lambda x, y: x & y, conditions), diff --git a/freqtrade/templates/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py index c8067ad28..7f05c4430 100644 --- a/freqtrade/templates/sample_hyperopt_advanced.py +++ b/freqtrade/templates/sample_hyperopt_advanced.py @@ -93,6 +93,9 @@ class AdvancedSampleHyperOpt(IHyperOpt): dataframe['close'], dataframe['sar'] )) + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) + if conditions: dataframe.loc[ reduce(lambda x, y: x & y, conditions), @@ -153,6 +156,9 @@ class AdvancedSampleHyperOpt(IHyperOpt): dataframe['sar'], dataframe['close'] )) + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) + if conditions: dataframe.loc[ reduce(lambda x, y: x & y, conditions), From 129a88d5da1879e1cae93e1771a995da6d5f0bfc Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 Mar 2020 19:53:28 +0100 Subject: [PATCH 05/79] Extract emptyness check to it's own method --- freqtrade/data/history/idatahandler.py | 27 ++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index 87810c95f..bde9612f2 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -147,12 +147,7 @@ class IDataHandler(ABC): pairdf = self._ohlcv_load(pair, timeframe, timerange=timerange_startup) - if pairdf.empty: - if warn_no_data: - logger.warning( - f'No history data for pair: "{pair}", timeframe: {timeframe}. ' - 'Use `freqtrade download-data` to download the data' - ) + if self._check_empty_df(pairdf, pair, timeframe, warn_no_data): return pairdf else: enddate = pairdf.iloc[-1]['date'] @@ -160,12 +155,7 @@ class IDataHandler(ABC): if timerange_startup: self._validate_pairdata(pair, pairdf, timerange_startup) pairdf = trim_dataframe(pairdf, timerange_startup) - if pairdf.empty: - if warn_no_data: - logger.warning( - f'No history data for pair: "{pair}", timeframe: {timeframe}. ' - 'Use `freqtrade download-data` to download the data' - ) + if self._check_empty_df(pairdf, pair, timeframe, warn_no_data): return pairdf # incomplete candles should only be dropped if we didn't trim the end beforehand. @@ -175,6 +165,19 @@ class IDataHandler(ABC): drop_incomplete=(drop_incomplete and enddate == pairdf.iloc[-1]['date'])) + def _check_empty_df(self, pairdf: DataFrame, pair: str, timeframe: str, warn_no_data: bool): + """ + Warn on empty dataframe + """ + if pairdf.empty: + if warn_no_data: + logger.warning( + f'No history data for pair: "{pair}", timeframe: {timeframe}. ' + 'Use `freqtrade download-data` to download the data' + ) + return True + return False + def _validate_pairdata(self, pair, pairdata: DataFrame, timerange: TimeRange): """ Validates pairdata for missing data at start end end and logs warnings. From 6f67b8d9b900961afb93508214d981925261d3ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 12 Mar 2020 19:50:46 +0100 Subject: [PATCH 06/79] iCheck after clean_dataframe, too --- freqtrade/data/history/idatahandler.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index bde9612f2..b0a6a97dc 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -159,11 +159,13 @@ class IDataHandler(ABC): return pairdf # incomplete candles should only be dropped if we didn't trim the end beforehand. - return clean_ohlcv_dataframe(pairdf, timeframe, - pair=pair, - fill_missing=fill_missing, - drop_incomplete=(drop_incomplete and - enddate == pairdf.iloc[-1]['date'])) + pairdf = clean_ohlcv_dataframe(pairdf, timeframe, + pair=pair, + fill_missing=fill_missing, + drop_incomplete=(drop_incomplete and + enddate == pairdf.iloc[-1]['date'])) + self._check_empty_df(pairdf, pair, timeframe, warn_no_data) + return pairdf def _check_empty_df(self, pairdf: DataFrame, pair: str, timeframe: str, warn_no_data: bool): """ From ebb0187f4040aa7f23ed54cacd8faa9919640994 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 13 Mar 2020 03:54:56 +0300 Subject: [PATCH 07/79] dataframe -> df_analyzed in backtesting and edge --- freqtrade/edge/edge_positioning.py | 14 +++++++------- freqtrade/optimize/backtesting.py | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index a24e29efb..256b67383 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -137,10 +137,10 @@ class Edge: pair_data = pair_data.sort_values(by=['date']) pair_data = pair_data.reset_index(drop=True) - dataframe = self.strategy.advise_sell( + df_analyzed = self.strategy.advise_sell( self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() - trades += self._find_trades_for_stoploss_range(dataframe, pair, self._stoploss_range) + trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range) # If no trade found then exit if len(trades) == 0: @@ -359,11 +359,11 @@ class Edge: # Returning a list of pairs in order of "expectancy" return final - def _find_trades_for_stoploss_range(self, dataframe, pair, stoploss_range): - buy_column = dataframe['buy'].values - sell_column = dataframe['sell'].values - date_column = dataframe['date'].values - ohlc_columns = dataframe[['open', 'high', 'low', 'close']].values + def _find_trades_for_stoploss_range(self, df, pair, stoploss_range): + buy_column = df['buy'].values + sell_column = df['sell'].values + date_column = df['date'].values + ohlc_columns = df[['open', 'high', 'low', 'close']].values result: list = [] for stoploss in stoploss_range: diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 949c072c5..210fe3c66 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -164,19 +164,19 @@ class Backtesting: pair_data.loc[:, 'buy'] = 0 # cleanup from previous run pair_data.loc[:, 'sell'] = 0 # cleanup from previous run - dataframe = self.strategy.advise_sell( + df_analyzed = self.strategy.advise_sell( self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() # To avoid using data from future, we use buy/sell signals shifted # from the previous candle - dataframe.loc[:, 'buy'] = dataframe['buy'].shift(1) - dataframe.loc[:, 'sell'] = dataframe['sell'].shift(1) + df_analyzed.loc[:, 'buy'] = df_analyzed['buy'].shift(1) + df_analyzed.loc[:, 'sell'] = df_analyzed['sell'].shift(1) - dataframe.drop(dataframe.head(1).index, inplace=True) + df_analyzed.drop(df_analyzed.head(1).index, inplace=True) # Convert from Pandas to list for performance reasons # (Looping Pandas is slow.) - data[pair] = [x for x in dataframe.itertuples()] + data[pair] = [x for x in df_analyzed.itertuples()] return data def _get_close_rate(self, sell_row, trade: Trade, sell: SellCheckTuple, From b2952cd42ad22ecb0e623909524c07ac273803f3 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 13 Mar 2020 03:58:16 +0300 Subject: [PATCH 08/79] remove redundant dict --- freqtrade/plot/plotting.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index cfbda6714..220056d2d 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -454,10 +454,8 @@ def load_and_plot_trades(config: Dict[str, Any]): for pair, data in plot_elements["ohlcv"].items(): pair_counter += 1 logger.info("analyse pair %s", pair) - ohlcv = {} - ohlcv[pair] = data - dataframe = strategy.analyze_ticker(ohlcv[pair], {'pair': pair}) + dataframe = strategy.analyze_ticker(data, {'pair': pair}) trades_pair = trades.loc[trades['pair'] == pair] trades_pair = extract_trades_of_period(dataframe, trades_pair) From ddfe5b5f1cb4788936699afbc36c1f19e85d2fc4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 13 Mar 2020 04:00:24 +0300 Subject: [PATCH 09/79] dataframe -> df_analyzed in plotting --- freqtrade/plot/plotting.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 220056d2d..be7be2de0 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -455,13 +455,13 @@ def load_and_plot_trades(config: Dict[str, Any]): pair_counter += 1 logger.info("analyse pair %s", pair) - dataframe = strategy.analyze_ticker(data, {'pair': pair}) + df_analyzed = strategy.analyze_ticker(data, {'pair': pair}) trades_pair = trades.loc[trades['pair'] == pair] - trades_pair = extract_trades_of_period(dataframe, trades_pair) + trades_pair = extract_trades_of_period(df_analyzed, trades_pair) fig = generate_candlestick_graph( pair=pair, - data=dataframe, + data=df_analyzed, trades=trades_pair, indicators1=config.get("indicators1", []), indicators2=config.get("indicators2", []), From a7ed51c6424202dccb27f44a847721dbbf4eecde Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 13 Mar 2020 04:04:23 +0300 Subject: [PATCH 10/79] return back the name of the hyperopt data file --- freqtrade/optimize/hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index ed58db977..a6b13f93f 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -76,7 +76,7 @@ class Hyperopt: self.trials_file = (self.config['user_data_dir'] / 'hyperopt_results' / 'hyperopt_results.pickle') self.data_pickle_file = (self.config['user_data_dir'] / - 'hyperopt_results' / 'hyperopt_data.pkl') + 'hyperopt_results' / 'hyperopt_tickerdata.pkl') self.total_epochs = config.get('epochs', 0) self.current_best_loss = 100 From 59fadabb5ba01bc113c0d87cced31a7e7074df66 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 13 Mar 2020 20:26:14 +0300 Subject: [PATCH 11/79] Fix merging --- freqtrade/data/history/idatahandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index ae050103b..1bb4d5971 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -150,7 +150,7 @@ class IDataHandler(ABC): if self._check_empty_df(pairdf, pair, timeframe, warn_no_data): return pairdf else: - enddate = df.iloc[-1]['date'] + enddate = pairdf.iloc[-1]['date'] if timerange_startup: self._validate_pairdata(pair, pairdf, timerange_startup) From c56cbc21b1658102d912435fa00316ae3da51443 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Mar 2020 10:42:01 +0100 Subject: [PATCH 12/79] Remove indexing warning in edge --- freqtrade/edge/edge_positioning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 256b67383..d196ab4b3 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -317,7 +317,7 @@ class Edge: } # Group by (pair and stoploss) by applying above aggregator - df = results.groupby(['pair', 'stoploss'])['profit_abs', 'trade_duration'].agg( + df = results.groupby(['pair', 'stoploss'])[['profit_abs', 'trade_duration']].agg( groupby_aggregator).reset_index(col_level=1) # Dropping level 0 as we don't need it From 308d8fe2a9daf78fd58642c884dd6a9c4bd9a119 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Mar 2020 10:44:46 +0100 Subject: [PATCH 13/79] Remove deprecation warnings due to date conversion --- tests/edge/test_edge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index c68ac477c..3bebeee65 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -163,8 +163,8 @@ def test_edge_results(edge_conf, mocker, caplog, data) -> None: for c, trade in enumerate(data.trades): res = results.iloc[c] assert res.exit_type == trade.sell_reason - assert res.open_time == np.datetime64(_get_frame_time_from_offset(trade.open_tick)) - assert res.close_time == np.datetime64(_get_frame_time_from_offset(trade.close_tick)) + assert res.open_time == _get_frame_time_from_offset(trade.open_tick).replace(tzinfo=None) + assert res.close_time == _get_frame_time_from_offset(trade.close_tick).replace(tzinfo=None) def test_adjust(mocker, edge_conf): From 27faf12fdeef84f404aa7bf32f27e7591cc1a910 Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Sat, 14 Mar 2020 22:15:03 +0100 Subject: [PATCH 14/79] Fix if no file exists --- freqtrade/commands/arguments.py | 2 +- freqtrade/commands/cli_options.py | 5 +++++ freqtrade/data/btanalysis.py | 8 ++++++-- freqtrade/plot/plotting.py | 19 +++++++++++++++---- tests/data/test_btanalysis.py | 5 ++++- 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 1a8cca72b..66fa0b038 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -59,7 +59,7 @@ ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchang ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", "export", "exportfilename", - "timerange", "ticker_interval"] + "timerange", "ticker_interval", "skip_trades"] ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source", "ticker_interval"] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 42c697d56..187e0a424 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -413,6 +413,11 @@ AVAILABLE_CLI_OPTIONS = { metavar='INT', default=750, ), + "skip_trades": Arg( + '--skip-trades', + help='Skip using trades file from backtesting and DB.', + action='store_true', + ), "trade_source": Arg( '--trade-source', help='Specify the source for trades (Can be DB or file (backtest file)) ' diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 681bf6734..fc938ad7e 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -111,7 +111,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: 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), + if t.close_date else None), t.sell_reason, t.fee_open, t.fee_close, t.open_rate_requested, @@ -129,12 +129,16 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: return trades -def load_trades(source: str, db_url: str, exportfilename: str) -> pd.DataFrame: +def load_trades(source: str, db_url: str, exportfilename: str, skip_trades: bool) -> pd.DataFrame: """ Based on configuration option "trade_source": * loads data from DB (using `db_url`) * loads data from backtestfile (using `exportfilename`) """ + if skip_trades: + df = pd.DataFrame(columns=BT_DATA_COLUMNS) + return df + if source == "DB": return load_trades_from_db(db_url) elif source == "file": diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index be7be2de0..cb2686878 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -3,6 +3,7 @@ from pathlib import Path from typing import Any, Dict, List import pandas as pd +from os.path import isfile from freqtrade.configuration import TimeRange from freqtrade.data.btanalysis import (calculate_max_drawdown, @@ -48,11 +49,21 @@ def init_plotscript(config): data_format=config.get('dataformat_ohlcv', 'json'), ) - trades = load_trades(config['trade_source'], - db_url=config.get('db_url'), - exportfilename=config.get('exportfilename'), - ) + skip_trades = False + if not isfile(config.get('exportfilename')) and config['trade_source'] == 'file': + logger.info("Backtest file is missing skipping trades.") + skip_trades = True + elif config.get('skip_trades', False): + skip_trades = True + + trades = load_trades( + config['trade_source'], + db_url=config.get('db_url'), + exportfilename=config.get('exportfilename'), + skip_trades=skip_trades + ) trades = trim_dataframe(trades, timerange, 'open_time') + return {"ohlcv": data, "trades": trades, "pairs": pairs, diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index da5d225b9..44e9c1072 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -104,6 +104,7 @@ def test_load_trades(default_conf, mocker): load_trades("DB", db_url=default_conf.get('db_url'), exportfilename=default_conf.get('exportfilename'), + skip_trades=False ) assert db_mock.call_count == 1 @@ -114,7 +115,9 @@ def test_load_trades(default_conf, mocker): default_conf['exportfilename'] = "testfile.json" load_trades("file", db_url=default_conf.get('db_url'), - exportfilename=default_conf.get('exportfilename'),) + exportfilename=default_conf.get('exportfilename'), + skip_trades=False + ) assert db_mock.call_count == 0 assert bt_mock.call_count == 1 From cf7e80f45d5d664c1dcb1cc92335cffb67186d12 Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Sat, 14 Mar 2020 23:55:13 +0100 Subject: [PATCH 15/79] Docs and logging --- docs/plotting.md | 62 +++++++++++++++++++++++++------------- freqtrade/plot/plotting.py | 2 +- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index 3eef8f8e7..ef28f0fe3 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -23,44 +23,64 @@ The `freqtrade plot-dataframe` subcommand shows an interactive graph with three Possible arguments: ``` -usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] - [--strategy-path PATH] [-p PAIRS [PAIRS ...]] [--indicators1 INDICATORS1 [INDICATORS1 ...]] - [--indicators2 INDICATORS2 [INDICATORS2 ...]] [--plot-limit INT] [--db-url PATH] - [--trade-source {DB,file}] [--export EXPORT] [--export-filename PATH] [--timerange TIMERANGE] - [-i TICKER_INTERVAL] +usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH] + [-d PATH] [--userdir PATH] [-s NAME] + [--strategy-path PATH] [-p PAIRS [PAIRS ...]] + [--indicators1 INDICATORS1 [INDICATORS1 ...]] + [--indicators2 INDICATORS2 [INDICATORS2 ...]] + [--plot-limit INT] [--db-url PATH] + [--trade-source {DB,file}] [--export EXPORT] + [--export-filename PATH] + [--timerange TIMERANGE] [-i TICKER_INTERVAL] + [--skip-trades] optional arguments: -h, --help show this help message and exit -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] - Show profits for only these pairs. Pairs are space-separated. + Show profits for only these pairs. Pairs are space- + separated. --indicators1 INDICATORS1 [INDICATORS1 ...] - Set indicators from your strategy you want in the first row of the graph. Space-separated list. Example: + Set indicators from your strategy you want in the + first row of the graph. Space-separated list. Example: `ema3 ema5`. Default: `['sma', 'ema3', 'ema5']`. --indicators2 INDICATORS2 [INDICATORS2 ...] - Set indicators from your strategy you want in the third row of the graph. Space-separated list. Example: + Set indicators from your strategy you want in the + third row of the graph. Space-separated list. Example: `fastd fastk`. Default: `['macd', 'macdsignal']`. - --plot-limit INT Specify tick limit for plotting. Notice: too high values cause huge files. Default: 750. - --db-url PATH Override trades database URL, this is useful in custom deployments (default: `sqlite:///tradesv3.sqlite` - for Live Run mode, `sqlite:///tradesv3.dryrun.sqlite` for Dry Run). + --plot-limit INT Specify tick limit for plotting. Notice: too high + values cause huge files. Default: 750. + --db-url PATH Override trades database URL, this is useful in custom + deployments (default: `sqlite:///tradesv3.sqlite` for + Live Run mode, `sqlite:///tradesv3.dryrun.sqlite` for + Dry Run). --trade-source {DB,file} - Specify the source for trades (Can be DB or file (backtest file)) Default: file - --export EXPORT Export backtest results, argument are: trades. Example: `--export=trades` + Specify the source for trades (Can be DB or file + (backtest file)) Default: file + --export EXPORT Export backtest results, argument are: trades. + Example: `--export=trades` --export-filename PATH - Save backtest results to the file with this filename. Requires `--export` to be set as well. Example: - `--export-filename=user_data/backtest_results/backtest_today.json` + Save backtest results to the file with this filename. + Requires `--export` to be set as well. Example: + `--export-filename=user_data/backtest_results/backtest + _today.json` --timerange TIMERANGE Specify what timerange of data to use. -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`). + Specify ticker interval (`1m`, `5m`, `30m`, `1h`, + `1d`). + --skip-trades Skip using trades file from backtesting and DB. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --logfile FILE Log to the file specified. Special values are: 'syslog', 'journald'. See the documentation for more + --logfile FILE Log to the file specified. Special values are: + 'syslog', 'journald'. See the documentation for more details. -V, --version show program's version number and exit -c PATH, --config PATH - Specify configuration file (default: `config.json`). Multiple --config options may be used. Can be set to - `-` to read config from stdin. + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. -d PATH, --datadir PATH Path to directory with historical backtesting data. --userdir PATH, --user-data-dir PATH @@ -68,9 +88,9 @@ Common arguments: Strategy arguments: -s NAME, --strategy NAME - Specify strategy class name which will be used by the bot. + Specify strategy class name which will be used by the + bot. --strategy-path PATH Specify additional strategy lookup path. - ``` Example: diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index cb2686878..8da61597f 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -51,7 +51,7 @@ def init_plotscript(config): skip_trades = False if not isfile(config.get('exportfilename')) and config['trade_source'] == 'file': - logger.info("Backtest file is missing skipping trades.") + logger.warning("Backtest file is missing skipping trades.") skip_trades = True elif config.get('skip_trades', False): skip_trades = True From 2c0980aa3ab2af40e46411819f2e53b76eae6fa4 Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Sun, 15 Mar 2020 00:09:08 +0100 Subject: [PATCH 16/79] Tests --- tests/data/test_btanalysis.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 44e9c1072..95f371d7f 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -122,6 +122,18 @@ def test_load_trades(default_conf, mocker): assert db_mock.call_count == 0 assert bt_mock.call_count == 1 + db_mock.reset_mock() + bt_mock.reset_mock() + default_conf['exportfilename'] = "testfile.json" + load_trades("file", + db_url=default_conf.get('db_url'), + exportfilename=default_conf.get('exportfilename'), + skip_trades=True + ) + + assert db_mock.call_count == 0 + assert bt_mock.call_count == 0 + def test_combine_dataframes_with_mean(testdatadir): pairs = ["ETH/BTC", "ADA/BTC"] From 0f1640bed4691cc0d27ed1d665e77c8d2dd85944 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Mar 2020 09:39:45 +0100 Subject: [PATCH 17/79] convert exportfilename to Path when config parsing --- freqtrade/configuration/configuration.py | 1 + freqtrade/data/btanalysis.py | 8 ++++++-- freqtrade/optimize/backtesting.py | 2 +- tests/data/test_btanalysis.py | 5 +++-- tests/optimize/test_backtesting.py | 1 + tests/test_plotting.py | 6 +++--- 6 files changed, 15 insertions(+), 8 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 0645d72be..ce2101441 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -196,6 +196,7 @@ class Configuration: if self.args.get('exportfilename'): self._args_to_config(config, argname='exportfilename', logstring='Storing backtest results to {} ...') + config['exportfilename'] = Path(config['exportfilename']) else: config['exportfilename'] = (config['user_data_dir'] / 'backtest_results/backtest-result.json') diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 681bf6734..e8ec03fea 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -129,16 +129,20 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: return trades -def load_trades(source: str, db_url: str, exportfilename: str) -> pd.DataFrame: +def load_trades(source: str, db_url: str, exportfilename: Path) -> pd.DataFrame: """ Based on configuration option "trade_source": * loads data from DB (using `db_url`) * loads data from backtestfile (using `exportfilename`) + :param source: "DB" or "file" - specify source to load from + :param db_url: sqlalchemy formatted url to a database + :param exportfilename: Json file generated by backtesting + :return: DataFrame containing trades """ if source == "DB": return load_trades_from_db(db_url) elif source == "file": - return load_backtest_data(Path(exportfilename)) + return load_backtest_data(exportfilename) def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> pd.DataFrame: diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 210fe3c66..40e6590f7 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -421,7 +421,7 @@ class Backtesting: for strategy, results in all_results.items(): if self.config.get('export', False): - self._store_backtest_result(Path(self.config['exportfilename']), results, + self._store_backtest_result(self.config['exportfilename'], results, strategy if len(self.strategylist) > 1 else None) print(f"Result for strategy {strategy}") diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index da5d225b9..7513991ea 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -1,8 +1,9 @@ +from pathlib import Path from unittest.mock import MagicMock import pytest from arrow import Arrow -from pandas import DataFrame, DateOffset, to_datetime, Timestamp +from pandas import DataFrame, DateOffset, Timestamp, to_datetime from freqtrade.configuration import TimeRange from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, @@ -111,7 +112,7 @@ def test_load_trades(default_conf, mocker): db_mock.reset_mock() bt_mock.reset_mock() - default_conf['exportfilename'] = "testfile.json" + default_conf['exportfilename'] = Path("testfile.json") load_trades("file", db_url=default_conf.get('db_url'), exportfilename=default_conf.get('exportfilename'),) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index f78b44704..da23a9af4 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -224,6 +224,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> assert 'export' in config assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog) assert 'exportfilename' in config + assert isinstance(config['exportfilename'], Path) assert log_has('Storing backtest results to {} ...'.format(config['exportfilename']), caplog) assert 'fee' in config diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 42010ad0d..a5c965429 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -49,7 +49,7 @@ def test_init_plotscript(default_conf, mocker, testdatadir): default_conf['trade_source'] = "file" default_conf['ticker_interval'] = "5m" default_conf["datadir"] = testdatadir - default_conf['exportfilename'] = str(testdatadir / "backtest-result_test.json") + default_conf['exportfilename'] = testdatadir / "backtest-result_test.json" ret = init_plotscript(default_conf) assert "ohlcv" in ret assert "trades" in ret @@ -318,7 +318,7 @@ def test_start_plot_dataframe(mocker): def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir): default_conf['trade_source'] = 'file' default_conf["datadir"] = testdatadir - default_conf['exportfilename'] = str(testdatadir / "backtest-result_test.json") + default_conf['exportfilename'] = testdatadir / "backtest-result_test.json" default_conf['indicators1'] = ["sma5", "ema10"] default_conf['indicators2'] = ["macd"] default_conf['pairs'] = ["ETH/BTC", "LTC/BTC"] @@ -374,7 +374,7 @@ def test_start_plot_profit_error(mocker): def test_plot_profit(default_conf, mocker, testdatadir, caplog): default_conf['trade_source'] = 'file' default_conf["datadir"] = testdatadir - default_conf['exportfilename'] = str(testdatadir / "backtest-result_test.json") + default_conf['exportfilename'] = testdatadir / "backtest-result_test.json" default_conf['pairs'] = ["ETH/BTC", "LTC/BTC"] profit_mock = MagicMock() From 328dbd3930981103d2646e27e53156f104e59745 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Mar 2020 15:04:48 +0100 Subject: [PATCH 18/79] Remove unnecessary parameter to generate_text_table_sell_reason --- freqtrade/optimize/backtesting.py | 7 +++---- freqtrade/optimize/optimize_reports.py | 8 ++++---- tests/optimize/test_optimize_reports.py | 6 ++---- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 40e6590f7..5ee5a058f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -7,7 +7,7 @@ import logging from copy import deepcopy from datetime import datetime, timedelta from pathlib import Path -from typing import Any, Dict, List, NamedTuple, Optional +from typing import Any, Dict, List, NamedTuple, Optional, Tuple import arrow from pandas import DataFrame @@ -108,7 +108,7 @@ class Backtesting: # And the regular "stoploss" function would not apply to that case self.strategy.order_types['stoploss_on_exchange'] = False - def load_bt_data(self): + def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]: timerange = TimeRange.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) @@ -432,8 +432,7 @@ class Backtesting: print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) print(table) - table = generate_text_table_sell_reason(data, - stake_currency=self.config['stake_currency'], + table = generate_text_table_sell_reason(stake_currency=self.config['stake_currency'], max_open_trades=self.config['max_open_trades'], results=results) if isinstance(table, str): diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 39bde50a8..ee29aa283 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -69,12 +69,12 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore -def generate_text_table_sell_reason( - data: Dict[str, Dict], stake_currency: str, max_open_trades: int, results: DataFrame -) -> str: +def generate_text_table_sell_reason(stake_currency: str, max_open_trades: int, + results: DataFrame) -> str: """ Generate small table outlining Backtest results - :param data: Dict of containing data that was used during backtesting. + :param stake_currency: Stakecurrency used + :param max_open_trades: Max_open_trades parameter :param results: Dataframe containing the backtest results :return: pretty printed table with tabulate as string """ diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 285ecaa02..5e68a5ef8 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -61,10 +61,8 @@ def test_generate_text_table_sell_reason(default_conf, mocker): '| stop_loss | 1 | 0 | 0 | 1 |' ' -10 | -10 | -0.2 | -5 |' ) - assert generate_text_table_sell_reason( - data={'ETH/BTC': {}}, - stake_currency='BTC', max_open_trades=2, - results=results) == result_str + assert generate_text_table_sell_reason(stake_currency='BTC', max_open_trades=2, + results=results) == result_str def test_generate_text_table_strategy(default_conf, mocker): From 6106d59e1a0122863849e03e671db698f02c5c96 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Mar 2020 15:17:35 +0100 Subject: [PATCH 19/79] Move store_backtest_results to optimize_reports --- freqtrade/optimize/backtesting.py | 18 ------------------ freqtrade/optimize/optimize_reports.py | 26 +++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 5ee5a058f..9dd1b8429 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -6,7 +6,6 @@ This module contains the backtesting logic import logging from copy import deepcopy from datetime import datetime, timedelta -from pathlib import Path from typing import Any, Dict, List, NamedTuple, Optional, Tuple import arrow @@ -134,23 +133,6 @@ class Backtesting: return data, timerange - def _store_backtest_result(self, recordfilename: Path, results: DataFrame, - strategyname: Optional[str] = None) -> None: - - records = [(t.pair, t.profit_percent, t.open_time.timestamp(), - t.close_time.timestamp(), t.open_index - 1, t.trade_duration, - t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value) - for index, t in results.iterrows()] - - if records: - if strategyname: - # Inject strategyname to filename - recordfilename = Path.joinpath( - recordfilename.parent, - f'{recordfilename.stem}-{strategyname}').with_suffix(recordfilename.suffix) - logger.info(f'Dumping backtest results to {recordfilename}') - file_dump_json(recordfilename, records) - def _get_ohlcv_as_lists(self, processed: Dict) -> Dict[str, DataFrame]: """ Helper function to convert a processed dataframes into lists for performance reasons. diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index ee29aa283..fb407f681 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -1,9 +1,33 @@ +import logging from datetime import timedelta -from typing import Dict +from pathlib import Path +from typing import Dict, Optional from pandas import DataFrame from tabulate import tabulate +from freqtrade.misc import file_dump_json + +logger = logging.getLogger(__name__) + + +def store_backtest_result(recordfilename: Path, results: DataFrame, + strategyname: Optional[str] = None) -> None: + + records = [(t.pair, t.profit_percent, t.open_time.timestamp(), + t.close_time.timestamp(), t.open_index - 1, t.trade_duration, + t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value) + for index, t in results.iterrows()] + + if records: + if strategyname: + # Inject strategyname to filename + recordfilename = Path.joinpath( + recordfilename.parent, + f'{recordfilename.stem}-{strategyname}').with_suffix(recordfilename.suffix) + logger.info(f'Dumping backtest results to {recordfilename}') + file_dump_json(recordfilename, records) + def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, results: DataFrame, skip_nan: bool = False) -> str: From a13d581658173fb764c110ae9966767f17c85e7d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Mar 2020 15:17:53 +0100 Subject: [PATCH 20/79] Move backtest-result visualization out of backtesting class --- freqtrade/optimize/backtesting.py | 48 ++------------------------ freqtrade/optimize/optimize_reports.py | 44 +++++++++++++++++++++++ 2 files changed, 47 insertions(+), 45 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9dd1b8429..323331bc6 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -18,10 +18,7 @@ from freqtrade.data.converter import trim_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds -from freqtrade.misc import file_dump_json -from freqtrade.optimize.optimize_reports import ( - generate_text_table, generate_text_table_sell_reason, - generate_text_table_strategy) +from freqtrade.optimize.optimize_reports import show_backtest_results from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode @@ -399,44 +396,5 @@ class Backtesting: max_open_trades=max_open_trades, position_stacking=position_stacking, ) - - for strategy, results in all_results.items(): - - if self.config.get('export', False): - self._store_backtest_result(self.config['exportfilename'], results, - strategy if len(self.strategylist) > 1 else None) - - print(f"Result for strategy {strategy}") - table = generate_text_table(data, stake_currency=self.config['stake_currency'], - max_open_trades=self.config['max_open_trades'], - results=results) - if isinstance(table, str): - print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) - print(table) - - table = generate_text_table_sell_reason(stake_currency=self.config['stake_currency'], - max_open_trades=self.config['max_open_trades'], - results=results) - if isinstance(table, str): - print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) - print(table) - - table = generate_text_table(data, - stake_currency=self.config['stake_currency'], - max_open_trades=self.config['max_open_trades'], - results=results.loc[results.open_at_end], skip_nan=True) - if isinstance(table, str): - print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) - print(table) - if isinstance(table, str): - print('=' * len(table.splitlines()[0])) - print() - if len(all_results) > 1: - # Print Strategy summary table - table = generate_text_table_strategy(self.config['stake_currency'], - self.config['max_open_trades'], - all_results=all_results) - print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '=')) - print(table) - print('=' * len(table.splitlines()[0])) - print('\nFor more details, please look at the detail tables above') + # Show backtest results + show_backtest_results(self.config, data, all_results) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index fb407f681..fef0accb0 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -197,3 +197,47 @@ def generate_edge_table(results: dict) -> str: # Ignore type as floatfmt does allow tuples but mypy does not know that return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore + + +def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], + all_results: Dict[str, DataFrame]): + for strategy, results in all_results.items(): + + if config.get('export', False): + store_backtest_result(config['exportfilename'], results, + strategy if len(all_results) > 1 else None) + + print(f"Result for strategy {strategy}") + table = generate_text_table(btdata, stake_currency=config['stake_currency'], + max_open_trades=config['max_open_trades'], + results=results) + if isinstance(table, str): + print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) + print(table) + + table = generate_text_table_sell_reason(stake_currency=config['stake_currency'], + max_open_trades=config['max_open_trades'], + results=results) + if isinstance(table, str): + print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) + print(table) + + table = generate_text_table(btdata, + stake_currency=config['stake_currency'], + max_open_trades=config['max_open_trades'], + results=results.loc[results.open_at_end], skip_nan=True) + if isinstance(table, str): + print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) + print(table) + if isinstance(table, str): + print('=' * len(table.splitlines()[0])) + print() + if len(all_results) > 1: + # Print Strategy summary table + table = generate_text_table_strategy(config['stake_currency'], + config['max_open_trades'], + all_results=all_results) + print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '=')) + print(table) + print('=' * len(table.splitlines()[0])) + print('\nFor more details, please look at the detail tables above') From e95665cecaeec2337dee47660674401535b406a7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Mar 2020 15:36:23 +0100 Subject: [PATCH 21/79] Make backtestresult storing independent from printing --- freqtrade/optimize/backtesting.py | 6 ++++- freqtrade/optimize/optimize_reports.py | 36 ++++++++++++-------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 323331bc6..1725a7d13 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -18,7 +18,8 @@ from freqtrade.data.converter import trim_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds -from freqtrade.optimize.optimize_reports import show_backtest_results +from freqtrade.optimize.optimize_reports import (show_backtest_results, + store_backtest_result) from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode @@ -396,5 +397,8 @@ class Backtesting: max_open_trades=max_open_trades, position_stacking=position_stacking, ) + + if self.config.get('export', False): + store_backtest_result(self.config['exportfilename'], all_results) # Show backtest results show_backtest_results(self.config, data, all_results) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index fef0accb0..701432071 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -11,22 +11,22 @@ from freqtrade.misc import file_dump_json logger = logging.getLogger(__name__) -def store_backtest_result(recordfilename: Path, results: DataFrame, - strategyname: Optional[str] = None) -> None: +def store_backtest_result(recordfilename: Path, all_results: Dict[str, DataFrame]) -> None: - records = [(t.pair, t.profit_percent, t.open_time.timestamp(), - t.close_time.timestamp(), t.open_index - 1, t.trade_duration, - t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value) - for index, t in results.iterrows()] + for strategy, results in all_results.items(): + records = [(t.pair, t.profit_percent, t.open_time.timestamp(), + t.close_time.timestamp(), t.open_index - 1, t.trade_duration, + t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value) + for index, t in results.iterrows()] - if records: - if strategyname: - # Inject strategyname to filename - recordfilename = Path.joinpath( - recordfilename.parent, - f'{recordfilename.stem}-{strategyname}').with_suffix(recordfilename.suffix) - logger.info(f'Dumping backtest results to {recordfilename}') - file_dump_json(recordfilename, records) + if records: + if len(all_results) > 1: + # Inject strategy to filename + recordfilename = Path.joinpath( + recordfilename.parent, + f'{recordfilename.stem}-{strategy}').with_suffix(recordfilename.suffix) + logger.info(f'Dumping backtest results to {recordfilename}') + file_dump_json(recordfilename, records) def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, @@ -203,10 +203,6 @@ def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], all_results: Dict[str, DataFrame]): for strategy, results in all_results.items(): - if config.get('export', False): - store_backtest_result(config['exportfilename'], results, - strategy if len(all_results) > 1 else None) - print(f"Result for strategy {strategy}") table = generate_text_table(btdata, stake_currency=config['stake_currency'], max_open_trades=config['max_open_trades'], @@ -235,8 +231,8 @@ def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame], if len(all_results) > 1: # Print Strategy summary table table = generate_text_table_strategy(config['stake_currency'], - config['max_open_trades'], - all_results=all_results) + config['max_open_trades'], + all_results=all_results) print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '=')) print(table) print('=' * len(table.splitlines()[0])) From fe50a0f3a13e0d0f92084314af081ce1230acaac Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Mar 2020 15:36:53 +0100 Subject: [PATCH 22/79] Move test for store_bt_results to optimize_reports --- tests/optimize/test_backtesting.py | 89 +++---------------------- tests/optimize/test_optimize_reports.py | 76 ++++++++++++++++++++- 2 files changed, 83 insertions(+), 82 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index da23a9af4..1c4d3b16a 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -331,8 +331,8 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: mocker.patch('freqtrade.data.history.get_timerange', get_timerange) patch_exchange(mocker) - mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) - mocker.patch('freqtrade.optimize.backtesting.generate_text_table', MagicMock(return_value=1)) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') + mocker.patch('freqtrade.optimize.backtesting.show_backtest_results') default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['ticker_interval'] = '1m' @@ -361,8 +361,8 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> MagicMock(return_value=pd.DataFrame())) mocker.patch('freqtrade.data.history.get_timerange', get_timerange) patch_exchange(mocker) - mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) - mocker.patch('freqtrade.optimize.backtesting.generate_text_table', MagicMock(return_value=1)) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') + mocker.patch('freqtrade.optimize.backtesting.show_backtest_results') default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['ticker_interval'] = "1m" @@ -507,7 +507,6 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir): def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch('freqtrade.optimize.backtesting.file_dump_json', MagicMock()) backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC', datadir=testdatadir) default_conf['ticker_interval'] = '1m' @@ -515,7 +514,6 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): backtesting.strategy.advise_buy = _trend_alternate # Override backtesting.strategy.advise_sell = _trend_alternate # Override results = backtesting.backtest(**backtest_conf) - backtesting._store_backtest_result("test_.json", results) # 200 candles in backtest data # won't buy on first (shifted by 1) # 100 buys signals @@ -586,84 +584,12 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) assert len(evaluate_result_multi(results, '5m', 1)) == 0 -def test_backtest_record(default_conf, fee, mocker): - names = [] - records = [] - patch_exchange(mocker) - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch( - 'freqtrade.optimize.backtesting.file_dump_json', - new=lambda n, r: (names.append(n), records.append(r)) - ) - - backtesting = Backtesting(default_conf) - results = pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", - "UNITTEST/BTC", "UNITTEST/BTC"], - "profit_percent": [0.003312, 0.010801, 0.013803, 0.002780], - "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], - "open_time": [Arrow(2017, 11, 14, 19, 32, 00).datetime, - Arrow(2017, 11, 14, 21, 36, 00).datetime, - Arrow(2017, 11, 14, 22, 12, 00).datetime, - Arrow(2017, 11, 14, 22, 44, 00).datetime], - "close_time": [Arrow(2017, 11, 14, 21, 35, 00).datetime, - Arrow(2017, 11, 14, 22, 10, 00).datetime, - Arrow(2017, 11, 14, 22, 43, 00).datetime, - Arrow(2017, 11, 14, 22, 58, 00).datetime], - "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], - "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], - "open_index": [1, 119, 153, 185], - "close_index": [118, 151, 184, 199], - "trade_duration": [123, 34, 31, 14], - "open_at_end": [False, False, False, True], - "sell_reason": [SellType.ROI, SellType.STOP_LOSS, - SellType.ROI, SellType.FORCE_SELL] - }) - backtesting._store_backtest_result("backtest-result.json", results) - assert len(results) == 4 - # Assert file_dump_json was only called once - assert names == ['backtest-result.json'] - records = records[0] - # Ensure records are of correct type - assert len(records) == 4 - - # reset test to test with strategy name - names = [] - records = [] - backtesting._store_backtest_result(Path("backtest-result.json"), results, "DefStrat") - assert len(results) == 4 - # Assert file_dump_json was only called once - assert names == [Path('backtest-result-DefStrat.json')] - records = records[0] - # Ensure records are of correct type - assert len(records) == 4 - - # ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117) - # Below follows just a typecheck of the schema/type of trade-records - oix = None - for (pair, profit, date_buy, date_sell, buy_index, dur, - openr, closer, open_at_end, sell_reason) in records: - assert pair == 'UNITTEST/BTC' - assert isinstance(profit, float) - # FIX: buy/sell should be converted to ints - assert isinstance(date_buy, float) - assert isinstance(date_sell, float) - assert isinstance(openr, float) - assert isinstance(closer, float) - assert isinstance(open_at_end, bool) - assert isinstance(sell_reason, str) - isinstance(buy_index, pd._libs.tslib.Timestamp) - if oix: - assert buy_index > oix - oix = buy_index - assert dur > 0 - - def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) - mocker.patch('freqtrade.optimize.backtesting.generate_text_table', MagicMock()) + mocker.patch('freqtrade.optimize.backtesting.show_backtest_results', MagicMock()) patched_configuration_load_config_file(mocker, default_conf) @@ -705,9 +631,10 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): backtestmock = MagicMock() mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) gen_table_mock = MagicMock() - mocker.patch('freqtrade.optimize.backtesting.generate_text_table', gen_table_mock) + mocker.patch('freqtrade.optimize.optimize_reports.generate_text_table', gen_table_mock) gen_strattable_mock = MagicMock() - mocker.patch('freqtrade.optimize.backtesting.generate_text_table_strategy', gen_strattable_mock) + mocker.patch('freqtrade.optimize.optimize_reports.generate_text_table_strategy', + gen_strattable_mock) patched_configuration_load_config_file(mocker, default_conf) args = [ diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 5e68a5ef8..36c9a93b3 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -1,10 +1,14 @@ +from pathlib import Path + import pandas as pd +from arrow import Arrow from freqtrade.edge import PairInfo from freqtrade.optimize.optimize_reports import ( generate_edge_table, generate_text_table, generate_text_table_sell_reason, - generate_text_table_strategy) + generate_text_table_strategy, store_backtest_result) from freqtrade.strategy.interface import SellType +from tests.conftest import patch_exchange def test_generate_text_table(default_conf, mocker): @@ -113,3 +117,73 @@ def test_generate_edge_table(edge_conf, mocker): assert generate_edge_table(results).count('| ETH/BTC |') == 1 assert generate_edge_table(results).count( '| Risk Reward Ratio | Required Risk Reward | Expectancy |') == 1 + + +def test_backtest_record(default_conf, fee, mocker): + names = [] + records = [] + patch_exchange(mocker) + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch( + 'freqtrade.optimize.optimize_reports.file_dump_json', + new=lambda n, r: (names.append(n), records.append(r)) + ) + + results = {'DefStrat': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", + "UNITTEST/BTC", "UNITTEST/BTC"], + "profit_percent": [0.003312, 0.010801, 0.013803, 0.002780], + "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], + "open_time": [Arrow(2017, 11, 14, 19, 32, 00).datetime, + Arrow(2017, 11, 14, 21, 36, 00).datetime, + Arrow(2017, 11, 14, 22, 12, 00).datetime, + Arrow(2017, 11, 14, 22, 44, 00).datetime], + "close_time": [Arrow(2017, 11, 14, 21, 35, 00).datetime, + Arrow(2017, 11, 14, 22, 10, 00).datetime, + Arrow(2017, 11, 14, 22, 43, 00).datetime, + Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], + "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], + "open_index": [1, 119, 153, 185], + "close_index": [118, 151, 184, 199], + "trade_duration": [123, 34, 31, 14], + "open_at_end": [False, False, False, True], + "sell_reason": [SellType.ROI, SellType.STOP_LOSS, + SellType.ROI, SellType.FORCE_SELL] + })} + store_backtest_result(Path("backtest-result.json"), results) + # Assert file_dump_json was only called once + assert names == [Path('backtest-result.json')] + records = records[0] + # Ensure records are of correct type + assert len(records) == 4 + + # reset test to test with strategy name + names = [] + records = [] + results['Strat'] = pd.DataFrame() + store_backtest_result(Path("backtest-result.json"), results) + # Assert file_dump_json was only called once + assert names == [Path('backtest-result-DefStrat.json')] + records = records[0] + # Ensure records are of correct type + assert len(records) == 4 + + # ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117) + # Below follows just a typecheck of the schema/type of trade-records + oix = None + for (pair, profit, date_buy, date_sell, buy_index, dur, + openr, closer, open_at_end, sell_reason) in records: + assert pair == 'UNITTEST/BTC' + assert isinstance(profit, float) + # FIX: buy/sell should be converted to ints + assert isinstance(date_buy, float) + assert isinstance(date_sell, float) + assert isinstance(openr, float) + assert isinstance(closer, float) + assert isinstance(open_at_end, bool) + assert isinstance(sell_reason, str) + isinstance(buy_index, pd._libs.tslib.Timestamp) + if oix: + assert buy_index > oix + oix = buy_index + assert dur > 0 From e1b08ad76caf8482ee712d782bad482c643b0307 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Mar 2020 15:38:26 +0100 Subject: [PATCH 23/79] Add docstring to store_backtest_result --- freqtrade/optimize/optimize_reports.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 701432071..abfbaf1d8 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -12,7 +12,11 @@ logger = logging.getLogger(__name__) def store_backtest_result(recordfilename: Path, all_results: Dict[str, DataFrame]) -> None: - + """ + Stores backtest results to file (one file per strategy) + :param recordfilename: Destination filename + :param all_results: Dict of Dataframes, one results dataframe per strategy + """ for strategy, results in all_results.items(): records = [(t.pair, t.profit_percent, t.open_time.timestamp(), t.close_time.timestamp(), t.open_index - 1, t.trade_duration, From 3d4664c2a6da70027189d73a04b75bdd1821f79e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Mar 2020 15:40:12 +0100 Subject: [PATCH 24/79] Remove unnecessary import --- freqtrade/optimize/optimize_reports.py | 2 +- tests/optimize/test_optimize_reports.py | 40 ++++++++++++------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index abfbaf1d8..251da9159 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -1,7 +1,7 @@ import logging from datetime import timedelta from pathlib import Path -from typing import Dict, Optional +from typing import Dict from pandas import DataFrame from tabulate import tabulate diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 36c9a93b3..f19668459 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -130,26 +130,26 @@ def test_backtest_record(default_conf, fee, mocker): ) results = {'DefStrat': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", - "UNITTEST/BTC", "UNITTEST/BTC"], - "profit_percent": [0.003312, 0.010801, 0.013803, 0.002780], - "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], - "open_time": [Arrow(2017, 11, 14, 19, 32, 00).datetime, - Arrow(2017, 11, 14, 21, 36, 00).datetime, - Arrow(2017, 11, 14, 22, 12, 00).datetime, - Arrow(2017, 11, 14, 22, 44, 00).datetime], - "close_time": [Arrow(2017, 11, 14, 21, 35, 00).datetime, - Arrow(2017, 11, 14, 22, 10, 00).datetime, - Arrow(2017, 11, 14, 22, 43, 00).datetime, - Arrow(2017, 11, 14, 22, 58, 00).datetime], - "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], - "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], - "open_index": [1, 119, 153, 185], - "close_index": [118, 151, 184, 199], - "trade_duration": [123, 34, 31, 14], - "open_at_end": [False, False, False, True], - "sell_reason": [SellType.ROI, SellType.STOP_LOSS, - SellType.ROI, SellType.FORCE_SELL] - })} + "UNITTEST/BTC", "UNITTEST/BTC"], + "profit_percent": [0.003312, 0.010801, 0.013803, 0.002780], + "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], + "open_time": [Arrow(2017, 11, 14, 19, 32, 00).datetime, + Arrow(2017, 11, 14, 21, 36, 00).datetime, + Arrow(2017, 11, 14, 22, 12, 00).datetime, + Arrow(2017, 11, 14, 22, 44, 00).datetime], + "close_time": [Arrow(2017, 11, 14, 21, 35, 00).datetime, + Arrow(2017, 11, 14, 22, 10, 00).datetime, + Arrow(2017, 11, 14, 22, 43, 00).datetime, + Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], + "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], + "open_index": [1, 119, 153, 185], + "close_index": [118, 151, 184, 199], + "trade_duration": [123, 34, 31, 14], + "open_at_end": [False, False, False, True], + "sell_reason": [SellType.ROI, SellType.STOP_LOSS, + SellType.ROI, SellType.FORCE_SELL] + })} store_backtest_result(Path("backtest-result.json"), results) # Assert file_dump_json was only called once assert names == [Path('backtest-result.json')] From 8c33e07dc660853eee0eee0d10109063f52d11cf Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Sun, 15 Mar 2020 21:20:32 +0100 Subject: [PATCH 25/79] Update based on comments --- docs/plotting.md | 2 +- freqtrade/commands/arguments.py | 2 +- freqtrade/commands/cli_options.py | 6 +++--- freqtrade/data/btanalysis.py | 6 +++--- freqtrade/plot/plotting.py | 13 ++++++------- tests/data/test_btanalysis.py | 6 +++--- 6 files changed, 17 insertions(+), 18 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index ef28f0fe3..56906ebec 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -68,7 +68,7 @@ optional arguments: -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`). - --skip-trades Skip using trades file from backtesting and DB. + --no-trades Skip using trades from backtesting file and DB. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 66fa0b038..8c64c5857 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -59,7 +59,7 @@ ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchang ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", "export", "exportfilename", - "timerange", "ticker_interval", "skip_trades"] + "timerange", "ticker_interval", "no_trades"] ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source", "ticker_interval"] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 187e0a424..5cf1b7fce 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -413,9 +413,9 @@ AVAILABLE_CLI_OPTIONS = { metavar='INT', default=750, ), - "skip_trades": Arg( - '--skip-trades', - help='Skip using trades file from backtesting and DB.', + "no_trades": Arg( + '--no-trades', + help='Skip using trades from backtesting file and DB.', action='store_true', ), "trade_source": Arg( diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 8670f30c6..17b243f56 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -129,7 +129,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: return trades -def load_trades(source: str, db_url: str, exportfilename: Path, skip_trades: bool) -> pd.DataFrame: +def load_trades(source: str, db_url: str, exportfilename: Path, no_trades: bool) -> pd.DataFrame: """ Based on configuration option "trade_source": * loads data from DB (using `db_url`) @@ -137,10 +137,10 @@ def load_trades(source: str, db_url: str, exportfilename: Path, skip_trades: boo :param source: "DB" or "file" - specify source to load from :param db_url: sqlalchemy formatted url to a database :param exportfilename: Json file generated by backtesting - :param skip_trades: Skip using trades, only return backtesting data columns + :param no_trades: Skip using trades, only return backtesting data columns :return: DataFrame containing trades """ - if skip_trades: + if no_trades: df = pd.DataFrame(columns=BT_DATA_COLUMNS) return df diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 8da61597f..fc8f25612 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -3,7 +3,6 @@ from pathlib import Path from typing import Any, Dict, List import pandas as pd -from os.path import isfile from freqtrade.configuration import TimeRange from freqtrade.data.btanalysis import (calculate_max_drawdown, @@ -49,18 +48,18 @@ def init_plotscript(config): data_format=config.get('dataformat_ohlcv', 'json'), ) - skip_trades = False - if not isfile(config.get('exportfilename')) and config['trade_source'] == 'file': + no_trades = False + if config.get('no_trades', False): + no_trades = True + elif not config['exportfilename'].is_file() and config['trade_source'] == 'file': logger.warning("Backtest file is missing skipping trades.") - skip_trades = True - elif config.get('skip_trades', False): - skip_trades = True + no_trades = True trades = load_trades( config['trade_source'], db_url=config.get('db_url'), exportfilename=config.get('exportfilename'), - skip_trades=skip_trades + no_trades=no_trades ) trades = trim_dataframe(trades, timerange, 'open_time') diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 8561f2b84..4b47faeee 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -105,7 +105,7 @@ def test_load_trades(default_conf, mocker): load_trades("DB", db_url=default_conf.get('db_url'), exportfilename=default_conf.get('exportfilename'), - skip_trades=False + no_trades=False ) assert db_mock.call_count == 1 @@ -117,7 +117,7 @@ def test_load_trades(default_conf, mocker): load_trades("file", db_url=default_conf.get('db_url'), exportfilename=default_conf.get('exportfilename'), - skip_trades=False + no_trades=False ) assert db_mock.call_count == 0 @@ -129,7 +129,7 @@ def test_load_trades(default_conf, mocker): load_trades("file", db_url=default_conf.get('db_url'), exportfilename=default_conf.get('exportfilename'), - skip_trades=True + no_trades=True ) assert db_mock.call_count == 0 From 06198c00283eeaef44f0dda391a3cc769acce6ae Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Sun, 15 Mar 2020 21:27:45 +0100 Subject: [PATCH 26/79] Missed configuration.py --- freqtrade/configuration/configuration.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index ce2101441..e5515670d 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -359,6 +359,9 @@ class Configuration: self._args_to_config(config, argname='erase', logstring='Erase detected. Deleting existing data.') + self._args_to_config(config, argname='no_trades', + logstring='Parameter --no-trades detected.') + self._args_to_config(config, argname='timeframes', logstring='timeframes --timeframes: {}') From e8a92cb3136971bbcd96257e4dc036e7b701da26 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2020 08:17:21 +0000 Subject: [PATCH 27/79] Bump pandas from 1.0.1 to 1.0.2 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.0.1 to 1.0.2. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.0.1...v1.0.2) Signed-off-by: dependabot-preview[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 68024f587..12d7336aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ -r requirements-common.txt numpy==1.18.1 -pandas==1.0.1 +pandas==1.0.2 From 0b341757525c23f1242e7e9b189d97b76f5b54b2 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2020 08:17:53 +0000 Subject: [PATCH 28/79] Bump ccxt from 1.23.81 to 1.24.31 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.23.81 to 1.24.31. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.23.81...1.24.31) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 784eef93c..f3f7d8a07 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.23.81 +ccxt==1.24.31 SQLAlchemy==1.3.13 python-telegram-bot==12.4.2 arrow==0.15.5 From 8cfff40fcfb2ba7145c12b165f75e34debcedae6 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2020 08:18:07 +0000 Subject: [PATCH 29/79] Bump plotly from 4.5.3 to 4.5.4 Bumps [plotly](https://github.com/plotly/plotly.py) from 4.5.3 to 4.5.4. - [Release notes](https://github.com/plotly/plotly.py/releases) - [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.py/compare/v4.5.3...v4.5.4) Signed-off-by: dependabot-preview[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index 381334a66..7a5b21e2d 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.5.3 +plotly==4.5.4 From 3eee0c43a727fe91bad3481e1dab0551793afa4e Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2020 08:18:33 +0000 Subject: [PATCH 30/79] Bump pytest from 5.3.5 to 5.4.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.3.5 to 5.4.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/5.3.5...5.4.1) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1e58ae6e0..f2801c15b 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==4.0.0 mypy==0.761 -pytest==5.3.5 +pytest==5.4.1 pytest-asyncio==0.10.0 pytest-cov==2.8.1 pytest-mock==2.0.0 From 7a7530d57d4ef7d39ff715ccee7cf9ddf2a820a0 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2020 08:53:20 +0000 Subject: [PATCH 31/79] Bump sqlalchemy from 1.3.13 to 1.3.15 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.13 to 1.3.15. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index f3f7d8a07..7c2ad4283 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,7 +1,7 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs ccxt==1.24.31 -SQLAlchemy==1.3.13 +SQLAlchemy==1.3.15 python-telegram-bot==12.4.2 arrow==0.15.5 cachetools==4.0.0 From 62d449251cc8a02182a46015bc2a36e71d549c85 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2020 08:54:12 +0000 Subject: [PATCH 32/79] Bump mypy from 0.761 to 0.770 Bumps [mypy](https://github.com/python/mypy) from 0.761 to 0.770. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.761...v0.770) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index f2801c15b..a4d83eb4f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ coveralls==1.11.1 flake8==3.7.9 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.0.0 -mypy==0.761 +mypy==0.770 pytest==5.4.1 pytest-asyncio==0.10.0 pytest-cov==2.8.1 From 4f46fb9bf5cb0ae22e3b365e552f62da712bd99f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Mar 2020 19:33:18 +0100 Subject: [PATCH 33/79] Add template and jupyter files to release --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 7529152a0..c67f5258f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,4 @@ include LICENSE include README.md include config.json.example recursive-include freqtrade *.py +recursive-include freqtrade/templates/ *.j2 *.ipynb From 05250ba6611278b794677780a2e00febdb14f476 Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Wed, 18 Mar 2020 11:00:33 +0100 Subject: [PATCH 34/79] Update docs/plotting.md Co-Authored-By: Matthias --- docs/plotting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plotting.md b/docs/plotting.md index 56906ebec..be83065a6 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -32,7 +32,7 @@ usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH] [--trade-source {DB,file}] [--export EXPORT] [--export-filename PATH] [--timerange TIMERANGE] [-i TICKER_INTERVAL] - [--skip-trades] + [--no-trades] optional arguments: -h, --help show this help message and exit From 0920d6fce4d35ebd3f99ed321131764fde628206 Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Wed, 18 Mar 2020 11:01:09 +0100 Subject: [PATCH 35/79] Update freqtrade/data/btanalysis.py Co-Authored-By: Matthias --- freqtrade/data/btanalysis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 17b243f56..898072748 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -129,7 +129,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: return trades -def load_trades(source: str, db_url: str, exportfilename: Path, no_trades: bool) -> pd.DataFrame: +def load_trades(source: str, db_url: str, exportfilename: Path, no_trades: bool = False) -> pd.DataFrame: """ Based on configuration option "trade_source": * loads data from DB (using `db_url`) From 3e1bef888ab1344f8763e130cdef835b0b030d6b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Mar 2020 11:42:42 +0100 Subject: [PATCH 36/79] Fix flake8 error --- freqtrade/data/btanalysis.py | 3 ++- tests/data/test_btanalysis.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 898072748..23a9f720c 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -129,7 +129,8 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: return trades -def load_trades(source: str, db_url: str, exportfilename: Path, no_trades: bool = False) -> pd.DataFrame: +def load_trades(source: str, db_url: str, exportfilename: Path, + no_trades: bool = False) -> pd.DataFrame: """ Based on configuration option "trade_source": * loads data from DB (using `db_url`) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 4b47faeee..463e5ae36 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -117,7 +117,6 @@ def test_load_trades(default_conf, mocker): load_trades("file", db_url=default_conf.get('db_url'), exportfilename=default_conf.get('exportfilename'), - no_trades=False ) assert db_mock.call_count == 0 From ecf3a3e07076e3101e804e5216ec38ff236a1686 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 19 Mar 2020 19:42:51 +0100 Subject: [PATCH 37/79] Add test validating different return values --- tests/test_freqtradebot.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 9f6e5ef0c..e37270bd3 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2228,10 +2228,16 @@ def test_handle_timedout_limit_buy(mocker, default_conf, limit_buy_order) -> Non assert cancel_order_mock.call_count == 1 -def test_handle_timedout_limit_buy_corder_empty(mocker, default_conf, limit_buy_order) -> None: +@pytest.mark.parametrize('cancelorder', [ + {}, + 'String Return value', + 123 +]) +def test_handle_timedout_limit_buy_corder_empty(mocker, default_conf, limit_buy_order, + cancelorder) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - cancel_order_mock = MagicMock(return_value={}) + cancel_order_mock = MagicMock(return_value=cancelorder) mocker.patch.multiple( 'freqtrade.exchange.Exchange', cancel_order=cancel_order_mock From 5e702f6891c76a066aae85833026780cc01ebee5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 19 Mar 2020 19:43:19 +0100 Subject: [PATCH 38/79] Verify cancel_order returnvalue is a dictionary --- freqtrade/freqtradebot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9897b39b4..bc3078a5c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -891,6 +891,9 @@ class FreqtradeBot: if order['status'] != 'canceled': reason = "cancelled due to timeout" corder = self.exchange.cancel_order(trade.open_order_id, trade.pair) + # Some exchanges don't return a dict here. + if not isinstance(corder, dict): + corder = {} logger.info('Buy order %s for %s.', reason, trade) else: # Order was cancelled already, so we can reuse the existing dict From 5f9479b39fe4c31ac0171501c28186ef0b1065b9 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 20 Mar 2020 02:10:44 +0300 Subject: [PATCH 39/79] Edge import cosmetics --- freqtrade/edge/edge_positioning.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index d196ab4b3..5305e23cf 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -8,10 +8,10 @@ import numpy as np import utils_find_1st as utf1st from pandas import DataFrame -from freqtrade import constants from freqtrade.configuration import TimeRange -from freqtrade.data import history +from freqtrade.constants import UNLIMITED_STAKE_AMOUNT from freqtrade.exceptions import OperationalException +from freqtrade.data.history import get_timerange, load_data, refresh_data from freqtrade.strategy.interface import SellType logger = logging.getLogger(__name__) @@ -54,7 +54,7 @@ class Edge: if self.config['max_open_trades'] != float('inf'): logger.critical('max_open_trades should be -1 in config !') - if self.config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT: + if self.config['stake_amount'] != UNLIMITED_STAKE_AMOUNT: raise OperationalException('Edge works only with unlimited stake amount') # Deprecated capital_available_percentage. Will use tradable_balance_ratio in the future. @@ -96,7 +96,7 @@ class Edge: logger.info('Using local backtesting data (using whitelist in given config) ...') if self._refresh_pairs: - history.refresh_data( + refresh_data( datadir=self.config['datadir'], pairs=pairs, exchange=self.exchange, @@ -104,7 +104,7 @@ class Edge: timerange=self._timerange, ) - data = history.load_data( + data = load_data( datadir=self.config['datadir'], pairs=pairs, timeframe=self.strategy.ticker_interval, @@ -122,7 +122,7 @@ class Edge: preprocessed = self.strategy.ohlcvdata_to_dataframe(data) # Print timeframe - min_date, max_date = history.get_timerange(preprocessed) + min_date, max_date = get_timerange(preprocessed) logger.info( 'Measuring data from %s up to %s (%s days) ...', min_date.isoformat(), From 3e0ffdce75aa6795caa830bfeb4c614a1750c207 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 20 Mar 2020 04:21:17 +0300 Subject: [PATCH 40/79] Adjust tests --- tests/edge/test_edge.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index 3bebeee65..2304c53c2 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -292,8 +292,8 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m', 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) + mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock()) + mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) assert edge.calculate() @@ -304,8 +304,8 @@ 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={})) + mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock()) + mocker.patch('freqtrade.edge.edge_positioning.load_data', MagicMock(return_value={})) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) assert not edge.calculate() @@ -317,8 +317,8 @@ 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) + mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock()) + mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data) # Return empty mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', MagicMock(return_value=[])) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) From 942792f1233f35eba31d51d96ecbc92163376af1 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Fri, 20 Mar 2020 05:48:53 +0100 Subject: [PATCH 41/79] updated as suggested --- freqtrade/freqtradebot.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e5ae9043a..378252c29 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -394,21 +394,23 @@ class FreqtradeBot: logger.info(f"Pair {pair} is currently locked.") return False + # get_free_open_trades is checked before create_trade is called + # but it is still used here to prevent opening too many trades within one iteration if not self.get_free_open_trades(): logger.debug(f"Can't open a new trade for {pair}: max number of trades is reached.") return False - stake_amount = self.get_trade_stake_amount(pair) - if not stake_amount: - logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") - return False - # running get_signal on historical data fetched (buy, sell) = self.strategy.get_signal( pair, self.strategy.ticker_interval, self.dataprovider.ohlcv(pair, self.strategy.ticker_interval)) if buy and not sell: + stake_amount = self.get_trade_stake_amount(pair) + if not stake_amount: + logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") + return False + logger.info(f"Buy signal found: about create a new trade with stake_amount: " f"{stake_amount} ...") From e30faf8c8c371de43b0e1c35b5eae89489e01ab9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Mar 2020 20:04:05 +0100 Subject: [PATCH 42/79] Remove partial candle documentation It wasn't working 100% correctly - see #2993 --- docs/exchanges.md | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 66a0e96da..c600077ce 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -74,23 +74,13 @@ Should you experience constant errors with Nonce (like `InvalidNonce`), it is be $ pip3 install web3 ``` -### Send incomplete candles to the strategy +### Getting latest price / Incomplete candles Most exchanges return current incomplete candle via their OHLCV/klines API interface. By default, Freqtrade assumes that incomplete candle is fetched from the exchange and removes the last candle assuming it's the 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. +Due to the danger of repainting, Freqtrade does not allow you to use this incomplete candle. -``` 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. +However, usually, this requirement is based on the need for the latest price - which can be aquired using the [data provider](strategy-customization.md#possible-options-for-dataprovider) from within the strategy. From 2c434e9b116fdfb71c2da4f57db4e25683ce7d39 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Mar 2020 11:16:09 +0100 Subject: [PATCH 43/79] Add close_proit_abs column --- freqtrade/freqtradebot.py | 1 + freqtrade/persistence.py | 11 ++++++++--- tests/test_persistence.py | 20 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index db632693a..570f8bea8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -954,6 +954,7 @@ class FreqtradeBot: trade.close_rate = None trade.close_profit = None + trade.close_profit_abs = None trade.close_date = None trade.is_open = True trade.open_order_id = None diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index ac084d12e..0d668596c 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, 'open_trade_price'): + if not has_column(cols, 'close_profit_abs'): logger.info(f'Running database migration - backup available as {table_back_name}') fee_open = get_column_def(cols, 'fee_open', 'fee') @@ -106,6 +106,9 @@ def check_migrate(engine) -> None: 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})') + close_profit_abs = get_column_def( + cols, 'close_profit_abs', + f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}") # Schema migration necessary engine.execute(f"alter table trades rename to {table_back_name}") @@ -123,7 +126,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, open_trade_price + ticker_interval, open_trade_price, close_profit_abs ) select id, lower(exchange), case @@ -143,7 +146,7 @@ def check_migrate(engine) -> None: {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, - {open_trade_price} open_trade_price + {open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs from {table_back_name} """) @@ -190,6 +193,7 @@ class Trade(_DECL_BASE): close_rate = Column(Float) close_rate_requested = Column(Float) close_profit = Column(Float) + close_profit_abs = Column(Float) stake_amount = Column(Float, nullable=False) amount = Column(Float) open_date = Column(DateTime, nullable=False, default=datetime.utcnow) @@ -334,6 +338,7 @@ class Trade(_DECL_BASE): """ self.close_rate = Decimal(rate) self.close_profit = self.calc_profit_ratio() + self.close_profit_abs = self.calc_profit() self.close_date = datetime.utcnow() self.is_open = False self.open_order_id = None diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 6bd7971a7..991922cba 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -476,12 +476,22 @@ def test_migrate_old(mocker, default_conf, fee): stake=default_conf.get("stake_amount"), amount=amount ) + insert_table_old2 = """INSERT INTO trades (exchange, pair, is_open, fee, + open_rate, close_rate, stake_amount, amount, open_date) + VALUES ('BITTREX', 'BTC_ETC', 0, {fee}, + 0.00258580, 0.00268580, {stake}, {amount}, + '2017-11-28 12:44:24.000000') + """.format(fee=fee.return_value, + stake=default_conf.get("stake_amount"), + amount=amount + ) engine = create_engine('sqlite://') mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine) # Create table using the old format engine.execute(create_table_old) engine.execute(insert_table_old) + engine.execute(insert_table_old2) # Run init to test migration init(default_conf['db_url'], default_conf['dry_run']) @@ -500,6 +510,15 @@ def test_migrate_old(mocker, default_conf, fee): assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 assert trade.open_trade_price == trade._calc_open_trade_price() + assert trade.close_profit_abs is None + + trade = Trade.query.filter(Trade.id == 2).first() + assert trade.close_rate is not None + assert trade.is_open == 0 + assert trade.open_rate_requested is None + assert trade.close_rate_requested is None + assert trade.close_rate is not None + assert pytest.approx(trade.close_profit_abs) == trade.calc_profit() def test_migrate_new(mocker, default_conf, fee, caplog): @@ -583,6 +602,7 @@ def test_migrate_new(mocker, default_conf, fee, 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() + assert trade.close_profit_abs is None def test_migrate_mid_state(mocker, default_conf, fee, caplog): From efd94c84debab78c00bc141b6108f43b366c2e07 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Mar 2020 11:22:49 +0100 Subject: [PATCH 44/79] Add example notebook to gitignore again --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9ac2c9d5d..f206fce66 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ user_data/* !user_data/strategy/sample_strategy.py !user_data/notebooks user_data/notebooks/* -!user_data/notebooks/*example.ipynb freqtrade-plot.html freqtrade-profit-plot.html From f14c496ce9ffca5186d4479a611c37c76c0a35ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Mar 2020 11:28:18 +0100 Subject: [PATCH 45/79] Remove calc_close_profit from RPC This is now possible - but only for closed trades, so certain occurances need to remain. --- freqtrade/rpc/rpc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 9014c1874..a0f50b070 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -197,7 +197,7 @@ class RPC: Trade.close_date >= profitday, Trade.close_date < (profitday + timedelta(days=1)) ]).order_by(Trade.close_date).all() - curdayprofit = sum(trade.calc_profit() for trade in trades) + curdayprofit = sum(trade.close_profit_abs for trade in trades) profit_days[profitday] = { 'amount': f'{curdayprofit:.8f}', 'trades': len(trades) @@ -246,8 +246,8 @@ class RPC: durations.append((trade.close_date - trade.open_date).total_seconds()) if not trade.is_open: - profit_ratio = trade.calc_profit_ratio() - profit_closed_coin.append(trade.calc_profit()) + profit_ratio = trade.close_profit + profit_closed_coin.append(trade.close_profit_abs) profit_closed_ratio.append(profit_ratio) else: # Get current rate From acd402187a179e1f040fc42fe1dba22b2519a2e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Mar 2020 19:39:36 +0100 Subject: [PATCH 46/79] Update max_open_trades documentation --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index b0f4c7554..78c643868 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -40,7 +40,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | Parameter | Description | |------------|-------------| -| `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades). [More information below](#configuring-amount-per-trade).
**Datatype:** Positive integer or -1. +| `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the lenght of your whitelist another limitation which can apply. If -1 then it is ignored (i.e. potentially unlimited open trades). [More information below](#configuring-amount-per-trade).
**Datatype:** Positive integer or -1. | `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** String | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Positive float or `"unlimited"`. | `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade).
*Defaults to `0.99` 99%).*
**Datatype:** Positive float between `0.1` and `1.0`. From 45aaa8c09d2a704e49b56ef47b360425f305b7b5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Mar 2020 20:09:01 +0100 Subject: [PATCH 47/79] Parse and show relevant configuration section --- freqtrade/configuration/load_config.py | 28 ++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py index 19179c6c3..0d2dc0955 100644 --- a/freqtrade/configuration/load_config.py +++ b/freqtrade/configuration/load_config.py @@ -1,13 +1,15 @@ """ This module contain functions to load the configuration file """ -import rapidjson import logging +import re import sys +from pathlib import Path from typing import Any, Dict -from freqtrade.exceptions import OperationalException +import rapidjson +from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) @@ -15,6 +17,22 @@ logger = logging.getLogger(__name__) CONFIG_PARSE_MODE = rapidjson.PM_COMMENTS | rapidjson.PM_TRAILING_COMMAS +def log_config_error_range(path: str, errmsg: str) -> str: + """ + Parses configuration file and prints range around error + """ + if path != '-': + offsetlist = re.findall(r'(?<=Parse\serror\sat\soffset\s)\d+', errmsg) + if offsetlist: + offset = int(offsetlist[0]) + text = Path(path).read_text() + # Fetch an offset of 80 characters around the error line + subtext = text[offset-min(80, offset):offset+80] + segments = subtext.split('\n') + # Remove first and last lines, to avoid odd truncations + return '\n'.join(segments[1:-1]) + + def load_config_file(path: str) -> Dict[str, Any]: """ Loads a config file from the given path @@ -29,5 +47,11 @@ def load_config_file(path: str) -> Dict[str, Any]: raise OperationalException( f'Config file "{path}" not found!' ' Please create a config file or check whether it exists.') + except rapidjson.JSONDecodeError as e: + err_range = log_config_error_range(path, str(e)) + raise OperationalException( + f'{e}\n' + f'Please verify the following segment of your configuration:\n{err_range}' + ) return config From 6c55b40fe04f4cea4b92841212b150d69f5d200a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Mar 2020 20:15:33 +0100 Subject: [PATCH 48/79] Add test verifying config printing --- tests/test_configuration.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 1e9d6440d..ec6753feb 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -66,6 +66,17 @@ def test_load_config_file(default_conf, mocker, caplog) -> None: assert validated_conf.items() >= default_conf.items() +def test_load_config_file_error(default_conf, mocker, caplog) -> None: + del default_conf['user_data_dir'] + filedata = json.dumps(default_conf).replace( + '"stake_amount": 0.001,', '"stake_amount": .001,') + mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open(read_data=filedata)) + mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata)) + + with pytest.raises(OperationalException, match=f".*Please verify the following segment.*"): + load_config_file('somefile') + + def test__args_to_config(caplog): arg_list = ['trade', '--strategy-path', 'TestTest'] From 8f7e113d798bb8b973efd6a66b59d1abf18722f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Mar 2020 07:54:27 +0100 Subject: [PATCH 49/79] Add additional test --- freqtrade/configuration/load_config.py | 7 +++++-- tests/test_configuration.py | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py index 0d2dc0955..55da28913 100644 --- a/freqtrade/configuration/load_config.py +++ b/freqtrade/configuration/load_config.py @@ -29,8 +29,11 @@ def log_config_error_range(path: str, errmsg: str) -> str: # Fetch an offset of 80 characters around the error line subtext = text[offset-min(80, offset):offset+80] segments = subtext.split('\n') - # Remove first and last lines, to avoid odd truncations - return '\n'.join(segments[1:-1]) + if len(segments) > 3: + # Remove first and last lines, to avoid odd truncations + return '\n'.join(segments[1:-1]) + else: + return subtext def load_config_file(path: str) -> Dict[str, Any]: diff --git a/tests/test_configuration.py b/tests/test_configuration.py index ec6753feb..a1936aee0 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -18,7 +18,7 @@ from freqtrade.configuration.config_validation import validate_config_schema from freqtrade.configuration.deprecated_settings import ( check_conflicting_settings, process_deprecated_setting, process_temporary_deprecated_settings) -from freqtrade.configuration.load_config import load_config_file +from freqtrade.configuration.load_config import load_config_file, log_config_error_range from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.exceptions import OperationalException from freqtrade.loggers import _set_loggers, setup_logging @@ -77,6 +77,19 @@ def test_load_config_file_error(default_conf, mocker, caplog) -> None: load_config_file('somefile') +def test_load_config_file_error_range(default_conf, mocker, caplog) -> None: + del default_conf['user_data_dir'] + filedata = json.dumps(default_conf).replace( + '"stake_amount": 0.001,', '"stake_amount": .001,') + mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata)) + + x = log_config_error_range('somefile', 'Parse error at offset 64: Invalid value.') + assert isinstance(x, str) + assert (x == '{"max_open_trades": 1, "stake_currency": "BTC", ' + '"stake_amount": .001, "fiat_display_currency": "USD", ' + '"ticker_interval": "5m", "dry_run": true, ') + + def test__args_to_config(caplog): arg_list = ['trade', '--strategy-path', 'TestTest'] From d581b7e2d79493ddccb6cbd2cbe41b6baa028b38 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Mar 2020 07:57:30 +0100 Subject: [PATCH 50/79] Add fallback if no error could be determined --- freqtrade/configuration/load_config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py index 55da28913..a24ee3d0a 100644 --- a/freqtrade/configuration/load_config.py +++ b/freqtrade/configuration/load_config.py @@ -34,6 +34,7 @@ def log_config_error_range(path: str, errmsg: str) -> str: return '\n'.join(segments[1:-1]) else: return subtext + return '' def load_config_file(path: str) -> Dict[str, Any]: @@ -55,6 +56,7 @@ def load_config_file(path: str) -> Dict[str, Any]: raise OperationalException( f'{e}\n' f'Please verify the following segment of your configuration:\n{err_range}' + if err_range else 'Please verify your configuration file for syntax errors.' ) return config From f4a69ba5a753c38f9b18ec81e596d38c86ce66de Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2020 09:04:50 +0000 Subject: [PATCH 51/79] Bump numpy from 1.18.1 to 1.18.2 Bumps [numpy](https://github.com/numpy/numpy) from 1.18.1 to 1.18.2. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/master/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.18.1...v1.18.2) Signed-off-by: dependabot-preview[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 12d7336aa..e69d919e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Load common requirements -r requirements-common.txt -numpy==1.18.1 +numpy==1.18.2 pandas==1.0.2 From de91e169bc6f3316f5d3a5ad52add147ec71eba6 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2020 09:05:12 +0000 Subject: [PATCH 52/79] Bump tabulate from 0.8.6 to 0.8.7 Bumps [tabulate](https://github.com/astanin/python-tabulate) from 0.8.6 to 0.8.7. - [Release notes](https://github.com/astanin/python-tabulate/releases) - [Changelog](https://github.com/astanin/python-tabulate/blob/master/CHANGELOG) - [Commits](https://github.com/astanin/python-tabulate/compare/v0.8.6...v0.8.7) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 7c2ad4283..c1e23dfa9 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -10,7 +10,7 @@ urllib3==1.25.8 wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.17 -tabulate==0.8.6 +tabulate==0.8.7 pycoingecko==1.2.0 jinja2==2.11.1 From 98dc4b4ca3893452e80db2f58dd9d72baaeb5f26 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2020 09:06:41 +0000 Subject: [PATCH 53/79] Bump ccxt from 1.24.31 to 1.24.83 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.24.31 to 1.24.83. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.24.31...1.24.83) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 7c2ad4283..6274297c2 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.24.31 +ccxt==1.24.83 SQLAlchemy==1.3.15 python-telegram-bot==12.4.2 arrow==0.15.5 From cb1bc5d5abb1b125be1282c7538f9e169d17d3b8 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2020 09:15:43 +0000 Subject: [PATCH 54/79] Bump pandas from 1.0.2 to 1.0.3 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.0.2 to 1.0.3. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.0.2...v1.0.3) Signed-off-by: dependabot-preview[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e69d919e0..b1a4b4403 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ -r requirements-common.txt numpy==1.18.2 -pandas==1.0.2 +pandas==1.0.3 From 0f53e646fd7d3e707f470244d785539211a17793 Mon Sep 17 00:00:00 2001 From: orehunt Date: Tue, 24 Mar 2020 13:54:46 +0100 Subject: [PATCH 55/79] check that the strategy dataframe matches the one given by the bot --- freqtrade/strategy/interface.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 696d2b2d2..530cd0af4 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -241,8 +241,18 @@ class IStrategy(ABC): return dataframe - def get_signal(self, pair: str, interval: str, - dataframe: DataFrame) -> Tuple[bool, bool]: + @staticmethod + def preserve_df(d: DataFrame) -> Tuple[int, float, datetime]: + """ keep some data for dataframes """ + return len(d), d["close"].iloc[-1], d["date"].iloc[-1] + + @staticmethod + def assert_df(d: DataFrame, df_len: int, df_close: float, df_date: datetime): + """ make sure data is unmodified """ + if df_len != len(d) or df_close != d["close"].iloc[-1] or df_date != d["date"].iloc[-1]: + raise Exception("Dataframe returned from strategy does not match original") + + def get_signal(self, pair: str, interval: str, dataframe: DataFrame) -> Tuple[bool, bool]: """ Calculates current signal based several technical analysis indicators :param pair: pair in format ANT/BTC @@ -254,8 +264,11 @@ class IStrategy(ABC): logger.warning('Empty candle (OHLCV) data for pair %s', pair) return False, False + latest_date = dataframe['date'].max() try: + df_len, df_close, df_date = self.preserve_df(dataframe) dataframe = self._analyze_ticker_internal(dataframe, {'pair': pair}) + self.assert_df(dataframe, df_len, df_close, df_date) except ValueError as error: logger.warning( 'Unable to analyze candle (OHLCV) data for pair %s: %s', @@ -275,7 +288,7 @@ class IStrategy(ABC): logger.warning('Empty dataframe for pair %s', pair) return False, False - latest = dataframe.iloc[-1] + latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1] # Check if dataframe is out of date signal_date = arrow.get(latest['date']) From be41981ef08f65d2d377d7a225456fbdc6221b83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Mar 2020 20:10:15 +0100 Subject: [PATCH 56/79] Test warnings with filter always on --- tests/test_configuration.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 1e9d6440d..79387ba7a 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -73,6 +73,7 @@ def test__args_to_config(caplog): configuration = Configuration(args) config = {} with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") # No warnings ... configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef") assert len(w) == 0 @@ -82,6 +83,7 @@ def test__args_to_config(caplog): configuration = Configuration(args) config = {} with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") # Deprecation warnings! configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef", deprecated_msg="Going away soon!") From d9a5e1cd48955041a6e41e1e6138be75b15c46f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Mar 2020 08:30:18 +0100 Subject: [PATCH 57/79] Update docs/exchanges.md Co-Authored-By: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- docs/exchanges.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index c600077ce..06db26f89 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -83,4 +83,4 @@ Whether your exchange returns incomplete candles or not can be checked using [th Due to the danger of repainting, Freqtrade does not allow you to use this incomplete candle. -However, usually, this requirement is based on the need for the latest price - which can be aquired using the [data provider](strategy-customization.md#possible-options-for-dataprovider) from within the strategy. +However, if it is based on the need for the latest price for your strategy - then this requirement can be acquired using the [data provider](strategy-customization.md#possible-options-for-dataprovider) from within the strategy. From dfac7448d1260b81653e600fda27dc57d41af0ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Mar 2020 08:33:22 +0100 Subject: [PATCH 58/79] fix typo Co-Authored-By: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 78c643868..9ecb0a13e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -40,7 +40,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | Parameter | Description | |------------|-------------| -| `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the lenght of your whitelist another limitation which can apply. If -1 then it is ignored (i.e. potentially unlimited open trades). [More information below](#configuring-amount-per-trade).
**Datatype:** Positive integer or -1. +| `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your whitelist another limitation which can apply. If -1 then it is ignored (i.e. potentially unlimited open trades). [More information below](#configuring-amount-per-trade).
**Datatype:** Positive integer or -1. | `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** String | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Positive float or `"unlimited"`. | `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade).
*Defaults to `0.99` 99%).*
**Datatype:** Positive float between `0.1` and `1.0`. From 6f687c97ce1743e43dc8b4a1bcb166cf7be4a523 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Mar 2020 09:24:37 +0100 Subject: [PATCH 59/79] Update docs/configuration.md Co-Authored-By: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 9ecb0a13e..d8a9653c3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -40,7 +40,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | Parameter | Description | |------------|-------------| -| `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your whitelist another limitation which can apply. If -1 then it is ignored (i.e. potentially unlimited open trades). [More information below](#configuring-amount-per-trade).
**Datatype:** Positive integer or -1. +| `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation which can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade).
**Datatype:** Positive integer or -1. | `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** String | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Positive float or `"unlimited"`. | `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade).
*Defaults to `0.99` 99%).*
**Datatype:** Positive float between `0.1` and `1.0`. From 3ef568029f48a55dc22b64d4ea904da75d50b2d3 Mon Sep 17 00:00:00 2001 From: orehunt Date: Thu, 26 Mar 2020 07:05:30 +0100 Subject: [PATCH 60/79] different exception messages --- freqtrade/strategy/interface.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 530cd0af4..4f833be23 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -16,6 +16,7 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.exchange import timeframe_to_minutes from freqtrade.persistence import Trade from freqtrade.wallets import Wallets +from freqtrade.exceptions import DependencyException logger = logging.getLogger(__name__) @@ -242,15 +243,23 @@ class IStrategy(ABC): return dataframe @staticmethod - def preserve_df(d: DataFrame) -> Tuple[int, float, datetime]: + def preserve_df(dataframe: DataFrame) -> Tuple[int, float, datetime]: """ keep some data for dataframes """ - return len(d), d["close"].iloc[-1], d["date"].iloc[-1] + return len(dataframe), dataframe["close"].iloc[-1], dataframe["date"].iloc[-1] @staticmethod - def assert_df(d: DataFrame, df_len: int, df_close: float, df_date: datetime): + def assert_df(dataframe: DataFrame, df_len: int, df_close: float, df_date: datetime): """ make sure data is unmodified """ - if df_len != len(d) or df_close != d["close"].iloc[-1] or df_date != d["date"].iloc[-1]: - raise Exception("Dataframe returned from strategy does not match original") + message = "" + if df_len != len(dataframe): + message = "length" + elif df_close != dataframe["close"].iloc[-1]: + message = "last close price" + elif df_date != dataframe["date"].iloc[-1]: + message = "last date" + if message: + raise DependencyException("Dataframe returned from strategy has mismatching " + f"{message}.") def get_signal(self, pair: str, interval: str, dataframe: DataFrame) -> Tuple[bool, bool]: """ From 78aa65825550726142ca154b9b0c4427fac283c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Mar 2020 11:27:40 +0200 Subject: [PATCH 61/79] Remove unnecessary test (it's a copy of the remaining test) --- tests/strategy/test_interface.py | 38 ++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 949dda4a0..4e8d8f708 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -17,33 +17,36 @@ from tests.conftest import get_patched_exchange, log_has _STRATEGY = DefaultStrategy(config={}) -def test_returns_latest_buy_signal(mocker, default_conf, ohlcv_history): - mocker.patch.object( - _STRATEGY, '_analyze_ticker_internal', - return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}]) - ) - assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (True, False) +def test_returns_latest_signal(mocker, default_conf, ohlcv_history): + ohlcv_history.loc[1, 'date'] = arrow.utcnow() + # Take a copy to correctly modify the call + mocked_history = ohlcv_history.copy() + mocked_history['sell'] = 0 + mocked_history['buy'] = 0 + mocked_history.loc[1, 'sell'] = 1 mocker.patch.object( _STRATEGY, '_analyze_ticker_internal', - return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}]) - ) - assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, True) - - -def test_returns_latest_sell_signal(mocker, default_conf, ohlcv_history): - mocker.patch.object( - _STRATEGY, '_analyze_ticker_internal', - return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}]) + return_value=mocked_history ) assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, True) + mocked_history.loc[1, 'sell'] = 0 + mocked_history.loc[1, 'buy'] = 1 mocker.patch.object( _STRATEGY, '_analyze_ticker_internal', - return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}]) + return_value=mocked_history ) assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (True, False) + mocked_history.loc[1, 'sell'] = 0 + mocked_history.loc[1, 'buy'] = 0 + + mocker.patch.object( + _STRATEGY, '_analyze_ticker_internal', + return_value=mocked_history + ) + assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, False) def test_get_signal_empty(default_conf, mocker, caplog): @@ -74,6 +77,8 @@ def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ohlcv_history) _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame([]) ) + mocker.patch.object(_STRATEGY, 'assert_df') + assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], ohlcv_history) assert log_has('Empty dataframe for pair xyz', caplog) @@ -89,6 +94,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame(ticks) ) + mocker.patch.object(_STRATEGY, 'assert_df') assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], ohlcv_history) assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) From 0887a0212c73816e3ae68c7ca7f7a4cb09a3f192 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Mar 2020 11:29:31 +0200 Subject: [PATCH 62/79] Adjust tests to pass validation --- tests/strategy/test_interface.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 4e8d8f708..be8750c3c 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -85,14 +85,19 @@ def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ohlcv_history) def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): - caplog.set_level(logging.INFO) # default_conf defines a 5m interval. we check interval * 2 + 5m # this is necessary as the last candle is removed (partial candles) by default - oldtime = arrow.utcnow().shift(minutes=-16) - ticks = DataFrame([{'buy': 1, 'date': oldtime}]) + ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16) + # Take a copy to correctly modify the call + mocked_history = ohlcv_history.copy() + mocked_history['sell'] = 0 + mocked_history['buy'] = 0 + mocked_history.loc[1, 'buy'] = 1 + + caplog.set_level(logging.INFO) mocker.patch.object( _STRATEGY, '_analyze_ticker_internal', - return_value=DataFrame(ticks) + return_value=mocked_history ) mocker.patch.object(_STRATEGY, 'assert_df') assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], From cd2e738e351ae2e84d144009291c5346848f029b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Mar 2020 11:40:13 +0200 Subject: [PATCH 63/79] Add test for assert error --- freqtrade/strategy/interface.py | 11 ++++++----- tests/strategy/test_interface.py | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 4f833be23..89a38bf54 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -279,11 +279,12 @@ class IStrategy(ABC): dataframe = self._analyze_ticker_internal(dataframe, {'pair': pair}) self.assert_df(dataframe, df_len, df_close, df_date) except ValueError as error: - logger.warning( - 'Unable to analyze candle (OHLCV) data for pair %s: %s', - pair, - str(error) - ) + logger.warning('Unable to analyze candle (OHLCV) data for pair %s: %s', + pair, str(error)) + return False, False + except DependencyException as error: + logger.warning("Unable to analyze candle (OHLCV) data for pair %s: %s", + pair, str(error)) return False, False except Exception as error: logger.exception( diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index be8750c3c..1f496a01b 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock import arrow from pandas import DataFrame +from freqtrade.exceptions import DependencyException from freqtrade.configuration import TimeRange from freqtrade.data.history import load_data from freqtrade.persistence import Trade @@ -105,6 +106,26 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) +def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history): + # default_conf defines a 5m interval. we check interval * 2 + 5m + # this is necessary as the last candle is removed (partial candles) by default + ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16) + # Take a copy to correctly modify the call + mocked_history = ohlcv_history.copy() + mocked_history['sell'] = 0 + mocked_history['buy'] = 0 + mocked_history.loc[1, 'buy'] = 1 + + caplog.set_level(logging.INFO) + mocker.patch.object( + _STRATEGY, 'assert_df', + side_effect=DependencyException('Dataframe returned...') + ) + assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], + ohlcv_history) + assert log_has('Unable to analyze candle (OHLCV) data for pair xyz: Dataframe returned...', caplog) + + def test_get_signal_handles_exceptions(mocker, default_conf): exchange = get_patched_exchange(mocker, default_conf) mocker.patch.object( From 83cc121b706efe6e364885f24778255e9c0eb60a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Mar 2020 11:44:36 +0200 Subject: [PATCH 64/79] Add tsts for assert_df (ensuring it raises when it should) --- tests/strategy/test_interface.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 1f496a01b..8bc399f42 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -4,16 +4,18 @@ import logging from unittest.mock import MagicMock import arrow +import pytest from pandas import DataFrame -from freqtrade.exceptions import DependencyException from freqtrade.configuration import TimeRange from freqtrade.data.history import load_data +from freqtrade.exceptions import DependencyException from freqtrade.persistence import Trade from freqtrade.resolvers import StrategyResolver -from .strats.default_strategy import DefaultStrategy from tests.conftest import get_patched_exchange, log_has +from .strats.default_strategy import DefaultStrategy + # Avoid to reinit the same object again and again _STRATEGY = DefaultStrategy(config={}) @@ -123,7 +125,27 @@ def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history): ) assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], ohlcv_history) - assert log_has('Unable to analyze candle (OHLCV) data for pair xyz: Dataframe returned...', caplog) + assert log_has('Unable to analyze candle (OHLCV) data for pair xyz: Dataframe returned...', + caplog) + + +def test_assert_df(default_conf, mocker, ohlcv_history): + # Ensure it's running when passed correctly + _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), + ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date']) + + with pytest.raises(DependencyException, match=r"Dataframe returned from strategy.*length\."): + _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history) + 1, + ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date']) + + with pytest.raises(DependencyException, + match=r"Dataframe returned from strategy.*last close price\."): + _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), + ohlcv_history.loc[1, 'close'] + 0.01, ohlcv_history.loc[1, 'date']) + with pytest.raises(DependencyException, + match=r"Dataframe returned from strategy.*last date\."): + _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), + ohlcv_history.loc[1, 'close'], ohlcv_history.loc[0, 'date']) def test_get_signal_handles_exceptions(mocker, default_conf): From a5d00ce7176a78058cb9af3d091a51902fa5b764 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Mar 2020 07:56:17 +0200 Subject: [PATCH 65/79] Remove defaultstrategy occurance from docs --- docs/bot-usage.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 78e137676..60cacfb94 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -145,9 +145,9 @@ It is recommended to use version control to keep track of changes to your strate This parameter will allow you to load your custom strategy class. Per default without `--strategy` or `-s` the bot will load the -`DefaultStrategy` included with the bot (`freqtrade/strategy/default_strategy.py`). +`SampleStrategy` installed by the `create-userdir` subcommand (usually `user_data/strategy/sample_strategy.py`). -The bot will search your strategy file within `user_data/strategies` and `freqtrade/strategy`. +The bot will search your strategy file within `user_data/strategies`. To load a strategy, simply pass the class name (e.g.: `CustomStrategy`) in this parameter. From f1b92e2569f8db26fb092ad3e579d94542d28f01 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Mar 2020 08:11:38 +0200 Subject: [PATCH 66/79] Improve wording of documentation --- docs/bot-usage.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 60cacfb94..b1649374a 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -144,10 +144,10 @@ It is recommended to use version control to keep track of changes to your strate ### How to use **--strategy**? This parameter will allow you to load your custom strategy class. -Per default without `--strategy` or `-s` the bot will load the -`SampleStrategy` installed by the `create-userdir` subcommand (usually `user_data/strategy/sample_strategy.py`). +To test the bot installation, you can use the `SampleStrategy` installed by the `create-userdir` subcommand (usually `user_data/strategy/sample_strategy.py`). The bot will search your strategy file within `user_data/strategies`. +To use other directories, please read the next section about `--strategy-path`. To load a strategy, simply pass the class name (e.g.: `CustomStrategy`) in this parameter. From 7e1719cfc71b0e4a26cf3922b6b279a78c4bd326 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2020 08:56:50 +0000 Subject: [PATCH 67/79] Bump prompt-toolkit from 3.0.4 to 3.0.5 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.4 to 3.0.5. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.4...3.0.5) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index e490c8927..c3ae57c72 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -30,4 +30,4 @@ flask==1.1.1 colorama==0.4.3 # Building config files interactively questionary==1.5.1 -prompt-toolkit==3.0.4 +prompt-toolkit==3.0.5 From 7e60e0549afd5a20c8d10178baa33ab89ae72d02 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2020 08:57:16 +0000 Subject: [PATCH 68/79] Bump flake8-tidy-imports from 4.0.0 to 4.1.0 Bumps [flake8-tidy-imports](https://github.com/adamchainz/flake8-tidy-imports) from 4.0.0 to 4.1.0. - [Release notes](https://github.com/adamchainz/flake8-tidy-imports/releases) - [Changelog](https://github.com/adamchainz/flake8-tidy-imports/blob/master/HISTORY.rst) - [Commits](https://github.com/adamchainz/flake8-tidy-imports/compare/4.0.0...4.1.0) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index a4d83eb4f..01f189c56 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,7 +6,7 @@ coveralls==1.11.1 flake8==3.7.9 flake8-type-annotations==0.1.0 -flake8-tidy-imports==4.0.0 +flake8-tidy-imports==4.1.0 mypy==0.770 pytest==5.4.1 pytest-asyncio==0.10.0 From 2de10d4c5665e6183f2d9d72f035f20288beb1af Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2020 08:58:21 +0000 Subject: [PATCH 69/79] Bump ccxt from 1.24.83 to 1.25.38 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.24.83 to 1.25.38. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.24.83...1.25.38) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index e490c8927..54a738e06 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.24.83 +ccxt==1.25.38 SQLAlchemy==1.3.15 python-telegram-bot==12.4.2 arrow==0.15.5 From d8d6fe3574ae73f8867233fc7f6daeb9cb03a3ec Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2020 08:58:54 +0000 Subject: [PATCH 70/79] Bump python-telegram-bot from 12.4.2 to 12.5 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 12.4.2 to 12.5. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v12.4.2...v12.5) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index e490c8927..57617697f 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -2,7 +2,7 @@ # mainly used for Raspberry pi installs ccxt==1.24.83 SQLAlchemy==1.3.15 -python-telegram-bot==12.4.2 +python-telegram-bot==12.5 arrow==0.15.5 cachetools==4.0.0 requests==2.23.0 From 54d20cb81c90f567924e4a41a6aabeb19a46b288 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Mar 2020 20:08:07 +0200 Subject: [PATCH 71/79] Plot percent correctly --- freqtrade/plot/plotting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index fc8f25612..fac3aa2a3 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -136,8 +136,8 @@ def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame) -> m df_comb.loc[lowdate, 'cum_profit'], ], mode='markers', - name=f"Max drawdown {max_drawdown:.2f}%", - text=f"Max drawdown {max_drawdown:.2f}%", + name=f"Max drawdown {max_drawdown * 100:.2f}%", + text=f"Max drawdown {max_drawdown * 100:.2f}%", marker=dict( symbol='square-open', size=9, From 45fb4d25abf06484c8a0020102fd9786826eaa8b Mon Sep 17 00:00:00 2001 From: orehunt Date: Tue, 31 Mar 2020 18:47:53 +0200 Subject: [PATCH 72/79] use equality instead of index for row lookups --- freqtrade/plot/plotting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index fc8f25612..b311c591a 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -132,8 +132,8 @@ def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame) -> m drawdown = go.Scatter( x=[highdate, lowdate], y=[ - df_comb.loc[highdate, 'cum_profit'], - df_comb.loc[lowdate, 'cum_profit'], + df_comb.loc[df_comb.index == highdate, 'cum_profit'], + df_comb.loc[df_comb.index == lowdate, 'cum_profit'], ], mode='markers', name=f"Max drawdown {max_drawdown:.2f}%", From 4e907e2304da16c5ed28f93aa239f244e4c2a79d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Apr 2020 14:35:53 +0200 Subject: [PATCH 73/79] Use timeframe_to_prev_date to move trade-date to candle --- freqtrade/plot/plotting.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index b311c591a..5da067069 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -10,6 +10,7 @@ from freqtrade.data.btanalysis import (calculate_max_drawdown, create_cum_profit, extract_trades_of_period, load_trades) from freqtrade.data.converter import trim_dataframe +from freqtrade.exchange import timeframe_to_prev_date from freqtrade.data.history import load_data from freqtrade.misc import pair_to_filename from freqtrade.resolvers import StrategyResolver @@ -122,7 +123,8 @@ def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_sub return fig -def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame) -> make_subplots: +def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame, + timeframe: str) -> make_subplots: """ Add scatter points indicating max drawdown """ @@ -132,8 +134,8 @@ def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame) -> m drawdown = go.Scatter( x=[highdate, lowdate], y=[ - df_comb.loc[df_comb.index == highdate, 'cum_profit'], - df_comb.loc[df_comb.index == lowdate, 'cum_profit'], + df_comb.loc[timeframe_to_prev_date(timeframe, highdate), 'cum_profit'], + df_comb.loc[timeframe_to_prev_date(timeframe, lowdate), 'cum_profit'], ], mode='markers', name=f"Max drawdown {max_drawdown:.2f}%", @@ -405,7 +407,7 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame], fig.add_trace(avgclose, 1, 1) fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit') - fig = add_max_drawdown(fig, 2, trades, df_comb) + fig = add_max_drawdown(fig, 2, trades, df_comb, timeframe) for pair in pairs: profit_col = f'cum_profit_{pair}' From a1e81a51efcc8497e1cdae8cb0303d894b6f2a17 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2020 09:09:54 +0000 Subject: [PATCH 74/79] Bump ccxt from 1.25.38 to 1.25.81 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.25.38 to 1.25.81. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.25.38...1.25.81) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 3c9802990..7e873aa83 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.25.38 +ccxt==1.25.81 SQLAlchemy==1.3.15 python-telegram-bot==12.5 arrow==0.15.5 From 5de7ee3bdb1f11d3ae082face12a960c804136db Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2020 09:10:07 +0000 Subject: [PATCH 75/79] Bump plotly from 4.5.4 to 4.6.0 Bumps [plotly](https://github.com/plotly/plotly.py) from 4.5.4 to 4.6.0. - [Release notes](https://github.com/plotly/plotly.py/releases) - [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.py/compare/v4.5.4...v4.6.0) Signed-off-by: dependabot-preview[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index 7a5b21e2d..3db48a201 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.5.4 +plotly==4.6.0 From 144d252e19bff28837f3f22fb29b3d42024e99eb Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2020 09:10:30 +0000 Subject: [PATCH 76/79] Bump flask from 1.1.1 to 1.1.2 Bumps [flask](https://github.com/pallets/flask) from 1.1.1 to 1.1.2. - [Release notes](https://github.com/pallets/flask/releases) - [Changelog](https://github.com/pallets/flask/blob/master/CHANGES.rst) - [Commits](https://github.com/pallets/flask/compare/1.1.1...1.1.2) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 3c9802990..310383680 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -24,7 +24,7 @@ python-rapidjson==0.9.1 sdnotify==0.3.2 # Api server -flask==1.1.1 +flask==1.1.2 # Support for colorized terminal output colorama==0.4.3 From 0b30cb7f8f42fd0e1fe430c3ec5299fda03dec09 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2020 09:11:33 +0000 Subject: [PATCH 77/79] Bump pytest-mock from 2.0.0 to 3.0.0 Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 2.0.0 to 3.0.0. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v2.0.0...v3.0.0) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 01f189c56..814003cbf 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ mypy==0.770 pytest==5.4.1 pytest-asyncio==0.10.0 pytest-cov==2.8.1 -pytest-mock==2.0.0 +pytest-mock==3.0.0 pytest-random-order==1.0.4 # Convert jupyter notebooks to markdown documents From be76e3c55482e9ae55bc820ca8b8e53dec247b75 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2020 09:23:21 +0000 Subject: [PATCH 78/79] Bump python-telegram-bot from 12.5 to 12.5.1 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 12.5 to 12.5.1. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v12.5...v12.5.1) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 7e873aa83..7f2b39ff2 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -2,7 +2,7 @@ # mainly used for Raspberry pi installs ccxt==1.25.81 SQLAlchemy==1.3.15 -python-telegram-bot==12.5 +python-telegram-bot==12.5.1 arrow==0.15.5 cachetools==4.0.0 requests==2.23.0 From 20abb379aaeaea97f63b193d5e1a2caddb892533 Mon Sep 17 00:00:00 2001 From: orehunt Date: Mon, 6 Apr 2020 15:49:59 +0200 Subject: [PATCH 79/79] trim trades to the available ohlcv data before plotting profits --- freqtrade/data/btanalysis.py | 13 ++++++++++--- freqtrade/plot/plotting.py | 3 +++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 23a9f720c..780980ad6 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -151,13 +151,20 @@ def load_trades(source: str, db_url: str, exportfilename: Path, return load_backtest_data(exportfilename) -def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> pd.DataFrame: +def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame, + date_index=False) -> pd.DataFrame: """ Compare trades and backtested pair DataFrames to get trades performed on backtested period :return: the DataFrame of a trades of period """ - trades = trades.loc[(trades['open_time'] >= dataframe.iloc[0]['date']) & - (trades['close_time'] <= dataframe.iloc[-1]['date'])] + if date_index: + trades_start = dataframe.index[0] + trades_stop = dataframe.index[-1] + else: + trades_start = dataframe.iloc[0]['date'] + trades_stop = dataframe.iloc[-1]['date'] + trades = trades.loc[(trades['open_time'] >= trades_start) & + (trades['close_time'] <= trades_stop)] return trades diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 5da067069..6dcb71cc4 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -385,6 +385,9 @@ def generate_profit_graph(pairs: str, data: Dict[str, pd.DataFrame], # Combine close-values for all pairs, rename columns to "pair" df_comb = combine_dataframes_with_mean(data, "close") + # Trim trades to available OHLCV data + trades = extract_trades_of_period(df_comb, trades, date_index=True) + # Add combined cumulative profit df_comb = create_cum_profit(df_comb, trades, 'cum_profit', timeframe)