Predicts forward average return and trades only when the prediction is strong relative to the model's recent training distribution and the local trend filter agrees.
Timeframe
5m
Direction
Long Only
Stoploss
-8.0%
Trailing Stop
Yes
ROI
0m: 6.0%, 90m: 3.0%, 240m: 1.5%, 720m: 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
"""
FreqaiTrendRegressorV1
Long-only FreqAI strategy for bull-trend / directional continuation regimes.
Designed for research, backtesting, hyperopt, and dry-run in a Freqtrade lab.
Suggested model: LightGBMRegressor or XGBoostRegressor.
Suggested timeframe: 5m.
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 FreqaiTrendRegressorV1(IStrategy):
"""
Predicts forward average return and trades only when the prediction is strong
relative to the model's recent training distribution and the local trend filter agrees.
"""
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.06,
"90": 0.03,
"240": 0.015,
"720": 0.0,
}
stoploss = -0.08
trailing_stop = True
trailing_stop_positive = 0.015
trailing_stop_positive_offset = 0.035
trailing_only_offset_is_reached = True
# --- Hyperoptable entry thresholds. Keep these out of feature engineering. ---
buy_z = DecimalParameter(0.50, 2.50, decimals=2, default=1.15, space="buy", optimize=True)
buy_pred_floor = DecimalParameter(0.002, 0.030, decimals=3, default=0.008, space="buy", optimize=True)
buy_di_max = DecimalParameter(0.50, 3.00, decimals=2, default=1.50, space="buy", optimize=True)
buy_adx_min = IntParameter(12, 40, default=20, space="buy", optimize=True)
buy_rsi_max = IntParameter(50, 78, default=68, space="buy", optimize=True)
# --- Hyperoptable exit thresholds. ---
sell_z = DecimalParameter(0.10, 1.75, decimals=2, default=0.55, space="sell", optimize=True)
sell_pred_floor = DecimalParameter(-0.030, 0.005, decimals=3, default=-0.005, space="sell", optimize=True)
sell_rsi_min = IntParameter(25, 55, default=42, space="sell", optimize=True)
# --- Hyperoptable protections. ---
protection_cooldown = IntParameter(1, 24, default=6, space="protection", optimize=True)
protection_stop_duration = IntParameter(12, 96, default=36, space="protection", optimize=True)
@property
def protections(self):
return [
{
"method": "CooldownPeriod",
"stop_duration_candles": self.protection_cooldown.value,
},
{
"method": "StoplossGuard",
"lookback_period_candles": 72,
"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:
"""
Auto-expanded features. FreqAI multiplies these by configured periods,
timeframes, shifted candles, and correlated pairs.
"""
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["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
dataframe["%-roc-period"] = ta.ROC(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_mid = bollinger["mid"].replace(0, np.nan)
dataframe["%-bb_width-period"] = (bollinger["upper"] - bollinger["lower"]) / bb_mid
dataframe["%-close_to_bb_mid-period"] = dataframe["close"] / bb_mid
rolling_volume = dataframe["volume"].rolling(period).mean().replace(0, np.nan)
dataframe["%-relative_volume-period"] = dataframe["volume"] / rolling_volume
return dataframe
def feature_engineering_expand_basic(
self, dataframe: DataFrame, metadata: dict, **kwargs
) -> DataFrame:
"""Basic auto-expanded features not duplicated by indicator_periods_candles."""
dataframe["%-pct_change"] = dataframe["close"].pct_change()
dataframe["%-raw_volume"] = dataframe["volume"]
dataframe["%-raw_price"] = dataframe["close"]
dataframe["%-high_low_range"] = (dataframe["high"] - dataframe["low"]) / dataframe[
"close"
].replace(0, np.nan)
dataframe["%-close_open_range"] = (dataframe["close"] - dataframe["open"]) / dataframe[
"open"
].replace(0, np.nan)
return dataframe
def feature_engineering_standard(
self, dataframe: DataFrame, metadata: dict, **kwargs
) -> DataFrame:
"""
Base-timeframe features. Double-percent columns are returned to the strategy
after FreqAI so they can be used as deterministic trade filters.
"""
dataframe["%%-ema_12"] = ta.EMA(dataframe, timeperiod=12)
dataframe["%%-ema_26"] = ta.EMA(dataframe, timeperiod=26)
dataframe["%%-ema_50"] = ta.EMA(dataframe, timeperiod=50)
dataframe["%%-ema_200"] = ta.EMA(dataframe, timeperiod=200)
dataframe["%%-rsi"] = ta.RSI(dataframe, timeperiod=14)
dataframe["%%-adx"] = ta.ADX(dataframe, timeperiod=14)
dataframe["%%-atr_pct"] = ta.ATR(dataframe, timeperiod=14) / dataframe["close"].replace(
0, np.nan
)
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:
"""
Regression target: mean forward return over the configured label horizon.
The target is for model training only. It is returned as predictions during inference.
"""
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)
# Defensive fallbacks for early startup, plotting, or partially configured FreqAI runs.
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["%%-ema_50"] > dataframe["%%-ema_200"],
dataframe["%%-ema_12"] > dataframe["%%-ema_26"],
dataframe["close"] > dataframe["%%-ema_50"],
dataframe["%%-adx"] > self.buy_adx_min.value,
dataframe["%%-rsi"] < self.buy_rsi_max.value,
dataframe["volume"] > 0,
]
if conditions:
dataframe.loc[reduce(lambda x, y: x & y, conditions), ["enter_long", "enter_tag"]] = (
1,
"freqai_trend_regressor",
)
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
)
trend_break = (
qtpylib.crossed_below(dataframe["%%-ema_12"], dataframe["%%-ema_26"])
| (dataframe["close"] < dataframe["%%-ema_50"])
| (dataframe["%%-rsi"] < self.sell_rsi_min.value)
)
conditions = [
dataframe["volume"] > 0,
model_turn | trend_break,
]
dataframe.loc[reduce(lambda x, y: x & y, conditions), ["exit_long", "exit_tag"]] = (
1,
"freqai_trend_exit",
)
return dataframe