Timeframe
1h
Direction
Long Only
Stoploss
-10.0%
Trailing Stop
No
ROI
0m: 10000.0%
Interface Version
3
Startup Candles
N/A
Indicators
1
freqtrade/freqtrade-strategies
this is an example class, implementing a PSAR based trailing stop loss you are supposed to take the `custom_stoploss()` and `populate_indicators()` parts and adapt it to your own strategy
freqtrade/freqtrade-strategies
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
HmmRegime4 — 4-state Gaussian HMM regime filter on BTC 1h bars.
Replaces the SMA slope gate in SmaRegime180 with a probabilistic regime
posterior from hmmlearn. Hypothesis: a 4-state HMM (low-vol bull, high-vol
bull, low-vol bear, high-vol bear) produces more adaptive regime labels than
a fixed SMA window, improving win rate from SmaRegime180's ~22% toward 40%+.
Features fed to the HMM:
- rolling 24-bar log return (price momentum)
- log normalised volume (participation / flow)
Entry: P(any bull state) crosses above BULL_THRESHOLD (0.65).
Exit: P(any bull state) falls below EXIT_THRESHOLD (0.45).
NOTE — look-ahead caveat: populate_indicators fits HMM on the full dataset
at once. This is valid for an initial comparison vs SmaRegime180 (which was
also fit on the same window). A production-grade implementation would use a
rolling walk-forward refit; that's a follow-up once we know whether HMM
signal quality justifies the added complexity.
Requires: pip install hmmlearn
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy
try:
from hmmlearn.hmm import GaussianHMM
_HMM_AVAILABLE = True
except ImportError:
_HMM_AVAILABLE = False
RETURN_WINDOW = 24 # rolling lookback for log return feature (bars = hours on 1h TF)
N_COMPONENTS = 4 # {low-vol bull, high-vol bull, low-vol bear, high-vol bear}
BULL_THRESHOLD = 0.65 # P(bull) to trigger entry
EXIT_THRESHOLD = 0.45 # P(bull) below which position is closed
MIN_FIT_BARS = 500 # minimum history before fitting; shorter series yield poor HMMs
class HmmRegime4(IStrategy):
INTERFACE_VERSION = 3
can_short = False
timeframe = "1h"
startup_candle_count = MIN_FIT_BARS + RETURN_WINDOW
minimal_roi = {"0": 100} # exit on regime signal only
stoploss = -0.10
trailing_stop = False
process_only_new_candles = True
use_exit_signal = True
exit_profit_only = False
ignore_roi_if_entry_signal = False
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
if not _HMM_AVAILABLE:
raise ImportError(
"hmmlearn is required for HmmRegime4. "
"Activate the freqtrade venv and run: pip install hmmlearn"
)
log_return = np.log(
dataframe["close"] / dataframe["close"].shift(RETURN_WINDOW)
)
# Normalise volume to z-score within the available window to reduce
# scale sensitivity; use log first to symmetrise the distribution.
log_vol = np.log(dataframe["volume"].clip(lower=1e-9))
log_vol_z = (log_vol - log_vol.mean()) / max(log_vol.std(), 1e-9)
dataframe["_log_return"] = log_return
dataframe["_log_vol_z"] = log_vol_z
valid = dataframe[["_log_return", "_log_vol_z"]].notna().all(axis=1)
dataframe["bull_prob"] = np.nan
if valid.sum() < MIN_FIT_BARS:
return dataframe
X = dataframe.loc[valid, ["_log_return", "_log_vol_z"]].values
model = GaussianHMM(
n_components=N_COMPONENTS,
covariance_type="full",
n_iter=200,
random_state=42,
)
try:
model.fit(X)
except Exception:
# Some coins produce non-PD covariance under "full". Leave bull_prob
# NaN so no entries fire on this pair.
return dataframe
# Identify bull states by positive mean log return.
bull_states = [i for i in range(N_COMPONENTS) if model.means_[i, 0] > 0]
if not bull_states:
# Fallback: treat the single highest-mean state as bull.
bull_states = [int(np.argmax(model.means_[:, 0]))]
posteriors = model.predict_proba(X) # shape (n_valid, N_COMPONENTS)
bull_prob = posteriors[:, bull_states].sum(axis=1)
dataframe.loc[valid, "bull_prob"] = bull_prob
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Enter on the bar where bull_prob crosses above BULL_THRESHOLD.
dataframe.loc[
(dataframe["bull_prob"] >= BULL_THRESHOLD)
& (dataframe["bull_prob"].shift(1) < BULL_THRESHOLD),
"enter_long",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Exit whenever bull_prob drops below EXIT_THRESHOLD.
dataframe.loc[
dataframe["bull_prob"] < EXIT_THRESHOLD,
"exit_long",
] = 1
return dataframe