"""Shadow Account generated signal engine.

This file is auto-generated; do not hand-edit. Regenerate via
``src.shadow_account.codegen.render_signal_engine``.
"""
from __future__ import annotations

import pandas as pd

SHADOW_ID = {{ shadow_id | py_literal }}
PROFITABLE_ROUNDTRIPS = {{ profitable_roundtrips | py_literal }}
DATE_RANGE = {{ date_range | py_literal }}
PREFERRED_MARKETS = {{ preferred_markets | py_literal }}

RULES = {{ rules | py_literal }}


def _market_of(code: str) -> str:
    """Infer a coarse market bucket from a symbol (matches backtest.runner)."""
    c = (code or "").upper()
    if c.endswith(".SH") or c.endswith(".SZ") or c.endswith(".BJ"):
        return "china_a"
    if c.endswith(".HK"):
        return "hk"
    if "-USDT" in c or "-USD" in c or "/" in c:
        return "crypto"
    if c and c[0].isalpha():
        return "us"
    return "other"


class SignalEngine:
    """Shadow Account signal engine (conditional entry, daily timeframe).

    For each code we pick the first rule whose market matches, then check
    per-bar entry conditions: hour window, plus optionally RSI range and
    prior-return range (read from the rule dict — absent keys skip the
    check). When all conditions pass we enter a position and hold for the
    rule's holding days. Non-matching codes yield a zero series.
    """

    @staticmethod
    def _compute_rsi(close: pd.Series, period: int = 14) -> pd.Series:
        """Causal Wilder-EWM RSI (mirrors ``compute_rsi`` in example_signal_engine.py:13)."""
        delta = close.diff()
        gain = delta.clip(lower=0)
        loss = (-delta).clip(lower=0)
        avg_gain = gain.ewm(alpha=1 / period, min_periods=period).mean()
        avg_loss = loss.ewm(alpha=1 / period, min_periods=period).mean()
        rs = avg_gain / avg_loss
        return 100 - 100 / (1 + rs)

    @staticmethod
    def _compute_prior_return(close: pd.Series, window: int = 5) -> pd.Series:
        """Trailing close-to-close return over *window* bars."""
        return close.pct_change(window)

    def generate(self, data_map: dict) -> dict:
        signals: dict[str, pd.Series] = {}
        for code, df in data_map.items():
            sig = pd.Series(0.0, index=df.index)
            rule = self._pick_rule(code)
            if rule is not None:
                close = df["close"]
                rsi = self._compute_rsi(close)
                prior_ret = self._compute_prior_return(close)
                sig = self._conditional_entry(
                    df.index, rsi, prior_ret, rule,
                )
            signals[code] = sig
        return signals

    def _pick_rule(self, code: str) -> dict | None:
        market = _market_of(code)
        for rule in RULES:
            if rule["market"] == market or rule["market"] == "other":
                return rule
        return RULES[0] if RULES else None

    @staticmethod
    def _conditional_entry(
        index: pd.Index,
        rsi: pd.Series,
        prior_ret: pd.Series,
        rule: dict,
    ) -> pd.Series:
        hold_days = max(1, int(rule["hold_days"]))
        hour_min = int(rule["entry_hour_min"])
        hour_max = int(rule["entry_hour_max"])
        weight = float(rule["weight"])

        rsi_min = rule.get("entry_rsi14_min")
        rsi_max = rule.get("entry_rsi14_max")
        ret_min = rule.get("prior_5d_return_min")
        ret_max = rule.get("prior_5d_return_max")

        values = [0.0] * len(index)
        remaining_hold = 0

        for i in range(len(index)):
            if remaining_hold > 0:
                values[i] = weight
                remaining_hold -= 1
                continue

            bar_hour = pd.Timestamp(index[i]).hour
            if bar_hour < hour_min or bar_hour > hour_max:
                continue

            if rsi_min is not None and rsi_max is not None:
                rsi_val = rsi.iloc[i]
                if pd.isna(rsi_val) or rsi_val < rsi_min or rsi_val > rsi_max:
                    continue

            if ret_min is not None and ret_max is not None:
                ret_val = prior_ret.iloc[i]
                if pd.isna(ret_val) or ret_val < ret_min or ret_val > ret_max:
                    continue

            values[i] = weight
            remaining_hold = hold_days - 1

        return pd.Series(values, index=index)
