Timeframe
15m
Direction
Long & Short
Stoploss
-2.5%
Trailing Stop
Yes
ROI
0m: 8.0%, 480m: 5.0%, 1440m: 3.0%, 4320m: 0.0%
Interface Version
3
Startup Candles
200
Indicators
8
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
Hunt Strategy Suite — Multiple variants for backtesting
Target: Sharpe > 1, Annual > 30%, beat BTC benchmark
"""
from pandas import DataFrame
import talib.abstract as ta
import numpy as np
from freqtrade.strategy import IStrategy
# ============================================================
# V1: Base Winner15m (benchmark) - 4-factor confirmation
# ============================================================
class HuntV1_Winner15m(IStrategy):
INTERFACE_VERSION = 3
timeframe = '15m'
can_short = True
stoploss = -0.025
trailing_stop = True
trailing_stop_positive = 0.005
trailing_stop_positive_offset = 0.018
trailing_only_offset_is_reached = True
minimal_roi = {"0": 0.08, "480": 0.05, "1440": 0.03, "4320": 0}
startup_candle_count = 200
process_only_new_candles = True
use_exit_signal = False
def populate_indicators(self, d, m):
d['e10'] = ta.EMA(d, timeperiod=10)
d['e30'] = ta.EMA(d, timeperiod=30)
macd = ta.MACD(d, fastperiod=12, slowperiod=26, signalperiod=9)
d['md'] = macd['macd']
d['ms'] = macd['macdsignal']
d['mom'] = ta.ROC(d, timeperiod=3)
d['adx'] = ta.ADX(d, timeperiod=14)
d['vr'] = d['volume'] / ta.SMA(d['volume'], timeperiod=20)
return d
def populate_entry_trend(self, d, m):
d.loc[(d['e10'] > d['e30']) & (d['md'] > d['ms']) & (d['mom'] > 0.1) & (d['adx'] > 18) & (d['vr'] > 1.0) & (d['volume'] > 0), ['enter_long', 'enter_tag']] = (1, 'L')
d.loc[(d['e10'] < d['e30']) & (d['md'] < d['ms']) & (d['mom'] < -0.1) & (d['adx'] > 18) & (d['vr'] > 1.0) & (d['volume'] > 0), ['enter_short', 'enter_tag']] = (1, 'S')
return d
def populate_exit_trend(self, d, m): return d
# ============================================================
# V2: Stricter entry - ADX>22, ROC>0.2, tighter stops
# ============================================================
class HuntV2_Strict(IStrategy):
INTERFACE_VERSION = 3
timeframe = '15m'
can_short = True
stoploss = -0.02
trailing_stop = True
trailing_stop_positive = 0.003
trailing_stop_positive_offset = 0.015
trailing_only_offset_is_reached = True
minimal_roi = {"0": 0.06, "240": 0.04, "960": 0.02, "2880": 0}
startup_candle_count = 200
process_only_new_candles = True
use_exit_signal = False
def populate_indicators(self, d, m):
d['e10'] = ta.EMA(d, timeperiod=10)
d['e30'] = ta.EMA(d, timeperiod=30)
macd = ta.MACD(d, fastperiod=12, slowperiod=26, signalperiod=9)
d['md'] = macd['macd']
d['ms'] = macd['macdsignal']
d['mom'] = ta.ROC(d, timeperiod=3)
d['adx'] = ta.ADX(d, timeperiod=14)
d['vr'] = d['volume'] / ta.SMA(d['volume'], timeperiod=20)
# RSI filter for overbought/oversold
d['rsi'] = ta.RSI(d, timeperiod=14)
return d
def populate_entry_trend(self, d, m):
d.loc[(d['e10'] > d['e30']) & (d['md'] > d['ms']) & (d['mom'] > 0.2) & (d['adx'] > 22) & (d['vr'] > 1.2) & (d['rsi'] < 70) & (d['volume'] > 0), ['enter_long', 'enter_tag']] = (1, 'L')
d.loc[(d['e10'] < d['e30']) & (d['md'] < d['ms']) & (d['mom'] < -0.2) & (d['adx'] > 22) & (d['vr'] > 1.2) & (d['rsi'] > 30) & (d['volume'] > 0), ['enter_short', 'enter_tag']] = (1, 'S')
return d
def populate_exit_trend(self, d, m): return d
# ============================================================
# V3: Aggressive - EMA5/20, loose entry, tight stop
# ============================================================
class HuntV3_Aggressive(IStrategy):
INTERFACE_VERSION = 3
timeframe = '15m'
can_short = True
stoploss = -0.015
trailing_stop = True
trailing_stop_positive = 0.003
trailing_stop_positive_offset = 0.008
trailing_only_offset_is_reached = True
minimal_roi = {"0": 0.04, "120": 0.025, "480": 0.015, "1440": 0}
startup_candle_count = 200
process_only_new_candles = True
use_exit_signal = False
def populate_indicators(self, d, m):
d['e5'] = ta.EMA(d, timeperiod=5)
d['e20'] = ta.EMA(d, timeperiod=20)
macd = ta.MACD(d, fastperiod=8, slowperiod=21, signalperiod=5)
d['md'] = macd['macd']
d['ms'] = macd['macdsignal']
d['mom'] = ta.ROC(d, timeperiod=2)
d['adx'] = ta.ADX(d, timeperiod=10)
d['vr'] = d['volume'] / ta.SMA(d['volume'], timeperiod=10)
return d
def populate_entry_trend(self, d, m):
d.loc[(d['e5'] > d['e20']) & (d['md'] > d['ms']) & (d['mom'] > 0.05) & (d['adx'] > 14) & (d['vr'] > 0.7) & (d['volume'] > 0), ['enter_long', 'enter_tag']] = (1, 'L')
d.loc[(d['e5'] < d['e20']) & (d['md'] < d['ms']) & (d['mom'] < -0.05) & (d['adx'] > 14) & (d['vr'] > 0.7) & (d['volume'] > 0), ['enter_short', 'enter_tag']] = (1, 'S')
return d
def populate_exit_trend(self, d, m): return d
# ============================================================
# V4: Mean Reversion - BB squeeze + RSI extreme
# ============================================================
class HuntV4_MeanRev(IStrategy):
INTERFACE_VERSION = 3
timeframe = '15m'
can_short = True
stoploss = -0.015
trailing_stop = True
trailing_stop_positive = 0.004
trailing_stop_positive_offset = 0.010
trailing_only_offset_is_reached = True
minimal_roi = {"0": 0.03, "240": 0.02, "720": 0.01, "1440": 0}
startup_candle_count = 200
process_only_new_candles = True
use_exit_signal = False
def populate_indicators(self, d, m):
bb = ta.BBANDS(d, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
d['bb_lower'] = bb['lowerband']
d['bb_upper'] = bb['upperband']
d['bb_mid'] = bb['middleband']
d['bb_width'] = (d['bb_upper'] - d['bb_lower']) / d['bb_mid']
d['rsi'] = ta.RSI(d, timeperiod=14)
d['vr'] = d['volume'] / ta.SMA(d['volume'], timeperiod=20)
d['atr'] = ta.ATR(d, timeperiod=14)
d['atr_pct'] = d['atr'] / d['close'] * 100
return d
def populate_entry_trend(self, d, m):
# Long: price near lower BB, RSI oversold, BB squeezing (low vol)
d.loc[(d['close'] <= d['bb_lower'] * 1.005) & (d['rsi'] < 35) & (d['bb_width'] < 0.05) & (d['volume'] > 0), ['enter_long', 'enter_tag']] = (1, 'L_mr')
# Short: price near upper BB, RSI overbought, BB squeezing
d.loc[(d['close'] >= d['bb_upper'] * 0.995) & (d['rsi'] > 65) & (d['bb_width'] < 0.05) & (d['volume'] > 0), ['enter_short', 'enter_tag']] = (1, 'S_mr')
return d
def populate_exit_trend(self, d, m): return d
# ============================================================
# V5: Multi-TF breakout - 1h trend + 15m entry
# ============================================================
class HuntV5_Breakout(IStrategy):
INTERFACE_VERSION = 3
timeframe = '15m'
can_short = True
stoploss = -0.02
trailing_stop = True
trailing_stop_positive = 0.005
trailing_stop_positive_offset = 0.015
trailing_only_offset_is_reached = True
minimal_roi = {"0": 0.05, "480": 0.04, "1440": 0.02, "4320": 0}
startup_candle_count = 200
process_only_new_candles = True
use_exit_signal = False
def populate_indicators(self, d, m):
# Price channels
d['hh_20'] = d['high'].rolling(20).max()
d['ll_20'] = d['low'].rolling(20).min()
d['hh_50'] = d['high'].rolling(50).max()
d['ll_50'] = d['low'].rolling(50).min()
# EMAs
d['e20'] = ta.EMA(d, timeperiod=20)
d['e50'] = ta.EMA(d, timeperiod=50)
d['e200'] = ta.EMA(d, timeperiod=200)
# Volume
d['vr'] = d['volume'] / ta.SMA(d['volume'], timeperiod=20)
d['vol_50'] = d['volume'] / d['volume'].rolling(50).mean()
# ATR for volatility
d['atr'] = ta.ATR(d, timeperiod=14)
d['atr_ratio'] = d['atr'] / d['close']
return d
def populate_entry_trend(self, d, m):
# Long: breakout above 20-high, trend aligned, high volume
d.loc[(d['close'] > d['hh_20'].shift(1)) & (d['close'] > d['e50']) & (d['vol_50'] > 1.5) & (d['volume'] > 0), ['enter_long', 'enter_tag']] = (1, 'L_bo')
# Short: breakdown below 20-low, trend aligned, high volume
d.loc[(d['close'] < d['ll_20'].shift(1)) & (d['close'] < d['e50']) & (d['vol_50'] > 1.5) & (d['volume'] > 0), ['enter_short', 'enter_tag']] = (1, 'S_bo')
return d
def populate_exit_trend(self, d, m): return d
# ============================================================
# V6: Trend Following + ATR trailing
# ============================================================
class HuntV6_TrendATR(IStrategy):
INTERFACE_VERSION = 3
timeframe = '15m'
can_short = True
stoploss = -0.025
trailing_stop = True
trailing_stop_positive = 0.004
trailing_stop_positive_offset = 0.020
trailing_only_offset_is_reached = True
minimal_roi = {"0": 0.10, "960": 0.06, "2880": 0.03, "5760": 0}
startup_candle_count = 200
process_only_new_candles = True
use_exit_signal = False
def populate_indicators(self, d, m):
d['e20'] = ta.EMA(d, timeperiod=20)
d['e50'] = ta.EMA(d, timeperiod=50)
d['e200'] = ta.EMA(d, timeperiod=200)
macd = ta.MACD(d, fastperiod=12, slowperiod=26, signalperiod=9)
d['md'] = macd['macd']
d['ms'] = macd['macdsignal']
d['md_hist'] = d['md'] - d['ms']
d['adx'] = ta.ADX(d, timeperiod=14)
d['di_plus'] = ta.PLUS_DI(d, timeperiod=14)
d['di_minus'] = ta.MINUS_DI(d, timeperiod=14)
d['atr'] = ta.ATR(d, timeperiod=14)
d['vr'] = d['volume'] / ta.SMA(d['volume'], timeperiod=20)
return d
def populate_entry_trend(self, d, m):
# Long: strong trend, DI aligned, above EMA50
d.loc[(d['e20'] > d['e50']) & (d['close'] > d['e50']) & (d['md'] > d['ms']) & (d['adx'] > 20) & (d['di_plus'] > d['di_minus']) & (d['vr'] > 0.8) & (d['volume'] > 0), ['enter_long', 'enter_tag']] = (1, 'L_trend')
# Short: strong downtrend, DI aligned, below EMA50
d.loc[(d['e20'] < d['e50']) & (d['close'] < d['e50']) & (d['md'] < d['ms']) & (d['adx'] > 20) & (d['di_minus'] > d['di_plus']) & (d['vr'] > 0.8) & (d['volume'] > 0), ['enter_short', 'enter_tag']] = (1, 'S_trend')
return d
def populate_exit_trend(self, d, m): return d
# ============================================================
# V7: Hybrid - Momentum + Mean Reversion combo
# ============================================================
class HuntV7_Hybrid(IStrategy):
INTERFACE_VERSION = 3
timeframe = '15m'
can_short = True
stoploss = -0.02
trailing_stop = True
trailing_stop_positive = 0.004
trailing_stop_positive_offset = 0.012
trailing_only_offset_is_reached = True
minimal_roi = {"0": 0.05, "480": 0.035, "1440": 0.02, "4320": 0}
startup_candle_count = 200
process_only_new_candles = True
use_exit_signal = False
def populate_indicators(self, d, m):
d['e10'] = ta.EMA(d, timeperiod=10)
d['e30'] = ta.EMA(d, timeperiod=30)
macd = ta.MACD(d, fastperiod=12, slowperiod=26, signalperiod=9)
d['md'] = macd['macd']
d['ms'] = macd['macdsignal']
d['mom'] = ta.ROC(d, timeperiod=5)
d['adx'] = ta.ADX(d, timeperiod=14)
d['rsi'] = ta.RSI(d, timeperiod=14)
bb = ta.BBANDS(d, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
d['bb_lower'] = bb['lowerband']
d['bb_upper'] = bb['upperband']
d['vr'] = d['volume'] / ta.SMA(d['volume'], timeperiod=20)
d['atr'] = ta.ATR(d, timeperiod=14)
return d
def populate_entry_trend(self, d, m):
# Long: momentum entry (trend) OR mean-reversion entry (dip buy)
trend_long = (d['e10'] > d['e30']) & (d['md'] > d['ms']) & (d['mom'] > 0.15) & (d['adx'] > 16) & (d['vr'] > 0.9)
dip_long = (d['e10'] > d['e30']) & (d['close'] <= d['bb_lower'] * 1.01) & (d['rsi'] < 40) & (d['volume'] > 0)
d.loc[(trend_long | dip_long) & (d['volume'] > 0), ['enter_long', 'enter_tag']] = (1, 'L_hyb')
# Short: momentum entry (trend) OR mean-reversion entry (rip short)
trend_short = (d['e10'] < d['e30']) & (d['md'] < d['ms']) & (d['mom'] < -0.15) & (d['adx'] > 16) & (d['vr'] > 0.9)
rip_short = (d['e10'] < d['e30']) & (d['close'] >= d['bb_upper'] * 0.99) & (d['rsi'] > 60) & (d['volume'] > 0)
d.loc[(trend_short | rip_short) & (d['volume'] > 0), ['enter_short', 'enter_tag']] = (1, 'S_hyb')
return d
def populate_exit_trend(self, d, m): return d
# ============================================================
# V8: Dual Timeframe - 1h trend filter + 15m entry
# ============================================================
class HuntV8_DualTF(IStrategy):
INTERFACE_VERSION = 3
timeframe = '15m'
can_short = True
stoploss = -0.025
trailing_stop = True
trailing_stop_positive = 0.005
trailing_stop_positive_offset = 0.018
trailing_only_offset_is_reached = True
minimal_roi = {"0": 0.08, "480": 0.05, "1440": 0.03, "4320": 0}
startup_candle_count = 400
process_only_new_candles = True
use_exit_signal = False
def populate_indicators(self, d, m):
d['e10'] = ta.EMA(d, timeperiod=10)
d['e30'] = ta.EMA(d, timeperiod=30)
macd = ta.MACD(d, fastperiod=12, slowperiod=26, signalperiod=9)
d['md'] = macd['macd']
d['ms'] = macd['macdsignal']
d['mom'] = ta.ROC(d, timeperiod=3)
d['adx'] = ta.ADX(d, timeperiod=14)
d['vr'] = d['volume'] / ta.SMA(d['volume'], timeperiod=20)
# 1h equivalent using 4x 15m candles
d['close_1h'] = d['close'].rolling(4).mean()
d['e50_1h'] = ta.EMA(d['close_1h'], timeperiod=50)
# Higher timeframe trend filter
d['htf_trend'] = d['close_1h'] > d['e50_1h']
return d
def populate_entry_trend(self, d, m):
# Long: 15m signals + 1h trend aligned
d.loc[(d['e10'] > d['e30']) & (d['md'] > d['ms']) & (d['mom'] > 0.1) & (d['adx'] > 18) & (d['vr'] > 1.0) & (d['htf_trend']) & (d['volume'] > 0), ['enter_long', 'enter_tag']] = (1, 'L_dtf')
# Short: 15m signals + 1h trend aligned
d.loc[(d['e10'] < d['e30']) & (d['md'] < d['ms']) & (d['mom'] < -0.1) & (d['adx'] > 18) & (d['vr'] > 1.0) & (~d['htf_trend']) & (d['volume'] > 0), ['enter_short', 'enter_tag']] = (1, 'S_dtf')
return d
def populate_exit_trend(self, d, m): return d