Higher Timeframe Confirmation Strategy.
Timeframe
5m
Direction
Long Only
Stoploss
-12.0%
Trailing Stop
No
ROI
0m: 10.0%, 30m: 3.5%, 60m: 1.5%, 120m: 0.0%
Interface Version
3
Startup Candles
N/A
Indicators
6
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
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, informative
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
class HighTimeframeStrategy(IStrategy):
"""
Higher Timeframe Confirmation Strategy.
Uses 1H chart for trend direction and 5m chart for precise entries.
This filters out the majority of false signals in the 5m mean reversion.
Architecture:
- 1H: EMA-50 slope determines trend direction (up/flat/down)
- 1H: RSI > 50 = bullish regime, RSI < 50 = bearish
- 5m: Mean reversion entries ONLY when 1H says oversold is valid
- 5m: Trend entries ONLY when 1H confirms uptrend
Expected improvement: +15-25% win rate vs pure 5m strategy
"""
INTERFACE_VERSION = 3
# 1H filter params
htf_ema_period = IntParameter(30, 100, default=50, space="buy")
htf_rsi_bull = IntParameter(45, 60, default=50, space="buy")
# 5m entry params
entry_rsi_buy = IntParameter(20, 40, default=28, space="buy")
entry_zscore = DecimalParameter(-3.0, -1.0, default=-1.5, space="buy", decimals=1)
entry_volume = DecimalParameter(1.5, 4.0, default=2.5, space="buy", decimals=1)
exit_rsi_sell = IntParameter(65, 85, default=75, space="sell")
exit_zscore = DecimalParameter(0.0, 2.0, default=0.5, space="sell", decimals=1)
minimal_roi = {"120": 0, "60": 0.015, "30": 0.035, "0": 0.10}
stoploss = -0.12
trailing_stop = False
timeframe = "5m"
process_only_new_candles = True
startup_candle_count: int = 200
@informative("1h")
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""1H indicators — used for higher timeframe trend filter."""
dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=self.htf_ema_period.value)
dataframe["ema_slope"] = dataframe["ema_50"] - dataframe["ema_50"].shift(3)
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
# Trend up: price above EMA + EMA rising + RSI bullish
dataframe["trend_up"] = (
(dataframe["close"] > dataframe["ema_50"]) &
(dataframe["ema_slope"] > 0) &
(dataframe["rsi"] > self.htf_rsi_bull.value)
).astype(int)
# Sideways: ADX low
dataframe["trend_sideways"] = (dataframe["adx"] < 20).astype(int)
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# 5m indicators
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
# Z-score mean reversion
dataframe["sma_80"] = ta.SMA(dataframe, timeperiod=80)
dataframe["std_80"] = dataframe["close"].rolling(window=80).std()
dataframe["zscore"] = (dataframe["close"] - dataframe["sma_80"]) / dataframe["std_80"]
# Volume spike
dataframe["volume_mean"] = dataframe["volume"].rolling(window=20).mean()
# MACD
macd = ta.MACD(dataframe)
dataframe["macd"] = macd["macd"]
dataframe["macdsignal"] = macd["macdsignal"]
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# 1H says sideways or uptrend AND 5m shows oversold Z-score
entry = (
# Higher timeframe filter: either sideways or uptrend on 1H
(
(dataframe["trend_up_1h"] == 1) |
(dataframe["trend_sideways_1h"] == 1)
) &
# 5m: mean reversion signal
(dataframe["zscore"] < self.entry_zscore.value) &
(dataframe["rsi"] < self.entry_rsi_buy.value) &
(dataframe["volume"] > self.entry_volume.value * dataframe["volume_mean"]) &
(dataframe["std_80"] > 0) &
(dataframe["volume"] > 0)
)
dataframe.loc[entry, "enter_long"] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
exit_signal = (
(dataframe["zscore"] > self.exit_zscore.value) &
(dataframe["rsi"] > self.exit_rsi_sell.value)
)
dataframe.loc[exit_signal, "exit_long"] = 1
return dataframe