diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index ec521470a..633b82385 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -58,12 +58,12 @@ file as reference.** !!! Note "Strategies and Backtesting" To avoid problems and unexpected differences between Backtesting and dry/live modes, please be aware - that during backtesting the full time-interval is passed to the `populate_*()` methods at once. + that during backtesting the full time range is passed to the `populate_*()` methods at once. It is therefore best to use vectorized operations (across the whole dataframe, not loops) and avoid index referencing (`df.iloc[-1]`), but instead use `df.shift()` to get to the previous candle. !!! Warning "Warning: Using future data" - Since backtesting passes the full time interval to the `populate_*()` methods, the strategy author + Since backtesting passes the full time range to the `populate_*()` methods, the strategy author needs to take care to avoid having the strategy utilize data from the future. Some common patterns for this are listed in the [Common Mistakes](#common-mistakes-when-developing-strategies) section of this document. @@ -251,7 +251,7 @@ minimal_roi = { While technically not completely disabled, this would sell once the trade reaches 10000% Profit. To use times based on candle duration (timeframe), the following snippet can be handy. -This will allow you to change the ticket_interval for the strategy, and ROI times will still be set as candles (e.g. after 3 candles ...) +This will allow you to change the timeframe for the strategy, and ROI times will still be set as candles (e.g. after 3 candles ...) ``` python from freqtrade.exchange import timeframe_to_minutes @@ -285,7 +285,7 @@ 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). -### Timeframe (ticker interval) +### Timeframe (formerly 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. @@ -333,10 +333,10 @@ class Awesomestrategy(IStrategy): #### Get data for non-tradeable pairs Data for additional, informative pairs (reference pairs) can be beneficial for some strategies. -Ohlcv data for these pairs will be downloaded as part of the regular whitelist refresh process and is available via `DataProvider` just as other pairs (see below). +OHLCV data for these pairs will be downloaded as part of the regular whitelist refresh process and is available via `DataProvider` just as other pairs (see below). These parts will **not** be traded unless they are also specified in the pair whitelist, or have been selected by Dynamic Whitelisting. -The pairs need to be specified as tuples in the format `("pair", "interval")`, with pair as the first and time interval as the second argument. +The pairs need to be specified as tuples in the format `("pair", "timeframe")`, with pair as the first and timeframe as the second argument. Sample: @@ -349,8 +349,8 @@ def informative_pairs(self): !!! Warning As these pairs will be refreshed as part of the regular whitelist refresh, it's best to keep this list short. - All intervals and all pairs can be specified as long as they are available (and active) on the used exchange. - It is however better to use resampling to longer time-intervals when possible + All timeframes and all pairs can be specified as long as they are available (and active) on the used exchange. + It is however better to use resampling to longer timeframes whenever possible to avoid hammering the exchange with too many requests and risk being blocked. *** @@ -363,10 +363,14 @@ All methods return `None` in case of failure (do not raise an exception). Please always check the mode of operation to select the correct method to get data (samples see below). +!!! Warning "Hyperopt" + Dataprovider is available during hyperopt, however it can only be used in `populate_indicators()`. + It is not available in `populate_buy()` and `populate_sell()` methods. + ### Possible options for DataProvider -- [`available_pairs`](#available_pairs) - Property with tuples listing cached pairs with their intervals (pair, interval). -- [`current_whitelist()`](#current_whitelist) - Returns a current list of whitelisted pairs. Useful for accessing dynamic whitelists (ie. VolumePairlist) +- [`available_pairs`](#available_pairs) - Property with tuples listing cached pairs with their timeframe (pair, timeframe). +- [`current_whitelist()`](#current_whitelist) - Returns a current list of whitelisted pairs. Useful for accessing dynamic whitelists (i.e. VolumePairlist) - [`get_pair_dataframe(pair, timeframe)`](#get_pair_dataframepair-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). - [`get_analyzed_dataframe(pair, timeframe)`](#get_analyzed_dataframepair-timeframe) - Returns the analyzed dataframe (after calling `populate_indicators()`, `populate_buy()`, `populate_sell()`) and the time of the latest analysis. - `historic_ohlcv(pair, timeframe)` - Returns historical data stored on disk. @@ -401,58 +405,13 @@ Since we can't resample our data we will have to use an informative pair; and si This is where calling `self.dp.current_whitelist()` comes in handy. ```python -class SampleStrategy(IStrategy): - # strategy init stuff... - - timeframe = '5m' - - # more strategy init stuff.. - def informative_pairs(self): # get access to all pairs available in whitelist. pairs = self.dp.current_whitelist() # Assign tf to each pair so they can be downloaded and cached for strategy. informative_pairs = [(pair, '1d') for pair in pairs] - return informative_pairs - - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - - inf_tf = '1d' - # Get the informative pair - informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=inf_tf) - # Get the 14 day rsi - informative['rsi'] = ta.RSI(informative, timeperiod=14) - - # Rename columns to be unique - informative.columns = [f"{col}_{inf_tf}" for col in informative.columns] - # Assuming inf_tf = '1d' - then the columns will now be: - # date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d - - # Combine the 2 dataframes - # all indicators on the informative sample MUST be calculated before this point - dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_{inf_tf}', how='left') - # FFill to have the 1d value available in every row throughout the day. - # Without this, comparisons would only work once per day. - dataframe = dataframe.ffill() - # Calculate rsi of the original dataframe (5m timeframe) - dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) - - # Do other stuff - # ... - - return dataframe - - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - - dataframe.loc[ - ( - (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 - (dataframe['rsi_1d'] < 30) & # Ensure daily RSI is < 30 - (dataframe['volume'] > 0) # Ensure this candle had volume (important for backtesting) - ), - 'buy'] = 1 - + return informative_pairs ``` ### *get_pair_dataframe(pair, timeframe)* @@ -479,7 +438,7 @@ It can also be used in specific callbacks to get the signal that caused the acti # fetch current dataframe if self.dp: dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=metadata['pair'], - timeframe=self.ticker_interval) + timeframe=self.timeframe) ``` !!! Note "No data available" @@ -516,6 +475,74 @@ if self.dp: does not always fills in the `last` field (so it can be None), etc. So you need to carefully verify the ticker data returned from the exchange and add appropriate error handling / defaults. +!!! Warning "Warning about backtesting" + This method will always return up-to-date values - so usage during backtesting / hyperopt will lead to wrong results. + +### Complete Data-provider sample + +```python +class SampleStrategy(IStrategy): + # strategy init stuff... + + timeframe = '5m' + + # more strategy init stuff.. + + def informative_pairs(self): + + # get access to all pairs available in whitelist. + pairs = self.dp.current_whitelist() + # Assign tf to each pair so they can be downloaded and cached for strategy. + informative_pairs = [(pair, '1d') for pair in pairs] + # Optionally Add additional "static" pairs + informative_pairs += [("ETH/USDT", "5m"), + ("BTC/TUSD", "15m"), + ] + return informative_pairs + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + if not self.dp: + # Don't do anything if DataProvider is not available. + return dataframe + + inf_tf = '1d' + # Get the informative pair + informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=inf_tf) + # Get the 14 day rsi + informative['rsi'] = ta.RSI(informative, timeperiod=14) + + # Rename columns to be unique + informative.columns = [f"{col}_{inf_tf}" for col in informative.columns] + # Assuming inf_tf = '1d' - then the columns will now be: + # date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d + + # Combine the 2 dataframes + # all indicators on the informative sample MUST be calculated before this point + dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_{inf_tf}', how='left') + # FFill to have the 1d value available in every row throughout the day. + # Without this, comparisons would only work once per day. + dataframe = dataframe.ffill() + + # Calculate rsi of the original dataframe (5m timeframe) + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + + # Do other stuff + # ... + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + dataframe.loc[ + ( + (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 + (dataframe['rsi_1d'] < 30) & # Ensure daily RSI is < 30 + (dataframe['volume'] > 0) # Ensure this candle had volume (important for backtesting) + ), + 'buy'] = 1 + +``` + *** ## Additional data (Wallets)