o
    ÕË¶jÒ/  ã                   @   sÌ   d Z ddlmZmZ ddlmZ ddlmZmZm	Z	m
Z
mZmZ dZdZddd	d
dddddœddddœ
ZeG dd„ dƒƒZdd„ Zdededefdd„Z		d#dedededededed edefd!d"„ZdS )$a
  
exit.py

Answers ONE question for an open position: should we still be in this?
Deliberately independent of smart_engine.py's entry logic -- per the
breakout_bot design doc's explicit rule, entry and exit must never
share one combined decision rule (smart_engine.py answers "should we
enter," this answers "should we remain").

Three layers, checked every poll:

  1. HARD stop -- always active, exits immediately, no confirmation
     needed. Pure capital protection. [BUGFIX 2026-09-22] Uses the
     position's own stop_price fixed at entry (smart_engine.py's Stage 3
     stop). It used to be recomputed every poll as current price minus
     ATR, which always sat below price and trailed it down -- it could
     never fire; 32 of 70 live trades (9/17-9/22) exited below their
     entry stop, $455 worse than exiting at it.

  2. [2026-09-17, redesigned 2026-09-18] Trade-flow-imbalance decline --
     the first of the two SIGNAL-based exits (checked right after the
     hard stop, ahead of the deterioration layer below). Originally
     checked once per completed 1-min bar; rebuilt after real-data
     testing (ABSI, 2026-09-17) showed the 1-min-bar version completely
     missed a real reversal -- the bar spanning ABSI's actual low read
     the MOST bullish imbalance of the whole window -- while it also
     fired falsely later on, during a recovery, on bar-to-bar noise with
     no price move behind it at all. A continuous, 5-second-cadence read
     of the SAME live rolling-window imbalance smart_engine.py's Stage 3
     uses (stream.py's get_trade_imbalance()) caught the real reversal
     within 5 seconds of the actual price low -- roughly as fast as the
     trend classifier itself, and far better than the 1-min-bar version.
     Now checks every poll (not gated behind price/trend going flat
     first -- that precondition would have excluded the one real catch
     found, since trend didn't reclassify until 5 seconds after the fast
     imbalance streak already completed). Fires immediately (no separate
     confirm-read wait -- the streak itself IS the confirmation) once
     imbalance_exit_confirm_reads consecutive polls have each read lower
     than the one before.

  3. SOFT "deterioration" exit -- requires a COMBINATION of signals
     (not just one) to agree, sustained for exit_confirm_reads
     consecutive polls, before it fires. This is the direct answer to
     the 2026-09-16 IIIV lesson: a single flat/quiet reading forced an
     exit there and caused a whipsaw (exit-then-rebuy-higher, twice) on
     a stock that was only pausing, not actually reversing. Requiring
     BOTH multiple agreeing signals AND sustained confirmation (not
     "any one blip") is what should tell a real breakdown apart from a
     stock that's just quiet for a bar or two.

Caller-held confirmation state per symbol, fed back in every call (same
pattern as smart_engine.py's persistence contract and sip_bot's
intraday_health.py confirm-reads hysteresis) -- now {"count": int,
"imbalance_decline_count": int, "imbalance_prev": float_or_None}.
é    )Ú	dataclassÚfield)Ú
get_config)ÚvwapÚ
vwap_slopeÚatrÚnormalized_slope_pctÚclassify_slopeÚis_lower_highs_lower_lowsÚHOLDÚEXITé   é   é   g333333ó?é   gš™™™™™©?g      Ð?)Úmin_flat_threshold_pctÚflat_threshold_atr_fractioné   é   )
Údeterioration_min_signalsÚdeterioration_signals_totalÚexit_confirm_readsÚatr_multiplier_stopÚ
atr_periodÚstructure_lookback_barsÚtrendÚimbalance_window_secondsÚimbalance_exit_confirm_readsÚdeterioration_trend_barsc                   @   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 )ÚExitDecisionÚsymbolÚstateFÚshould_exitÚ Úreason)Údefault_factoryÚmetricsÚconfirmationN)Ú__name__Ú
__module__Ú__qualname__ÚstrÚ__annotations__Ú
STATE_HOLDr!   r"   Úboolr$   r   Údictr&   r'   © r0   r0   ú/var/www/screener/trade/exit.pyr   R   s   
 r   c                 C   s:   i t ¥| ptƒ  di ¡¥}i t d ¥| di ¡¥|d< |S )NÚexitr   )ÚDEFAULT_CONFIGr   Úget)ÚcfgÚbaser0   r0   r1   Ú
_merge_cfg\   s   r7   Úconfirmation_stater5   Úreturnc                 C   sb   |  dd¡}|  d¡}| du rd|||dœS |duo| |k }|r$|d nd}||d k|| |dœS )	a˜  [2026-09-18] First signal-based exit gate, continuous cadence.
    trade_imbalance: stream.py's get_trade_imbalance(imbalance_window_seconds)
    live snapshot for this symbol -- the SAME rolling-window read
    smart_engine.py's Stage 3 uses, just read again every poll instead of
    once per bar. Compares this poll's reading directly against last
    poll's (confirmation_state["imbalance_prev"]), so the streak advances
    every ~poll_interval_seconds rather than every minute. A None reading
    (cold buffer / no trades in the window) holds the streak steady
    rather than resetting or advancing it -- fails open, doesn't punish a
    momentary data gap.Úimbalance_decline_countr   Úimbalance_prevNF)Ú	triggeredÚcountÚcur_imbalanceÚprev_imbalanceé   r   )r4   )Útrade_imbalancer8   r5   Úprior_countÚ	prior_imbÚ	decliningr=   r0   r0   r1   Ú_evaluate_imbalance_declineb   s   
ÿÿrE   Nr    ÚbarsÚentry_pricerA   Ú
stop_pricec                 C   s2  t |ƒ}|pi }t|||ƒ}|d |d dœ}t|ƒdk r)t| tdddi|¥d�S |d d	 }	t|ƒ}
t|ƒ}t|t|d
 t	dt|ƒd ƒƒd�}|	rQ||	 d nd}dd„ |D ƒ}t
|ƒ}t	|d d ||d d  ƒ}t||ƒ}t|t|d t|ƒƒd�}|}|	t|
dƒt|dƒt|dƒ|t|dƒ||dur�t|dƒnd|d |d |d dœ}|durÈ|	|krÈt| tdd|	d›d|d›�|ddi|¥d�S |d  rö|dk röt| tdd!|d › d"|d d#›d$|d d#›d%|d&›d'�	|dd|d d(œd�S |d  �r	d)|d › d*|d&›d+�|d,< |}| d-¡�r%||d-  d… }tt
|ƒ|ƒ}||d.< |	|
k |dk |d/k|g}t|ƒ}||d0 k �rRt| td1|› d2|d3 › d4�|ddi|¥d5�S | dd¡d }||d6 k �rt| td7|› d2|d3 › d8|› d2|d6 › d9�	|d|i|¥d5�S t| tdd:|› d2|d3 › d;|› d<�|ddi|¥d�S )=aý  
    bars: recent 1-min bars for this symbol, same short rolling window
        shape smart_engine.py takes.
    entry_price: this position's own entry price.
    stop_price: the position's stop fixed at entry -- Layer 1's hard stop.
        None disables Layer 1.
    confirmation_state: caller-held state for this symbol, fed back in
        every call (fresh {} = cold start, e.g. right after entry) --
        {"count": int, "imbalance_decline_count": int,
        "imbalance_prev": float_or_None}.
    trade_imbalance: stream.py's get_trade_imbalance(imbalance_window_seconds)
        live read for this symbol -- feeds the imbalance-decline gate
        below, checked every poll. None simply means that gate can't
        advance this poll (fails open).
    r=   r>   )r:   r;   r   zinsufficient bar datar   )r    r!   r$   r'   éÿÿÿÿÚcr   r   r@   )Úperiodg      Y@g        c                 S   s   g | ]}|d  ‘qS )rJ   r0   )Ú.0Úbr0   r0   r1   Ú
<listcomp>¡   s    zevaluate.<locals>.<listcomp>r   r   r   r   )Úlookbackr   Nr?   )Úpricer   r   r   r   Ú	slope_pctÚlower_highs_lower_lowsÚstopr:   Úimbalance_curr;   Tzhard stop: price $z.4fz <= entry stop $)r    r!   r"   r$   r&   r'   r<   zimbalance declining for z consecutive reads (z+.0%z -> z), price sloping down (z.3fz%))r=   r:   r;   zimbalance declined z) reads but price not sloping down (slope z%) -- withholding exitÚimbalance_reasonr   Úrecent_trendÚnegativer   z	healthy (ú/r   z deterioration signals))r    r!   r$   r&   r'   r   zdeteriorating (z! signals) but not yet confirmed (z reads)zdeterioration confirmed: z signals agreed for z consecutive reads)r7   rE   Úlenr   r-   r   r   r   ÚminÚmaxr   r	   r
   ÚroundÚ
STATE_EXITr4   Úsum)r    rF   rG   r8   rA   r5   rH   ÚimbÚimbalance_staterP   Ú
vwap_valueÚvslopeÚaÚatr_pctÚclosesrQ   Úflat_threshr   ÚlhlrS   r&   Ú	det_trendÚrecentÚsignalsÚagreeingr=   r0   r0   r1   Úevaluate   sª   
ÿ$ÿ
û	þÿÿþÿû
ÿÿüþÿÿüÿürl   )NNN)Ú__doc__Údataclassesr   r   Úconfig_loaderr   Ú
indicatorsr   r   r   r   r	   r
   r-   r]   r3   r   r7   r/   rE   r+   ÚlistÚfloatrl   r0   r0   r0   r1   Ú<module>   s@    8 ó	þÿÿþþ