💬 Forum

HurstCycle3

🏆 League #1913 / 1922

jaredrsommer/freqtradestrategies/HurstCycle3 (2).py · ★22 · ⑂9 · first seen 2026-07-16 · repo updated 2026-05-18 · ⬇ 1 download

Basics mode: spot timeframe: 15m
Settings stoploss: -0.04 has minimal roi trailing
Indicators ATR SMA scipy talib
Concepts breakout trailing
Methods leverage
8 related strategies ( identical code, similar name)

Each tile is a different kind of check — from an instant code lint to full sandboxed backtests and forward tests on recent data. Not sure what a check actually proves? See the FAQ →

Source

Download Raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
from freqtrade.strategy import IStrategy
import pandas as pd
import numpy as np
import talib
from scipy.fft import fft
from typing import Dict, List
import math

class HurstCycle3(IStrategy):
    timeframe = '15m'
    minimal_roi = {"0": 0.05, "60": 0.03, "120": 0.01}
    stoploss = -0.04
    trailing_stop = True
    trailing_stop_positive = 0.015
    trailing_stop_positive_offset = 0.02

    # Hyperparameters to optimize
    base_cycle_period = 20
    envelope_factor_short = 1.2
    envelope_factor_long = 2.0
    filter_weights = [1, 2, 3, 2, 1]  # Symmetric weights for smoothing (p. 177)

    def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        # Spectral Analysis
        close = dataframe['close'].values
        n = min(len(close), 500)
        yf = fft(close[-n:])
        freq = np.fft.fftfreq(n)
        power = np.abs(yf) ** 2
        dominant_idx = np.argmax(power[1:n//2]) + 1
        dominant_period = int(1 / freq[dominant_idx]) if freq[dominant_idx] != 0 else self.base_cycle_period
        self.short_cycle = max(10, min(30, dominant_period))
        self.long_cycle = self.short_cycle * 2

        # Numerical Filter
        weights = np.array(self.filter_weights) / sum(self.filter_weights)
        filtered = np.convolve(close, weights, mode='valid')
        dataframe['filtered_close'] = pd.Series(filtered, index=dataframe.index[-len(filtered):])

        # FLDs
        half_short = math.ceil(self.short_cycle / 2)
        half_mid = math.ceil(self.short_cycle)
        half_long = math.ceil(self.long_cycle / 2)
        fld_short_base = dataframe['filtered_close'].rolling(window=self.short_cycle, center=True).mean()
        fld_mid_base = dataframe['filtered_close'].rolling(window=(self.short_cycle * 2), center=True).mean()
        fld_long_base = dataframe['filtered_close'].rolling(window=self.long_cycle, center=True).mean()
        dataframe['fld_short'] = fld_short_base.shift(half_short)
        dataframe['fld_mid'] = fld_mid_base.shift(half_mid)
        dataframe['fld_long'] = fld_long_base.shift(half_long)
        for fld in ['fld_short', 'fld_mid', 'fld_long']:
            last_valid_idx = dataframe[fld].last_valid_index()
            if last_valid_idx is not None:
                last_valid_row = dataframe.index.get_loc(last_valid_idx)
                if last_valid_row < len(dataframe) - 1 and last_valid_row > 0:
                    for i in range(last_valid_row + 1, len(dataframe)):
                        prev_slope = (dataframe[fld].iloc[last_valid_row] - 
                                      dataframe[fld].iloc[last_valid_row - 1])
                        dataframe[fld].iloc[i] = dataframe[fld].iloc[last_valid_row] + \
                                                 prev_slope * (i - last_valid_row)

        # # FLD with shift and extrapolation
        # half_short = int(self.short_cycle / 2)
        # half_long = int(self.long_cycle / 2)
        # fld_short_base = dataframe['filtered_close'].rolling(window=self.short_cycle, center=True).mean()
        # fld_long_base = dataframe['filtered_close'].rolling(window=self.long_cycle, center=True).mean()
        # dataframe['fld_short'] = fld_short_base.shift(-half_short)
        # dataframe['fld_long'] = fld_long_base.shift(-half_short)
        # for i in range(1, half_short + 1):
        #     if len(dataframe) > half_short + 1:  # Ensure enough data
        #         dataframe['fld_short'].iloc[-i] = dataframe['fld_short'].iloc[-half_short-1] + \
        #                                           (dataframe['fld_short'].iloc[-half_short-1] - dataframe['fld_short'].iloc[-half_short-2])
        #         dataframe['fld_long'].iloc[-i] = dataframe['fld_long'].iloc[-half_short-1] + \
        #                                          (dataframe['fld_long'].iloc[-half_short-1] - dataframe['fld_long'].iloc[-half_short-2])

        # [Rest of indicators: envelopes, VTLs, trend...]
        dataframe['sma_short'] = talib.SMA(dataframe['filtered_close'], timeperiod=self.short_cycle)
        dataframe['sma_long'] = talib.SMA(dataframe['filtered_close'], timeperiod=self.long_cycle)
        dataframe['atr_short'] = talib.ATR(dataframe['high'], dataframe['low'], dataframe['filtered_close'], timeperiod=self.short_cycle)
        dataframe['atr_long'] = talib.ATR(dataframe['high'], dataframe['low'], dataframe['filtered_close'], timeperiod=self.long_cycle)
        dataframe['upper_env_short'] = dataframe['sma_short'] + self.envelope_factor_short * dataframe['atr_short']
        dataframe['lower_env_short'] = dataframe['sma_short'] - self.envelope_factor_short * dataframe['atr_short']
        dataframe['upper_env_long'] = dataframe['sma_long'] + self.envelope_factor_long * dataframe['atr_long']
        dataframe['lower_env_long'] = dataframe['sma_long'] - self.envelope_factor_long * dataframe['atr_long']
        dataframe['trough'] = dataframe['filtered_close'].rolling(self.short_cycle).min()
        dataframe['crest'] = dataframe['filtered_close'].rolling(self.short_cycle).max()
        dataframe['is_trough'] = np.where(dataframe['filtered_close'] == dataframe['trough'], 1, 0)
        dataframe['is_crest'] = np.where(dataframe['filtered_close'] == dataframe['crest'], 1, 0)
        troughs = dataframe[dataframe['is_trough'] == 1].index
        crests = dataframe[dataframe['is_crest'] == 1].index
        if len(troughs) >= 2:
            t1, t2 = troughs[-2], troughs[-1]
            dataframe['vtl_up_slope'] = (dataframe['filtered_close'][t2] - dataframe['filtered_close'][t1]) / (t2 - t1)
        if len(crests) >= 2:
            c1, c2 = crests[-2], crests[-1]
            dataframe['vtl_down_slope'] = (dataframe['filtered_close'][c2] - dataframe['filtered_close'][c1]) / (c2 - c1)
        dataframe['trend'] = np.where(dataframe['filtered_close'] > dataframe['fld_long'], 1, -1)

        return dataframe
    def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        # Long Entry: Trough, below FLD, short envelope breach, VTL breakout
        dataframe.loc[
            # (dataframe['is_trough'] == 1) &
            (dataframe['filtered_close'] > dataframe['fld_short']) &
            (dataframe['filtered_close'].shift() < dataframe['fld_short'].shift()),
            # (dataframe['trend'] == 1),
            'enter_long'] = 1

        # # Short Entry: Crest, above FLD, short envelope breach, VTL breakout
        # dataframe.loc[
        #     (dataframe['is_crest'] == 1) &
        #     (dataframe['filtered_close'] > dataframe['fld_short']) &
        #     (dataframe['filtered_close'] > dataframe['upper_env_short']) &
        #     (dataframe['trend'] == -1) &
        #     (dataframe['vtl_down_slope'].notna() & (dataframe['vtl_down_slope'] < 0)),
        #     'enter_short'] = 1

        return dataframe

    def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        # Exit Long: Hit FLD or upper long envelope
        dataframe.loc[
            (dataframe['filtered_close'] < dataframe['fld_short']) &
            (dataframe['filtered_close'].shift() > dataframe['fld_short'].shift()),
            'exit_long'] = 1

        # # Exit Short: Hit FLD or lower long envelope
        # dataframe.loc[
        #     (dataframe['enter_short'].shift(1) == 1) &
        #     ((dataframe['filtered_close'] <= dataframe['fld_long']) | 
        #      (dataframe['filtered_close'] <= dataframe['lower_env_long'])),
        #     'exit_short'] = 1

        return dataframe

    def leverage(self, pair: str, current_time, current_rate: float, proposed_leverage: float, 
                 max_leverage: float, side: str, **kwargs) -> float:
        return 3.0