# source: https://raw.githubusercontent.com/jcd1991/market-behavior-lab/16a04bd8b1004e90b9b17b556429a2eb730d96da/user_data/strategies/CryptoMomentumRotation.py
# directory_url: https://github.com/jcd1991/market-behavior-lab/blob/main/user_data/strategies/
# User: jcd1991
# Repository: market-behavior-lab
# --------------------"""Independent cross-sectional momentum research lane.

This strategy intentionally does not use the regime router.  It ranks each
pair against the contemporaneous universe median and trades only on scheduled
UTC rebalance bars.
"""

from __future__ import annotations

import numpy as np
import pandas as pd
import talib.abstract as ta
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy
from pandas import DataFrame

try:
    from user_data.strategies.research_strategy_helpers import scheduled_bars, utc_series
except ModuleNotFoundError:  # Direct strategy-module imports in focused tests.
    from research_strategy_helpers import scheduled_bars, utc_series


class Github_jcd1991_market_behavior_lab__CryptoMomentumRotation__20260921_130836(IStrategy):
    INTERFACE_VERSION = 3
    timeframe = "1h"
    can_short = True
    process_only_new_candles = True
    startup_candle_count = 220
    stoploss = -0.12
    minimal_roi = {"0": 100.0}

    buy_mom_lookback = IntParameter(12, 240, default=72, space="buy", optimize=True)
    buy_mom_rebalance_hours = IntParameter(4, 48, default=24, space="buy", optimize=True)
    buy_mom_edge = DecimalParameter(0.005, 0.20, default=0.025, decimals=3, space="buy", optimize=True)
    buy_mom_atr_max = DecimalParameter(0.02, 0.30, default=0.12, decimals=3, space="buy", optimize=True)
    sell_mom_exit_edge = DecimalParameter(-0.02, 0.02, default=0.0, decimals=3, space="sell", optimize=True)

    def informative_pairs(self):
        return []

    def _universe_median(self, dataframe: DataFrame) -> pd.Series:
        try:
            pairs = list(self.dp.current_whitelist() or [])
        except Exception:
            pairs = []
        lookback = int(self.buy_mom_lookback.value)
        series: list[pd.Series] = []
        for pair in pairs:
            try:
                frame = self.dp.get_pair_dataframe(pair=pair, timeframe=self.timeframe)
            except Exception:
                frame = None
            if frame is None or frame.empty:
                continue
            dates = utc_series(frame["date"])
            close = pd.to_numeric(frame["close"], errors="coerce")
            series.append(pd.Series(close.pct_change(lookback).to_numpy(), index=dates))
        if not series:
            return pd.Series(np.nan, index=utc_series(dataframe["date"]))
        target = pd.DatetimeIndex(utc_series(dataframe["date"]).to_numpy())
        median = pd.concat(series, axis=1).median(axis=1).sort_index()
        return median.reindex(target, method="ffill").set_axis(dataframe.index)

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        lookback = int(self.buy_mom_lookback.value)
        close = pd.to_numeric(dataframe["close"], errors="coerce")
        dataframe["mom_return"] = close.pct_change(lookback)
        dataframe["mom_universe_median"] = self._universe_median(dataframe)
        dataframe["mom_edge"] = dataframe["mom_return"] - dataframe["mom_universe_median"]
        atr = pd.to_numeric(ta.ATR(dataframe, timeperiod=14), errors="coerce")
        dataframe["mom_atr_pct"] = atr / close.replace(0, np.nan)
        dataframe["mom_schedule"] = scheduled_bars(dataframe, int(self.buy_mom_rebalance_hours.value))
        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        edge = float(self.buy_mom_edge.value)
        valid = dataframe["mom_schedule"] & dataframe["mom_atr_pct"].le(float(self.buy_mom_atr_max.value))
        dataframe["enter_long"] = (valid & dataframe["mom_edge"].ge(edge)).astype(int)
        dataframe["enter_short"] = (valid & dataframe["mom_edge"].le(-edge)).astype(int)
        dataframe["enter_tag"] = np.where(dataframe["enter_long"], "mom_long", np.where(dataframe["enter_short"], "mom_short", ""))
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        threshold = float(self.sell_mom_exit_edge.value)
        dataframe["exit_long"] = dataframe["mom_edge"].le(threshold).astype(int)
        dataframe["exit_short"] = dataframe["mom_edge"].ge(-threshold).astype(int)
        dataframe["exit_tag"] = "momentum_edge_reversal"
        return dataframe
