# =============================================================================
#  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
# =============================================================================

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 }

    # -------------------------------------------------------------------------
    #  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(),
            }
            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
