Timeframe
15m
Direction
Long & Short
Stoploss
-12.0%
Trailing Stop
Yes
ROI
0m: 10.0%, 240m: 5.0%, 480m: 0.0%
Interface Version
N/A
Startup Candles
1000
Indicators
5
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
# isort: skip_file
"""
Starter FreqAI strategy for freqtrade-strategy-lab.
Simplified regression setup: targets &s_close / &s_volatility, XGBoostRegressor in config.
Replace thresholds and logic as you iterate — see CLAUDE.md and user_data/info/PROJECT_NOTES.md.
"""
import numpy as np
import pandas as pd
from pandas import DataFrame
from datetime import datetime
import talib.abstract as ta
from freqtrade.strategy import IStrategy
from freqtrade.persistence import Trade
class StarterStrategy_V01(IStrategy):
minimal_roi = {"0": 0.10, "240": 0.05, "480": 0}
stoploss = -0.12
use_custom_stoploss = True
trailing_stop = True
trailing_stop_positive = 0.015
trailing_stop_positive_offset = 0.04
trailing_only_offset_is_reached = True
timeframe = "15m"
inf_1h = "1h"
can_short = True
startup_candle_count = 1000
# Fixed lab thresholds — move to Hyperopt DecimalParameter when ready
ai_direction_threshold = 0.012
ai_volatility_threshold = 0.012
di_max_threshold = 0.45
stoploss_multiplier = 1.4
stoploss_hard_cap = 0.04
def feature_engineering_expand_all(
self, dataframe: DataFrame, period: int, metadata: dict, **kwargs
) -> DataFrame:
dataframe[f"%-rsi-{period}"] = ta.RSI(dataframe, timeperiod=period)
dataframe[f"%-roc-{period}"] = ta.ROC(dataframe, timeperiod=period)
dataframe[f"%-atr-{period}"] = ta.ATR(dataframe, timeperiod=period)
safe_atr = dataframe[f"%-atr-{period}"].replace(0, np.nan)
dataframe[f"%-rsi_atr_ratio-{period}"] = dataframe[f"%-rsi-{period}"] / safe_atr
ema = ta.EMA(dataframe, timeperiod=period)
dataframe[f"%-ema_dist-{period}"] = (dataframe["close"] - ema) / ema
bollinger = ta.BBANDS(dataframe, timeperiod=period, nbdevup=2.0, nbdevdn=2.0, matype=0)
dataframe[f"%-bb_width-{period}"] = (
bollinger["upperband"] - bollinger["lowerband"]
) / bollinger["middleband"]
return dataframe
def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs) -> DataFrame:
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
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"] = np.log(dataframe["close"].shift(-label_period) / dataframe["close"])
future_high = dataframe["high"].rolling(label_period).max().shift(-label_period)
future_low = dataframe["low"].rolling(label_period).min().shift(-label_period)
dataframe["&s_volatility"] = (future_high - future_low) / dataframe["close"]
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe = self.freqai.start(dataframe, metadata, self)
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
dataframe["ema_fast"] = ta.EMA(dataframe, timeperiod=9)
dataframe["ema_slow"] = ta.EMA(dataframe, timeperiod=21)
dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
if not self.config.get("freqai", {}).get("enabled", False):
return dataframe
if "do_predict" not in dataframe.columns:
return dataframe
valid_prediction = dataframe["do_predict"] == 1
if "DI_values" in dataframe.columns:
safe_situation = dataframe["DI_values"] < self.di_max_threshold
else:
safe_situation = pd.Series(True, index=dataframe.index)
if "&s_close" not in dataframe.columns or "&s_volatility" not in dataframe.columns:
return dataframe
ai_long = (
(dataframe["&s_close"] > self.ai_direction_threshold)
& (dataframe["&s_volatility"] > self.ai_volatility_threshold)
)
tech_long = (dataframe["rsi"] < 70) & (dataframe["ema_fast"] > dataframe["ema_slow"])
dataframe.loc[
valid_prediction & safe_situation & ai_long & tech_long & (dataframe["volume"] > 0),
"enter_long",
] = 1
ai_short = (
(dataframe["&s_close"] < -self.ai_direction_threshold)
& (dataframe["&s_volatility"] > self.ai_volatility_threshold)
)
tech_short = (dataframe["rsi"] > 30) & (dataframe["ema_fast"] < dataframe["ema_slow"])
dataframe.loc[
valid_prediction & safe_situation & ai_short & tech_short & (dataframe["volume"] > 0),
"enter_short",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
if not self.config.get("freqai", {}).get("enabled", False):
return dataframe
if "&s_close" not in dataframe.columns or "DI_values" not in dataframe.columns:
return dataframe
di_breach = dataframe["DI_values"] >= self.di_max_threshold
dataframe.loc[(dataframe["&s_close"] < 0) | di_breach, "exit_long"] = 1
dataframe.loc[(dataframe["&s_close"] > 0) | di_breach, "exit_short"] = 1
return dataframe
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
**kwargs,
) -> float:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
if "atr" in last_candle and pd.notna(last_candle["atr"]):
atr_pct = last_candle["atr"] / trade.open_rate
dynamic_stop_pct = atr_pct * self.stoploss_multiplier
dynamic_stop_pct = max(0.010, min(self.stoploss_hard_cap, dynamic_stop_pct))
if trade.is_short:
stop_price = trade.open_rate * (1 + dynamic_stop_pct)
rel_stop = (current_rate - stop_price) / current_rate
else:
stop_price = trade.open_rate * (1 - dynamic_stop_pct)
rel_stop = (stop_price - current_rate) / current_rate
return rel_stop
return -0.025
def leverage(
self,
pair: str,
current_time: datetime,
current_rate: float,
proposed_leverage: float,
max_leverage: float,
entry_tag: str,
side: str,
**kwargs,
) -> float:
return min(3.0, max_leverage)