o
    ÕË¶jd/  ã                   @   sð   d Z ddlmZmZ ddlmZmZ ddlmZ dZdZ	ddd	d
ddddddddddddœZ
eG dd„ dƒƒZdd„ Zdd„ Zd0dedededefdd„Zded efd!d"„Zd#ed$ed efd%d&„Z	(d1d)eded*ed+ed,ed-ed efd.d/„Zd'S )2aú  
exit_resistance_stall.py

[2026-09-23] "Stalled at resistance -> sell at the ceiling" exit layer,
specified by the user after walking through OPTX (bought $7.61, then
tested a $7.70-7.72 ceiling ~10 times in 50 minutes on thin volume
without a breakout, while the 15c giveback-room gate never armed).

Every poll, for an open position:

  1. Find the ceiling. Among the 1-min bars since entry (and since the
     last confirmed breakout, if any), the ceiling is the HIGHEST bar
     high h that has at least min_touches bar highs within
     [h - band, h], band = max(band_cents, band_pct of price). A single
     wick above a real cluster doesn't count (it has no touches of its
     own), so the cluster under it stays the ceiling.
     zone_top = h, zone_bottom = the lowest high in that cluster.

  2. Proof of breakout: a COMPLETED bar closing above zone_top +
     breakout_buffer on volume >= breakout_volume_ratio x the average
     completed-bar volume. That turns the stall clock off: only bars
     after the breakout bar are considered from then on, so a new
     ceiling (if any) has to form above it and starts its own clock.

  3. The clock starts at the ceiling's FIRST TOUCH after entry (the
     earliest bar in the cluster). After stall_minutes with no breakout,
     "building" is checked once: rising lows (the minimum low of each
     third of the bars since first touch strictly increasing) OR rising
     volume on the touches (later touches' average volume >=
     rising_volume_ratio x earlier touches'). If building, the deadline
     moves out by extension_minutes (up to max_extensions times);
     otherwise the rule arms.

  4. Armed: the first poll where price >= zone_bottom sells (market
     order, via the caller). Applies whether the position is above or
     below entry at that point -- at the ceiling is the best exit the
     stall is going to offer.

Pure function of its inputs; caller holds state_in/state_out
({"breakout_after", "extensions", "armed"}), same convention as
exit_giveback_room.py.
é    )Ú	dataclassÚfield)ÚdatetimeÚ	timedelta)Ú
get_configÚHOLDÚEXITFé   g       @g333333Ó?ç      N@g      $@é   g      ð?g      ø?gÍÌÌÌÌÌô?é   é<   )ÚenabledÚmin_touchesÚ
band_centsÚband_pctÚstall_minutesÚextension_minutesÚmax_extensionsÚbreakout_buffer_centsÚbreakout_volume_ratioÚrising_volume_ratioÚlow_ceiling_enabledÚlow_ceiling_minutesÚdead_money_minutesÚdead_money_window_minÚdead_money_range_pctc                   @   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 )ÚResistanceStallDecisionÚsymbolÚstateFÚshould_exitÚ Úreason)Údefault_factoryÚmetricsÚ	state_outN)Ú__name__Ú
__module__Ú__qualname__ÚstrÚ__annotations__Ú
STATE_HOLDr   r    Úboolr"   r   Údictr$   r%   © r.   r.   ú0/var/www/screener/trade/exit_resistance_stall.pyr   P   s   
 r   c                 C   s   i t ¥| ptƒ  di ¡¥S )NÚresistance_stall)ÚDEFAULT_CONFIGr   Úget)Úcfgr.   r.   r/   Ú
_merge_cfgZ   s   r4   c                 C   s   t | tƒr
t | ¡S | S )N)Ú
isinstancer)   r   Úfromisoformat©Útr.   r.   r/   Ú_as_dt^   s   r9   ÚbarsÚbandr   Úmax_bars_abovec                    s„   t dd„ | D ƒdd�}|D ]/‰t‡fdd„| D ƒƒ|kr n‡ ‡fdd„| D ƒ}t|ƒ|kr<ˆtd	d„ |D ƒƒ|f  S qd
d
g fS )a   The ceiling has to be the top of the range: at most max_bars_above
    bars (a stray wick) may print above it. Without this, a stock trending
    away from an old consolidation would read that consolidation as its
    'ceiling'. Bars above only grow as h drops, so the search stops there.c                 S   s   h | ]}|d  ’qS ©Úhr.   ©Ú.0Úbr.   r.   r/   Ú	<setcomp>g   s    z _find_ceiling.<locals>.<setcomp>T)Úreversec                 3   s    � | ]}|d  ˆ krdV  qdS )r>   r   Nr.   r?   r=   r.   r/   Ú	<genexpr>i   s   € z _find_ceiling.<locals>.<genexpr>c                    s0   g | ]}ˆˆ  |d    krˆkrn n|‘qS r=   r.   r?   ©r;   r>   r.   r/   Ú
<listcomp>k   s   0 z!_find_ceiling.<locals>.<listcomp>c                 s   ó   � | ]}|d  V  qdS ©r>   Nr.   r?   r.   r.   r/   rD   m   ó   € N)ÚsortedÚsumÚlenÚmin)r:   r;   r   r<   ÚhighsÚclusterr.   rE   r/   Ú_find_ceilingb   s   ÿ
rP   Úreturnc                 C   sx   t | ƒdk rdS t | ƒd }| d |… | |d| … | d| d … g}dd„ |D ƒ}|d |d   k o9|d k S   S )	Né   Fr	   é   c                 S   s   g | ]}t d d„ |D ƒƒ‘qS )c                 s   rG   ©ÚlNr.   r?   r.   r.   r/   rD   v   rI   z*_rising_lows.<locals>.<listcomp>.<genexpr>)rM   )r@   Úpartr.   r.   r/   rF   v   s    z _rising_lows.<locals>.<listcomp>r   r   )rL   )r:   ÚnÚthirdsÚlowsr.   r.   r/   Ú_rising_lowsq   s   *$rZ   rO   Úratioc                 C   st   t | ƒdk rdS t | ƒd }tdd„ | d |… D ƒƒ| }tdd„ | |d … D ƒƒt | ƒ|  }|dko9||| kS )Né   FrS   c                 s   rG   ©ÚvNr.   r?   r.   r.   r/   rD   ~   rI   z'_rising_touch_volume.<locals>.<genexpr>c                 s   rG   r]   r.   r?   r.   r.   r/   rD      rI   r   )rL   rK   )rO   r[   ÚhalfÚearlyÚlater.   r.   r/   Ú_rising_touch_volumez   s   &rb   NTr   Ústate_inr3   Ú
slots_freeÚhealthyc           (         sz  t |ƒ}ddddœ|pi ¥}t|ƒt|ƒ}	}
|	jddd�}|s't| d|d�S |d d	 }t|d
 d ||d  d ƒ}|d rFt|d ƒn|‰‡fdd„|D ƒ}|dd… }t|||d ƒ\}}}|t|dƒdœ}|du rvt| d||d�S dd„ |dd… D ƒ}|r˜tdd„ |dd… D ƒƒt|dd… ƒ nd}t|d d ƒ‰|D ]k}t|d ƒˆk�r|d	 ||d d  k�r|�r|d ||d  k�rt|d ƒt	dd�  
¡ dddœ}t| d t|d ƒ d!¡› d"|d	 d#›d$|d#›d%|d d&›d'|d | d(›d)�i |¥||d*œ¥|d�  S q¤‡fd+d„|D ƒ}td,d„ |dd… D ƒ�p'|ƒ}t||d- ƒ}|
ˆ  ¡ d. }|d/ |d0 |d1   }| t|dƒt|dƒt|ƒˆ 
¡ t|dƒ||||d2 d3œ	¡ |d2 �s¬||k�r¬|�sr|�r¤|d0 |d4 k �r¤|d0  d7  < t| d5|d&›d6|d#›d7|d#›d8|› d9|› d:|d1 d&›d;�||d�S d<|d2< d<|d2< |d2 �rÖ||k�rÖt| td<d=t|ƒ› d>|d#›d7|d#›d?|d&›d@|d#›dA�||dB�S |d2 �r@|dC �r@|dD �r@|�r@|
|	  ¡ d. }|
t	|dE d� ‰‡fdFd„|D ƒ}||dD k�r@|�r@tdGd„ |D ƒƒtdHd„ |D ƒƒ}} |�r*||  | d nd}!t|!dIƒ|dJ< |!|dJ k�r@d}d<|dK< |d2 �r|dC �r|
t	|dL d� ‰ ‡ fdMd„|D ƒ}"|"�rgt|"||d ƒnddg f\}#}$}%|#du�r|#|k �r| t|#dƒt|$dƒt|%ƒ||dNœ¡ |�r’|�sÒ||$k�rÒ|�sœdOn	| dK¡�r¤dPndQ}&t| td<dR|d#›d7|d#›dS|dL d&›dT|$d#›d7|#d#›dUt|%ƒ› dV|&› dW|d#›�||dB�S |�ró|�rót| dX|d#›dY|dL d&›dZ|$d#›d7|#d#›d[�	||d�S t| dX|d#›d\|dL d&›dZ|$d#›dU|�s	dOnd]› dA�	||d�S |d2 �rd^|d#›�nd_|d#›d7|d#›d`t|ƒ› da|d&›db|d&›d;�}'t| |'||d�S )cz–
    bars: the symbol's 1-min bars, oldest first, forming bar last (same
        list exit.py gets). entry_time/now_ts: datetimes or ISO strings.
    Nr   F)Úbreakout_afterÚ
extensionsÚarmed)ÚsecondÚmicrosecondzno bars)r"   r%   éÿÿÿÿÚcr   g      Y@r   rf   c                    ó    g | ]}t |d  ƒˆ kr|‘qS r7   ©r9   r?   )Ústartr.   r/   rF   ”   ó     zevaluate.<locals>.<listcomp>r   r\   )Úpricer;   zno ceiling yet)r"   r$   r%   c                 S   ó   g | ]}|‘qS r.   r.   r?   r.   r.   r/   rF   �   ó    c                 s   rG   r]   r.   r?   r.   r.   r/   rD   ž   rI   zevaluate.<locals>.<genexpr>iÄÿÿÿg        r8   r   r^   r   r   )Úminuteszbreakout confirmed: z%H:%Mz bar closed z.4fz > ceiling z on z.0fz sh (z.1fzx avg) -- stall clock reset)Úzone_topÚzone_bottomc                    rm   r7   rn   r?   )Úfirst_touchr.   r/   rF   ­   rp   c                 S   rr   r.   r.   r?   r.   r.   r/   rF   ®   rs   r   r
   r   rg   r   rh   )	ru   rv   Útouchesrw   Ústalled_minutesÚdeadline_minutesÚrising_lowsÚrising_touch_volumerh   r   zstalled zm under ú-z but building (rising lows=z, rising test volume=z) -- +ÚmTzresistance stall: z touches of z over z'm, no breakout -- sold at the ceiling (ú))r   r    r"   r$   r%   r   r   r   c                    rm   r7   rn   r?   )Úwcutr.   r/   rF   Î   rp   c                 s   rG   rH   r.   r?   r.   r.   r/   rD   Ð   rI   c                 s   rG   rT   r.   r?   r.   r.   r/   rD   Ð   rI   rS   r   Ú
dead_moneyr   c                    rm   r7   rn   r?   )Úcutoffr.   r/   rF   Ù   rp   )Úlow_ceiling_topÚlow_ceiling_bottomÚlow_ceiling_touchesrd   re   zno free slotzdead money (flat range)zposition no longer healthyz(resistance stall, low ceiling: armed at z but the last zm topped at z (z touches); z -- sold at z	armed at z; last z
m ceiling z, holding (slot free, healthy)z; will sell at the last-znot healthyz"armed, waiting for price to reach zceiling z, z
 touches, ú/)r4   r9   Úreplacer   ÚmaxrP   ÚroundrK   rL   r   Ú	isoformatÚstrftimerZ   rb   Útotal_secondsÚupdateÚ
STATE_EXITrM   r2   )(r   r:   Ú
entry_timeÚnow_tsrc   r3   rd   re   r   Úentry_dtÚnowÚentry_minuterq   r;   ÚwindowÚ	completedru   rv   rO   r$   Úall_completedÚavg_volrA   Ú	new_stateÚsince_firstr{   Ú
rising_volÚstalled_minÚdeadlineÚheld_minÚwbarsÚhiÚloÚrngÚrecentÚlc_topÚ	lc_bottomÚ
lc_clusterÚwhyr"   r.   )r‚   rw   ro   r€   r/   Úevaluateƒ   s  2ÿÿÿÿÿþ
û€ 
üÿÿþüÿÿü$&&
ÿ
ÿÿÿÿþþþûÿÿýÿÿýÿÿÿr§   )r   )NTT)Ú__doc__Údataclassesr   r   r   r   Úconfig_loaderr   r+   rŽ   r1   r   r4   r9   ÚlistÚfloatÚintrP   r,   rZ   rb   r)   r-   r§   r.   r.   r.   r/   Ú<module>   sN    +è		
ÿÿÿÿÿ