Timeframe
5m
Direction
Long Only
Stoploss
-3.0%
Trailing Stop
No
ROI
0m: 2.0%, 30m: 1.0%, 90m: 0.0%
Interface Version
N/A
Startup Candles
200
Indicators
3
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
from typing import Dict, Optional
import logging
import numpy as np
import pandas as pd
from freqtrade.strategy import (
IStrategy,
IntParameter,
DecimalParameter,
)
from freqtrade.enums import RunMode
try:
import talib.abstract as ta
except Exception as e: # pragma: no cover - container provides TA-Lib
ta = None # type: ignore
try:
from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer # type: ignore
except Exception:
SentimentIntensityAnalyzer = None # type: ignore
import os
import requests
logger = logging.getLogger(__name__)
class MyFreqAIStrategy(IStrategy):
# Core settings
timeframe = "5m"
process_only_new_candles = True
startup_candle_count = 200
# Minimal ROI and stoploss for scalping focus
minimal_roi = {
"0": 0.02,
"30": 0.01,
"90": 0
}
stoploss = -0.03
use_exit_signal = True
exit_profit_only = False
ignore_buying_expired_candle_after = 120
plot_config = {
"main_plot": {
"close": {"color": "white"},
},
"subplots": {
"RSI": {"rsi": {"color": "blue"}},
"ADX": {"adx": {"color": "orange"}},
"WILLR": {"willr": {"color": "purple"}},
"Sentiment": {
"sentiment_normalized": {"color": "green"},
"fear_greed": {"color": "red"}
},
},
}
# Optional hyperopt ranges (kept minimal)
rsi_period = IntParameter(9, 21, default=14, space="buy")
willr_period = IntParameter(10, 21, default=14, space="buy")
adx_min = IntParameter(20, 35, default=25, space="buy")
sentiment_floor = DecimalParameter(0.0, 1.0, default=0.0, decimals=2, space="buy")
def informative_pairs(self):
return []
# ---------- Feature helpers ----------
def add_sentiment_features(self, dataframe: pd.DataFrame, metadata: Dict) -> pd.DataFrame:
"""Add sentiment features using VADER when available. Fallback to neutral.
This is a lightweight placeholder fetching stubbed texts – replace with real API calls.
"""
# Disable network during backtesting/hyperopt to keep runs reproducible
if self._is_historic_run():
dataframe["sentiment_compound"] = 0.0
dataframe["sentiment_normalized"] = 0.5
return dataframe
try:
analyzer = SentimentIntensityAnalyzer() if SentimentIntensityAnalyzer else None
except Exception:
analyzer = None
score: Optional[float] = None
if analyzer:
try:
texts = [
f"{metadata.get('pair', 'PAIR')} bullish momentum!",
f"Concerns around {metadata.get('pair', 'PAIR')} pullback",
]
scores = [analyzer.polarity_scores(t)["compound"] for t in texts]
score = float(pd.Series(scores).mean())
except Exception as e:
logger.warning("Sentiment analysis failed, defaulting neutral: %s", e)
if score is None:
score = 0.0
dataframe["sentiment_compound"] = score
# Normalize [-1,1] -> [0,1]
dataframe["sentiment_normalized"] = (dataframe["sentiment_compound"] + 1.0) / 2.0
return dataframe
def add_fear_greed(self, dataframe: pd.DataFrame) -> pd.DataFrame:
"""Attach Fear & Greed Index.
Priority: use historical CSV if present -> live API (non-historic only) -> neutral 0.5.
"""
# Try historical CSV merge first (reproducible backtests)
try:
fg_path = os.path.join("/freqtrade", "user_data", "data", "fear_greed.csv")
if os.path.exists(fg_path):
fg = pd.read_csv(fg_path)
# Expect columns: date (YYYY-MM-DD), value (0..100)
if {"date", "value"}.issubset(set(fg.columns)) and "date" in dataframe.columns:
fg["date"] = pd.to_datetime(fg["date"]).dt.date
left = dataframe.copy()
left["_date_only"] = pd.to_datetime(left["date"]).dt.date
fg["fear_greed"] = fg["value"].astype(float) / 100.0
merged = left.merge(
fg[["date", "fear_greed"]],
left_on="_date_only",
right_on="date",
how="left",
)
dataframe["fear_greed"] = merged["fear_greed"].fillna(0.5).values
return dataframe
except Exception as e: # pragma: no cover
logger.warning("Failed to merge historical Fear&Greed: %s", e)
# Live fetch only when not backtesting/hyperopting
fg_value = 0.5
if not self._is_historic_run():
try:
resp = requests.get("https://api.alternative.me/fng/?limit=1", timeout=5)
if resp.ok:
fg_value = int(resp.json()["data"][0]["value"]) / 100.0
except Exception as e:
logger.warning("Fear&Greed fetch failed, using neutral 0.5: %s", e)
dataframe["fear_greed"] = fg_value
return dataframe
# ---------- Indicators & Signals ----------
def populate_indicators(self, dataframe: pd.DataFrame, metadata: Dict) -> pd.DataFrame:
# RSI and WILLR
if ta is None:
# Basic numpy fallbacks if TA-Lib not present (very rough)
dataframe["rsi"] = pd.Series(np.nan, index=dataframe.index)
dataframe["willr"] = pd.Series(np.nan, index=dataframe.index)
dataframe["adx"] = pd.Series(np.nan, index=dataframe.index)
logger.warning("TA-Lib not available: indicators set to NaN; no trades will trigger.")
else:
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=int(self.rsi_period.value))
dataframe["willr"] = ta.WILLR(dataframe, timeperiod=int(self.willr_period.value))
dataframe["adx"] = ta.ADX(dataframe)
# Add sentiment & fear/greed
dataframe = self.add_sentiment_features(dataframe, metadata)
dataframe = self.add_fear_greed(dataframe)
# Volume rolling mean (basic filter)
if "volume" in dataframe.columns:
dataframe["vol_sma50"] = dataframe["volume"].rolling(50).mean()
# Attempt FreqAI pipeline (safe no-op if disabled or unavailable)
try:
if getattr(self, "freqai", None):
dataframe = self.freqai.start(dataframe, metadata, self)
except Exception as e: # pragma: no cover
logger.warning("FreqAI integration skipped due to error: %s", e)
return dataframe
def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: Dict) -> pd.DataFrame:
dataframe.loc[:, "enter_long"] = 0
cond = (
(dataframe["rsi"] < 30)
& (dataframe["willr"] < -80)
& (dataframe["adx"] > int(self.adx_min.value))
)
# Optional volume filter when available
if "vol_sma50" in dataframe.columns:
cond &= dataframe["volume"] > dataframe["vol_sma50"].fillna(0)
# Optional sentiment floor if provided via hyperopt/config (default 0.0 = disabled)
if "sentiment_normalized" in dataframe.columns and float(self.sentiment_floor.value) > 0.0:
cond &= dataframe["sentiment_normalized"] >= float(self.sentiment_floor.value)
# Optional FreqAI gating if predictions exist
if "do_predict" in dataframe.columns:
cond &= dataframe["do_predict"] == 1
if "DI_values" in dataframe.columns:
cond &= dataframe["DI_values"].fillna(1.0) < 0.05
dataframe.loc[cond, ["enter_long"]] = 1
return dataframe
# ---------- FreqAI hooks ----------
def set_freqai_targets(self, dataframe: pd.DataFrame, **kwargs) -> pd.DataFrame:
"""Define regression target as forward return over N candles (label_period_candles)."""
try:
n = int(self.freqai_info["feature_parameters"].get("label_period_candles", 12))
except Exception:
n = 12
future_close = dataframe["close"].shift(-n)
dataframe["&-return"] = (future_close / dataframe["close"]) - 1.0
return dataframe
def feature_engineering_standard(self, dataframe: pd.DataFrame, **kwargs) -> pd.DataFrame:
"""Provide core features for FreqAI. Columns must be prefixed with '%-'."""
# Ensure base indicators exist
if "rsi" not in dataframe.columns and ta is not None:
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=int(self.rsi_period.value))
if "willr" not in dataframe.columns and ta is not None:
dataframe["willr"] = ta.WILLR(dataframe, timeperiod=int(self.willr_period.value))
if "adx" not in dataframe.columns and ta is not None:
dataframe["adx"] = ta.ADX(dataframe)
dataframe["%-rsi"] = dataframe.get("rsi")
dataframe["%-willr"] = dataframe.get("willr")
dataframe["%-adx"] = dataframe.get("adx")
if "fear_greed" in dataframe.columns:
dataframe["%-fear_greed"] = dataframe["fear_greed"].fillna(0.5)
if "sentiment_normalized" in dataframe.columns:
dataframe["%-sentiment"] = dataframe["sentiment_normalized"].fillna(0.5)
if "volume" in dataframe.columns:
if "vol_sma50" not in dataframe.columns:
dataframe["vol_sma50"] = dataframe["volume"].rolling(50).mean()
dataframe["%-vol_above_sma50"] = (dataframe["volume"] > dataframe["vol_sma50"].fillna(0)).astype(float)
return dataframe
# ---------- Helpers ----------
def _is_historic_run(self) -> bool:
"""True for backtesting/hyperopt to ensure reproducibility (no live APIs)."""
try:
return bool(self.dp and self.dp.runmode in {RunMode.BACKTEST, RunMode.HYPEROPT})
except Exception:
return False
def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: Dict) -> pd.DataFrame:
dataframe.loc[:, "exit_long"] = 0
# Simple exit: Overbought or profit target met by ROI table
dataframe.loc[
(
(dataframe["rsi"] > 70)
| (dataframe["willr"] > -20)
),
["exit_long"],
] = 1
return dataframe