"""
exit_step_lock_experimental.py

[2026-09-20] NOT wired into monitor.py's live loop -- backtest-only.
Replaces the ATR-distance-from-CURRENT-price hard stop (exit.py's Layer
1, `stop = price - atr * atr_multiplier_stop`) with a stop anchored to
the position's own entry price and its own peak, in two phases:

  1. Cent-for-cent trailing to breakeven. The stop opens a fixed
     `initial_distance_pct` below entry and rises dollar-for-dollar with
     every new high (a flat-percent trailing stop, same shape as
     exit_cents_grace_experimental.py's flat-cents version, just sized
     in percent-of-entry instead of cents) until it reaches entry price,
     where it caps -- the stop never trails ABOVE breakeven during this
     phase.

  2. Step-lock above breakeven. Once the peak has run far enough that
     the phase-1 stop would sit at breakeven (peak gain >=
     initial_distance_pct), the stop holds at breakeven until the peak
     reaches 2 * step_pct above entry, then jumps to 1 step below the
     peak's current step, then 2 steps back the next threshold, and so
     on -- i.e. it always locks the step BEFORE the one just reached,
     never the one the peak is currently in. Between step thresholds the
     stop does not move at all, which is deliberate: it's what keeps an
     ordinary mid-run dip from stopping out a real runner, at the cost
     of giving back everything gained since the last threshold if the
     stock reverses before crossing the next one.

  Optional `giveback_cap_pct`: once in the step-lock phase, also floors
  the stop at (peak - giveback_cap_pct% of entry), so a big run can
  never give back more than that fixed slice even while parked at a
  stale step. Off by default (None) -- the step-lock-only version scored
  higher on the one session this was tuned against (10 trades,
  2026-09-18: +28.1% vs +26.7% with a 4% cap), but the cap trades some
  upside for capping worst-case giveback in the un-stepped band, and
  that tradeoff is a call each backtest run should make explicitly, not
  a hardcoded default. Sensitivity was steep in one direction (5% cap
  scored +22.8%, 6% changed nothing) -- don't trust one cap value
  without rerunning at neighboring sizes.

Parameter defaults below are NOT tuned recommendations, just the values
that best matched the design's intent on the one 10-trade session this
was built against -- rerun sweep-style (varying initial_distance_pct,
step_pct, giveback_cap_pct) on any new session before trusting them.
"""

from dataclasses import dataclass, field

STATE_HOLD = "HOLD"
STATE_EXIT = "EXIT"

DEFAULT_CONFIG = {
    "initial_distance_pct": 1.5,   # D -- fixed %-of-entry distance for the cent-for-cent phase
    "step_pct": 5.0,               # size of each post-breakeven lock step, in % of entry
    "giveback_cap_pct": None,      # optional: stop never more than this %-of-entry below the peak
    "breakeven_tolerance_pct": 0.0,  # optional: floor sits this %-of-entry BELOW breakeven instead
                                      # of exactly at it, so ordinary noise doesn't re-trigger the stop
    "handoff_at_breakeven": False,   # once the peak clears initial_distance_pct, stop enforcing
                                      # anything here at all -- let exit.py's regular hard-stop +
                                      # soft-deterioration layers run the position from there. This is
                                      # the "only protect the opening leg" mode: this module's whole
                                      # job is bridging entry up to breakeven; past that it gets out of
                                      # the way instead of holding a flat floor forever.
}


@dataclass
class StepLockDecision:
    symbol: str
    state: str = STATE_HOLD
    should_exit: bool = False
    reason: str = ""
    metrics: dict = field(default_factory=dict)
    state_out: dict = field(default_factory=dict)


def _merge_cfg(cfg):
    return {**DEFAULT_CONFIG, **(cfg or {})}


def _compute_stop(entry_price: float, peak_price: float, cfg: dict) -> tuple[float, str]:
    """Returns (stop_price, phase_label)."""
    d_pct = cfg["initial_distance_pct"]
    step_pct = cfg["step_pct"]
    cap_pct = cfg["giveback_cap_pct"]
    tol_pct = cfg.get("breakeven_tolerance_pct") or 0.0
    breakeven_floor = entry_price * (1 - tol_pct / 100.0)

    peak_gain_pct = (peak_price - entry_price) / entry_price * 100.0

    if peak_gain_pct < d_pct:
        # Phase 1: cent-for-cent trailing, capped at the breakeven floor
        # (exactly entry price when tol_pct is 0).
        stop = min(breakeven_floor, peak_price - (d_pct / 100.0) * entry_price)
        return stop, "trailing_to_breakeven"

    if not step_pct or step_pct <= 0:
        # Phase 2 disabled: breakeven-guarantee only -- once the peak has
        # cleared d_pct, the stop parks at the breakeven floor permanently
        # and never trails any further, no matter how far price runs.
        return breakeven_floor, "breakeven_guarantee"

    # Phase 2: step-lock. n = how many whole steps the peak has cleared;
    # lock at (n - 1) steps above entry, so the step just reached stays
    # unlocked until the NEXT threshold clears it. The unstepped (n<2)
    # band uses the same breakeven floor/tolerance as phase 1.
    n = int(peak_gain_pct // step_pct)
    locked_steps = max(0, n - 1)
    stop = breakeven_floor if locked_steps == 0 else entry_price * (1 + locked_steps * step_pct / 100.0)

    if cap_pct is not None:
        cap_stop = peak_price - (cap_pct / 100.0) * entry_price
        if cap_stop > stop:
            stop = cap_stop
            return stop, f"step_lock+giveback_cap(step={locked_steps})"

    return stop, f"step_lock(step={locked_steps})"


def evaluate(symbol: str, bars: list, entry_price: float, state_in: dict, cfg: dict = None) -> StepLockDecision:
    """
    bars: same short rolling 1-min-bar window exit.py takes -- only the
        latest close is used, no indicators.
    state_in: caller-held state fed back every poll -- {"peak_price"}.
        Fresh {} at entry.
    No confirmation/health gate, same as exit.py's Layer 1 hard stop --
    pure capital protection, fires immediately once price touches the
    stop.
    """
    cfg = _merge_cfg(cfg)
    state_in = dict(state_in or {})

    if not bars:
        return StepLockDecision(symbol=symbol, reason="insufficient bar data",
                                 state_out=state_in or {"peak_price": entry_price})

    price = bars[-1]["c"]
    prior_peak = state_in.get("peak_price", entry_price)
    peak_price = max(prior_peak, price)

    if cfg["handoff_at_breakeven"]:
        peak_gain_pct = (peak_price - entry_price) / entry_price * 100.0
        if peak_gain_pct >= cfg["initial_distance_pct"]:
            return StepLockDecision(
                symbol=symbol, state=STATE_HOLD,
                reason=f"breakeven reached (peak +{peak_gain_pct:.2f}%) -- handed off to regular exit",
                metrics={"price": price, "peak_price": round(peak_price, 4), "phase": "handed_off"},
                state_out={"peak_price": peak_price})

    stop, phase = _compute_stop(entry_price, peak_price, cfg)
    metrics = {"price": price, "peak_price": round(peak_price, 4), "stop": round(stop, 4),
               "phase": phase, "peak_gain_pct": round((peak_price - entry_price) / entry_price * 100.0, 3)}

    if price <= stop:
        return StepLockDecision(
            symbol=symbol, state=STATE_EXIT, should_exit=True,
            reason=f"step-lock stop: price ${price:.4f} <= stop ${stop:.4f} ({phase})",
            metrics=metrics, state_out={"peak_price": peak_price})

    return StepLockDecision(
        symbol=symbol, state=STATE_HOLD,
        reason=f"above stop ${stop:.4f} ({phase})",
        metrics=metrics, state_out={"peak_price": peak_price})
