# source: https://raw.githubusercontent.com/jcd1991/market-behavior-lab/41fe7ec83ee23f00dc447cfd0d749065d68738a5/user_data/strategies/RiskManagedCrossSectionalTrend.py
# directory_url: https://github.com/jcd1991/market-behavior-lab/blob/main/user_data/strategies/
# User: jcd1991
# Repository: market-behavior-lab
# --------------------"""Cross-sectional momentum with volatility-aware position sizing."""

from __future__ import annotations

import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy
from research_strategy_helpers import execution_cost_floor, quote_volume_features


class Github_jcd1991_market_behavior_lab__RiskManagedCrossSectionalTrend__20260925_031923(IStrategy):
    INTERFACE_VERSION = 3
    timeframe = "1h"
    can_short = True
    process_only_new_candles = True
    startup_candle_count = 800
    minimal_roi = {"0": 100.0}
    stoploss = -0.20

    buy_rct_momentum_bars = IntParameter(72, 240, default=144, space="buy", optimize=True)
    buy_rct_sharpe_bars = IntParameter(336, 960, default=720, space="buy", optimize=True)
    buy_rct_long_rank = DecimalParameter(0.60, 0.95, default=0.75, decimals=2, space="buy", optimize=True)
    buy_rct_short_rank = DecimalParameter(0.05, 0.40, default=0.25, decimals=2, space="buy", optimize=True)
    buy_rct_target_vol = DecimalParameter(0.10, 0.60, default=0.25, decimals=2, space="buy", optimize=True)
    buy_rct_min_edge = DecimalParameter(0.001, 0.050, default=0.005, decimals=3, space="buy", optimize=True)
    buy_rct_min_liq_ratio = DecimalParameter(0.25, 2.00, default=0.50, decimals=2, space="buy", optimize=True)
    buy_rct_min_quote_volume = IntParameter(10_000, 10_000_000, default=100_000, space="buy", optimize=False)

    def _universe(self, metric: str) -> pd.DataFrame:
        values = {}
        pairs = list(self.dp.current_whitelist() or []) if self.dp else []
        for pair in pairs:
            try:
                frame = self.dp.get_pair_dataframe(pair, self.timeframe)
            except Exception:
                continue
            if frame is None or frame.empty:
                continue
            dates = pd.to_datetime(frame["date"], utc=True)
            close = pd.to_numeric(frame["close"], errors="coerce")
            returns = close.pct_change()
            if metric == "momentum":
                values[pair] = pd.Series(close.pct_change(int(self.buy_rct_momentum_bars.value)).to_numpy(), index=dates)
            else:
                bars = int(self.buy_rct_sharpe_bars.value)
                values[pair] = pd.Series((returns.rolling(bars, min_periods=bars // 2).mean() / returns.rolling(bars, min_periods=bars // 2).std()).to_numpy(), index=dates)
        return pd.DataFrame(values).sort_index()

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        close = pd.to_numeric(dataframe["close"], errors="coerce")
        dates = pd.to_datetime(dataframe["date"], utc=True)
        momentum = close.pct_change(int(self.buy_rct_momentum_bars.value))
        returns = close.pct_change()
        sharpe_bars = int(self.buy_rct_sharpe_bars.value)
        sharpe = returns.rolling(sharpe_bars, min_periods=sharpe_bars // 2).mean() / returns.rolling(sharpe_bars, min_periods=sharpe_bars // 2).std()
        vol = returns.rolling(48, min_periods=24).std() * np.sqrt(24 * 365)
        universe_momentum = self._universe("momentum")
        universe_sharpe = self._universe("sharpe")
        if metadata["pair"] in universe_momentum:
            dataframe["rct_momentum_rank"] = universe_momentum.rank(axis=1, pct=True)[metadata["pair"]].reindex(dates, method="ffill").to_numpy()
        else:
            dataframe["rct_momentum_rank"] = np.nan
        if metadata["pair"] in universe_sharpe:
            dataframe["rct_sharpe_rank"] = universe_sharpe.rank(axis=1, pct=True)[metadata["pair"]].reindex(dates, method="ffill").to_numpy()
        else:
            dataframe["rct_sharpe_rank"] = np.nan
        dataframe["rct_momentum"] = momentum
        dataframe["rct_sharpe"] = sharpe
        dataframe["rct_vol"] = vol
        dataframe["rct_scale"] = (float(self.buy_rct_target_vol.value) / vol.replace(0, np.nan)).clip(0.20, 1.25).fillna(0.20)
        dataframe = quote_volume_features(dataframe, "rct")
        dataframe["rct_cost_floor"] = execution_cost_floor(self.config)
        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        liquid = (
            dataframe["rct_quote_volume"].ge(float(self.buy_rct_min_quote_volume.value))
            & dataframe["rct_volume_ratio"].ge(float(self.buy_rct_min_liq_ratio.value))
        )
        edge = dataframe["rct_momentum"].abs().ge(
            dataframe["rct_cost_floor"] + float(self.buy_rct_min_edge.value)
        )
        long = (dataframe["rct_momentum_rank"] >= float(self.buy_rct_long_rank.value)) & (dataframe["rct_sharpe"] > 0) & liquid & edge
        short = (dataframe["rct_momentum_rank"] <= float(self.buy_rct_short_rank.value)) & (dataframe["rct_sharpe"] < 0) & liquid & edge
        dataframe["enter_long"] = long.astype(int)
        dataframe["enter_short"] = short.astype(int)
        dataframe["enter_tag"] = np.where(long, "rct_top_rank", np.where(short, "rct_bottom_rank", ""))
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["exit_long"] = ((dataframe["rct_momentum_rank"] < 0.50) | (dataframe["rct_sharpe"] <= 0)).astype(int)
        dataframe["exit_short"] = ((dataframe["rct_momentum_rank"] > 0.50) | (dataframe["rct_sharpe"] >= 0)).astype(int)
        dataframe["exit_tag"] = "rct_rank_reversal"
        return dataframe

    def custom_stake_amount(self, pair, current_time, current_rate, proposed_stake, min_stake, max_stake, leverage, entry_tag, side, **kwargs):
        try:
            frame, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
            scale = float(frame.iloc[-1].get("rct_scale", 0.20)) if frame is not None and not frame.empty else 0.20
        except Exception:
            scale = 0.20
        return min(float(max_stake), max(float(min_stake or 0), float(proposed_stake) * scale))


class Github_jcd1991_market_behavior_lab__RiskManagedCrossSectionalTrend__20260925_031923Spot(Github_jcd1991_market_behavior_lab__RiskManagedCrossSectionalTrend__20260925_031923):
    """Long-only spot validation variant; short ranking is not silently used."""

    can_short = False

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe = super().populate_entry_trend(dataframe, metadata)
        dataframe["enter_short"] = 0
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe = super().populate_exit_trend(dataframe, metadata)
        dataframe["exit_short"] = 0
        return dataframe
