o
    ¬?°j©  ã                   @   s�   d Z ddlmZmZ dZdZddddd	d
œZeG dd„ dƒƒZdd„ Zde	de	de
dee	ef fdd„Zddedede	de
de
defdd„ZdS )aË	  
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.
é    )Ú	dataclassÚfieldÚHOLDÚEXITg      ø?g      @Nç        F)Úinitial_distance_pctÚstep_pctÚgiveback_cap_pctÚbreakeven_tolerance_pctÚhandoff_at_breakevenc                   @   s^   e Zd ZU eed< eZeed< dZeed< dZ	eed< e
ed�Zeed< e
ed�Zeed	< d
S )ÚStepLockDecisionÚsymbolÚstateFÚshould_exitÚ Úreason)Údefault_factoryÚmetricsÚ	state_outN)Ú__name__Ú
__module__Ú__qualname__ÚstrÚ__annotations__Ú
STATE_HOLDr   r   Úboolr   r   Údictr   r   © r   r   úexit_step_lock_experimental.pyr   C   s   
 r   c                 C   s   i t ¥| pi ¥S ©N)ÚDEFAULT_CONFIG)Úcfgr   r   r   Ú
_merge_cfgM   s   r"   Úentry_priceÚ
peak_pricer!   Úreturnc                 C   sþ   |d }|d }|d }|  d¡pd}| d|d   }||  |  d }||k r6t|||d |   ƒ}	|	dfS |r<|d	kr@|d
fS t|| ƒ}
td	|
d ƒ}|d	krS|n	| d|| d   }	|durw||d |   }||	krw|}	|	d|› d�fS |	d|› d�fS )z"Returns (stop_price, phase_label).r   r   r	   r
   r   é   ç      Y@Útrailing_to_breakevenr   Úbreakeven_guaranteeNzstep_lock+giveback_cap(step=ú)zstep_lock(step=)ÚgetÚminÚintÚmax)r#   r$   r!   Úd_pctr   Úcap_pctÚtol_pctÚbreakeven_floorÚpeak_gain_pctÚstopÚnÚlocked_stepsÚcap_stopr   r   r   Ú_compute_stopQ   s(    r8   r   ÚbarsÚstate_inc                 C   s>  t |ƒ}t|pi ƒ}|st| d|pd|id�S |d d }| d|¡}t||ƒ}|d rQ|| | d }||d krQt| td	|d
›d�|t|dƒddœd|id�S t|||ƒ\}	}
|t|dƒt|	dƒ|
t|| | d dƒdœ}||	krŒt| tdd|d›d|	d›d|
› d�|d|id�S t| td|	d›d|
› d�|d|id�S )at  
    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.
    zinsufficient bar datar$   )r   r   r   éÿÿÿÿÚcr   r'   r   zbreakeven reached (peak +z.2fz %) -- handed off to regular exité   Ú
handed_off)Úpricer$   Úphase)r   r   r   r   r   é   )r?   r$   r4   r@   r3   Tzstep-lock stop: price $z.4fz
 <= stop $z (r*   )r   r   r   r   r   r   zabove stop $)	r"   r   r   r+   r.   r   Úroundr8   Ú
STATE_EXIT)r   r9   r#   r:   r!   r?   Ú
prior_peakr$   r3   r4   r@   r   r   r   r   Úevaluatex   sB   

ÿ
üÿýýrE   r   )Ú__doc__Údataclassesr   r   r   rC   r    r   r"   Úfloatr   Útupler   r8   ÚlistrE   r   r   r   r   Ú<module>   s    .ú	"('