Mean-reversion strategy. It only enters stretched, relatively quiet ranges when FreqAI predicts a positive forward average return.
Timeframe
5m
Direction
Long Only
Stoploss
-6.0%
Trailing Stop
Yes
ROI
0m: 3.5%, 60m: 2.0%, 180m: 1.0%, 480m: 0.0%
Interface Version
3
Startup Candles
N/A
Indicators
8
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
"""
FreqaiRangeReversionV1
Long-only FreqAI strategy for low-volatility range / mean-reversion regimes.
Designed for research, backtesting, hyperopt, and dry-run in a Freqtrade lab.
Suggested model: LightGBMRegressor or XGBoostRegressor.
Suggested timeframe: 5m or 15m.
Hyperopt only entry/exit thresholds, ROI, stoploss, trailing, and protections.
Do not hyperopt feature_engineering_*() or set_freqai_targets().
"""
from __future__ import annotations
from functools import reduce
import numpy as np
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.strategy import DecimalParameter, IStrategy, IntParameter
from freqtrade.vendor.qtpylib import indicators as qtpylib
class FreqaiRangeReversionV1(IStrategy):
"""
Mean-reversion strategy. It only enters stretched, relatively quiet ranges when
FreqAI predicts a positive forward average return.
"""
INTERFACE_VERSION = 3
timeframe = "5m"
can_short = False
process_only_new_candles = True
startup_candle_count: int = 400
use_exit_signal = True
exit_profit_only = False
ignore_roi_if_entry_signal = False
minimal_roi = {
"0": 0.035,
"60": 0.020,
"180": 0.010,
"480": 0.0,
}
stoploss = -0.06
trailing_stop = True
trailing_stop_positive = 0.010
trailing_stop_positive_offset = 0.025
trailing_only_offset_is_reached = True
# Entry thresholds.
buy_z = DecimalParameter(0.10, 1.75, decimals=2, default=0.65, space="buy", optimize=True)
buy_pred_floor = DecimalParameter(0.001, 0.020, decimals=3, default=0.004, space="buy", optimize=True)
buy_di_max = DecimalParameter(0.50, 3.00, decimals=2, default=1.60, space="buy", optimize=True)
buy_rsi_max = IntParameter(15, 45, default=32, space="buy", optimize=True)
buy_adx_max = IntParameter(10, 32, default=24, space="buy", optimize=True)
buy_bb_percent_max = DecimalParameter(0.00, 0.35, decimals=2, default=0.12, space="buy", optimize=True)
buy_bb_width_max = DecimalParameter(0.010, 0.200, decimals=3, default=0.080, space="buy", optimize=True)
buy_atr_pct_max = DecimalParameter(0.002, 0.080, decimals=3, default=0.030, space="buy", optimize=True)
# Exit thresholds.
sell_z = DecimalParameter(0.00, 1.25, decimals=2, default=0.25, space="sell", optimize=True)
sell_pred_floor = DecimalParameter(-0.020, 0.010, decimals=3, default=-0.002, space="sell", optimize=True)
sell_bb_percent_min = DecimalParameter(0.45, 1.00, decimals=2, default=0.72, space="sell", optimize=True)
sell_rsi_min = IntParameter(50, 82, default=64, space="sell", optimize=True)
# Protections.
protection_cooldown = IntParameter(1, 24, default=4, space="protection", optimize=True)
protection_stop_duration = IntParameter(12, 96, default=30, space="protection", optimize=True)
@property
def protections(self):
return [
{
"method": "CooldownPeriod",
"stop_duration_candles": self.protection_cooldown.value,
},
{
"method": "StoplossGuard",
"lookback_period_candles": 96,
"trade_limit": 2,
"stop_duration_candles": self.protection_stop_duration.value,
"only_per_pair": False,
},
]
def feature_engineering_expand_all(
self, dataframe: DataFrame, period: int, metadata: dict, **kwargs
) -> DataFrame:
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
dataframe["%-cci-period"] = ta.CCI(dataframe, timeperiod=period)
dataframe["%-atr-period"] = ta.ATR(dataframe, timeperiod=period)
bollinger = qtpylib.bollinger_bands(
qtpylib.typical_price(dataframe), window=period, stds=2.0
)
bb_range = (bollinger["upper"] - bollinger["lower"]).replace(0, np.nan)
dataframe["%-bb_width-period"] = bb_range / bollinger["mid"].replace(0, np.nan)
dataframe["%-bb_percent-period"] = (dataframe["close"] - bollinger["lower"]) / bb_range
dataframe["%-relative_volume-period"] = dataframe["volume"] / dataframe[
"volume"
].rolling(period).mean().replace(0, np.nan)
return dataframe
def feature_engineering_expand_basic(
self, dataframe: DataFrame, metadata: dict, **kwargs
) -> DataFrame:
dataframe["%-pct_change"] = dataframe["close"].pct_change()
dataframe["%-raw_volume"] = dataframe["volume"]
dataframe["%-raw_price"] = dataframe["close"]
dataframe["%-candle_body_pct"] = (dataframe["close"] - dataframe["open"]) / dataframe[
"open"
].replace(0, np.nan)
dataframe["%-upper_wick_pct"] = (
dataframe["high"] - np.maximum(dataframe["open"], dataframe["close"])
) / dataframe["close"].replace(0, np.nan)
dataframe["%-lower_wick_pct"] = (
np.minimum(dataframe["open"], dataframe["close"]) - dataframe["low"]
) / dataframe["close"].replace(0, np.nan)
return dataframe
def feature_engineering_standard(
self, dataframe: DataFrame, metadata: dict, **kwargs
) -> DataFrame:
dataframe["%%-rsi"] = ta.RSI(dataframe, timeperiod=14)
dataframe["%%-adx"] = ta.ADX(dataframe, timeperiod=14)
dataframe["%%-ema_50"] = ta.EMA(dataframe, timeperiod=50)
dataframe["%%-ema_200"] = ta.EMA(dataframe, timeperiod=200)
dataframe["%%-atr_pct"] = ta.ATR(dataframe, timeperiod=14) / dataframe["close"].replace(
0, np.nan
)
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2.0)
bb_range = (bollinger["upper"] - bollinger["lower"]).replace(0, np.nan)
dataframe["%%-bb_lower"] = bollinger["lower"]
dataframe["%%-bb_mid"] = bollinger["mid"]
dataframe["%%-bb_upper"] = bollinger["upper"]
dataframe["%%-bb_width"] = bb_range / bollinger["mid"].replace(0, np.nan)
dataframe["%%-bb_percent"] = (dataframe["close"] - bollinger["lower"]) / bb_range
dataframe["%%-volume_ratio_20"] = dataframe["volume"] / dataframe["volume"].rolling(
20
).mean().replace(0, np.nan)
dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7
dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 24
return dataframe
def set_freqai_targets(
self, dataframe: DataFrame, metadata: dict, **kwargs
) -> DataFrame:
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
dataframe["&-s_close"] = (
dataframe["close"].shift(-label_period).rolling(label_period).mean()
/ dataframe["close"]
- 1.0
)
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe = self.freqai.start(dataframe, metadata, self)
if "do_predict" not in dataframe:
dataframe["do_predict"] = 0
if "DI_values" not in dataframe:
dataframe["DI_values"] = 0.0
if "&-s_close_mean" not in dataframe:
dataframe["&-s_close_mean"] = 0.0
if "&-s_close_std" not in dataframe:
dataframe["&-s_close_std"] = 0.0
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["enter_long"] = 0
dataframe["enter_tag"] = ""
prediction = dataframe["&-s_close"]
dynamic_target = dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * self.buy_z.value
conditions = [
dataframe["do_predict"] == 1,
dataframe["DI_values"] < self.buy_di_max.value,
prediction > dynamic_target,
prediction > self.buy_pred_floor.value,
dataframe["%%-bb_percent"] < self.buy_bb_percent_max.value,
dataframe["%%-bb_width"] < self.buy_bb_width_max.value,
dataframe["%%-atr_pct"] < self.buy_atr_pct_max.value,
dataframe["%%-rsi"] < self.buy_rsi_max.value,
dataframe["%%-adx"] < self.buy_adx_max.value,
dataframe["close"] < dataframe["%%-bb_lower"],
dataframe["volume"] > 0,
]
dataframe.loc[reduce(lambda x, y: x & y, conditions), ["enter_long", "enter_tag"]] = (
1,
"freqai_range_reversion",
)
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["exit_long"] = 0
dataframe["exit_tag"] = ""
dynamic_exit = dataframe["&-s_close_mean"] - dataframe["&-s_close_std"] * self.sell_z.value
model_turn = (dataframe["&-s_close"] < dynamic_exit) | (
dataframe["&-s_close"] < self.sell_pred_floor.value
)
mean_reversion_complete = (
(dataframe["%%-bb_percent"] > self.sell_bb_percent_min.value)
| (dataframe["close"] > dataframe["%%-bb_mid"])
| (dataframe["%%-rsi"] > self.sell_rsi_min.value)
)
conditions = [
dataframe["volume"] > 0,
model_turn | mean_reversion_complete,
]
dataframe.loc[reduce(lambda x, y: x & y, conditions), ["exit_long", "exit_tag"]] = (
1,
"freqai_range_exit",
)
return dataframe