A minimal Freqtrade strategy that filters entries through the Regime API.
Timeframe
1h
Direction
Long Only
Stoploss
-10.0%
Trailing Stop
No
ROI
0m: 10.0%, 240m: 5.0%, 720m: 2.0%, 1440m: 0.0%
Interface Version
3
Startup Candles
50
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
# =============================================================================
# Regime Filter — a drop-in Freqtrade strategy that only trades the right market
# =============================================================================
#
# What this does
# --------------
# Most bots lose money not on bad entries, but on trading the WRONG strategy for
# the current market regime — momentum plays during chop, longs into a bear leg.
# This strategy adds ONE gate on top of your existing entry logic: before any
# trade opens, it asks the Regime API what the market is doing right now
# (BULL / BEAR / CHOP, with a confidence score) and skips entries that don't
# match the regimes you allow.
#
# It is intentionally a thin, copy-paste-able filter. Keep your own indicators
# and entry signals — this just stops them from firing in the wrong regime.
#
# How to use
# ----------
# 1. Drop this file in your `user_data/strategies/` folder.
# 2. Get a free key at https://getregime.com (Free tier covers BTC/ETH).
# 3. Set it in your environment (never hard-code a key):
# export REGIME_API_KEY="your_key_here"
# 4. Replace the placeholder entry signal in `populate_entry_trend()` with your
# own, or subclass this strategy and override that one method.
# 5. Run as usual: `freqtrade trade --strategy RegimeFilterStrategy`
#
# Tune it
# -------
# - ALLOWED_REGIMES which regimes may open trades (default: bull + chop)
# - MIN_CONFIDENCE skip low-confidence reads, 0-100 scale (default: 55)
# - REGIME_FAIL_MODE 'allow' (default) keeps trading if the API is
# unreachable so your bot is never bricked; 'block'
# is the conservative choice — pick what fits your risk.
# - REGIME_CACHE_SECONDS how long to reuse a regime read (default: 300s).
#
# Disclaimer
# ----------
# This file and the Regime API are informational tools, not financial advice.
# Market regime classification is probabilistic, not a guarantee. Backtest,
# paper-trade, and size your own risk. You are responsible for your own trades.
#
# Docs: https://getregime.com/freqtrade · npm: getregime · @getregime
# Source + ⭐: https://github.com/Thordersonjg/freqtrade-regime-filter
# Found this useful? Star the repo and share it — it helps other traders find it.
# =============================================================================
import logging
import os
import time
from typing import Optional
import requests
from pandas import DataFrame
from freqtrade.strategy import IStrategy
logger = logging.getLogger(__name__)
class RegimeFilterStrategy(IStrategy):
"""
A minimal Freqtrade strategy that filters entries through the Regime API.
Subclass this and override `populate_indicators` / `populate_entry_trend`
with your own signals — the regime gate in `confirm_trade_entry` carries over.
"""
# ---- Freqtrade required attributes (override with your own values) -------
INTERFACE_VERSION = 3
timeframe = "1h"
can_short = False
# Conservative defaults — replace with your tuned values.
minimal_roi = {"0": 0.10, "240": 0.05, "720": 0.02, "1440": 0}
stoploss = -0.10
trailing_stop = False
startup_candle_count = 50
# ---- Regime filter configuration ----------------------------------------
# Which regimes are allowed to open a trade. Long-only bots usually want
# bull + chop and want to sit out bear legs. A short-enabled bot might allow
# 'bear' here and flip direction in its own logic.
ALLOWED_REGIMES = {"bull", "chop"}
# Ignore reads the engine isn't sure about (0-100 scale). 0 disables the gate.
MIN_CONFIDENCE = 55
# 'allow' = if the API is unreachable, let trades through (bot never bricks).
# 'block' = if the API is unreachable, hold all entries (conservative).
REGIME_FAIL_MODE = os.environ.get("REGIME_FAIL_MODE", "allow")
# Reuse a regime read for this many seconds to stay well within rate limits.
REGIME_CACHE_SECONDS = 300
# The Freqtrade-optimized endpoint returns { regime, confidence, action, ... }.
_api_url = "https://getregime.com/api/v1/freqtrade/regime"
_regime_cache: dict = {} # { "value": str, "confidence": int, "ts": float }
_delay_notice_shown = False # one-shot: warn once if the key is on delayed data
# -------------------------------------------------------------------------
# Your signals live here. This placeholder enters on a simple SMA cross so
# the file runs out-of-the-box — swap it for your real edge.
# -------------------------------------------------------------------------
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["sma_fast"] = dataframe["close"].rolling(window=20).mean()
dataframe["sma_slow"] = dataframe["close"].rolling(window=50).mean()
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(dataframe["sma_fast"] > dataframe["sma_slow"])
& (dataframe["volume"] > 0),
"enter_long",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(dataframe["sma_fast"] < dataframe["sma_slow"]),
"exit_long",
] = 1
return dataframe
# -------------------------------------------------------------------------
# The regime gate. confirm_trade_entry runs once, at trade time, on the
# live bot — the right place for an external call (populate_* is vectorized
# over history and must stay pure/fast, so we never call the API there).
# -------------------------------------------------------------------------
def confirm_trade_entry(
self,
pair: str,
order_type: str,
amount: float,
rate: float,
time_in_force: str,
current_time,
entry_tag: Optional[str],
side: str,
**kwargs,
) -> bool:
regime = self._get_regime()
if regime is None:
allowed = self.REGIME_FAIL_MODE != "block"
logger.warning(
"Regime unavailable for %s — %s entry (fail mode: %s)",
pair,
"allowing" if allowed else "blocking",
self.REGIME_FAIL_MODE,
)
return allowed
value = regime.get("value")
confidence = int(regime.get("confidence", 0))
if self.MIN_CONFIDENCE and confidence < self.MIN_CONFIDENCE:
logger.info(
"Skip %s entry — regime '%s' confidence %d%% < %d%% threshold",
pair, value, confidence, self.MIN_CONFIDENCE,
)
return False
if value not in self.ALLOWED_REGIMES:
logger.info(
"Skip %s entry — regime '%s' (%d%%) not in allowed %s",
pair, value, confidence, sorted(self.ALLOWED_REGIMES),
)
return False
logger.info(
"Allow %s entry — regime '%s' (%d%% confidence)", pair, value, confidence
)
return True
# -------------------------------------------------------------------------
# Regime fetch with a short cache. One read covers every pair for the cache
# window, so a busy bot makes a handful of calls per hour, not thousands.
# -------------------------------------------------------------------------
def _get_regime(self) -> Optional[dict]:
cache = self._regime_cache
if cache and (time.time() - cache.get("ts", 0)) < self.REGIME_CACHE_SECONDS:
return cache
api_key = os.environ.get("REGIME_API_KEY")
if not api_key:
logger.error(
"REGIME_API_KEY is not set — get a free key at https://getregime.com "
"and `export REGIME_API_KEY=...`"
)
return None
try:
resp = requests.get(
self._api_url,
headers={"Authorization": f"Bearer {api_key}"},
timeout=5,
)
resp.raise_for_status()
data = resp.json()
# The API returns confidence on a 0-1 scale (e.g. 0.72). Normalize to
# a 0-100 scale so MIN_CONFIDENCE reads as a percentage. Tolerate a
# value already in 0-100 in case the API shape ever changes.
raw_conf = float(data.get("confidence", 0) or 0)
confidence = raw_conf * 100 if raw_conf <= 1 else raw_conf
regime = {
"value": str(data.get("regime", "")).lower(),
"confidence": int(round(confidence)),
"ts": time.time(),
}
# Free-tier keys serve delayed data — your bot is gating live entries
# on a stale regime. Surface it once so it's a conscious choice, not a
# silent handicap. (The API marks this with _delayed / _delay_minutes.)
if data.get("_delayed") and not RegimeFilterStrategy._delay_notice_shown:
RegimeFilterStrategy._delay_notice_shown = True
logger.warning(
"Regime data is %s-min delayed (free tier) — entries are gated "
"on a stale read. Real-time (Pro, $49/mo) sharpens entry timing: "
"https://getregime.com/pricing",
int(data.get("_delay_minutes", 15)),
)
self._regime_cache = regime
return regime
except requests.RequestException as err:
logger.warning("Regime API request failed: %s", err)
return None
except (ValueError, KeyError) as err:
logger.warning("Regime API returned an unexpected payload: %s", err)
return None