o
    «›jy&  ã                   @   s  d Z ddlmZmZ ddlmZ dZdZdZdZ	dZ
eeee	e
gZd	d
dddœZeG dd„ dƒƒZdededefdd„Z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dedefdd„Zdedefdd „Zd'dededefd"d#„Zd$edefd%d&„Zd!S )(aÃ  
setup_engine.py

[SIMULATION-ONLY 2026-09-05] Detects the five concrete entry patterns
regime_engine.py's allowed_setups already references by name but nothing
until now actually implements: PULLBACK_CONTINUATION, VWAP_RECLAIM,
BREAKOUT_RETEST, HIGHER_LOW_REVERSAL, COMPRESSION_EXPANSION. Not consumed
anywhere in the live bot yet -- see config.json's setup_engine._note.

Each detector is a stateless checklist evaluated against the CURRENT
trend/structure/stream_features snapshot -- same pattern
entry_engine.evaluate_entry() already uses (a list of checks, each
either passed or not, with an explanation), not a multi-call tracked
state machine. This keeps every function here pure (no state, no I/O),
consistent with every other module built this session. The project's
original design brief describes each setup as a temporal SEQUENCE (e.g.
"impulse -> pullback -> volume contracts -> higher low -> EMA9 reclaim
-> ENTRY") -- what's implemented here checks that the CURRENT snapshot
is consistent with being at the end of that sequence (the structural/
momentum fingerprint a completed sequence leaves behind), not that the
sequence was traced step-by-step over time. That's a deliberate
simplification worth knowing about, not a hidden gap: a true multi-call
temporal tracker is a bigger, different kind of module (it would need
caller-held per-symbol history, like stream_features.py's `prior`
parameter) and isn't needed to get useful signal out of this layer yet.
é    )Ú	dataclassÚfield)Ú
get_configÚPULLBACK_CONTINUATIONÚVWAP_RECLAIMÚBREAKOUT_RETESTÚHIGHER_LOW_REVERSALÚCOMPRESSION_EXPANSIONg      >@g      à?g      ð?g      D@)Úvwap_reclaim_min_time_below_pctÚretest_buffer_pctÚmin_volume_accelerationÚ-compression_expansion_min_range_expansion_pctc                   @   sJ   e Zd ZU eed< eed< eed< eed�Z	eed< eed�Z
eed< dS )ÚSetupResultÚsetupÚvalidÚ
confidence)Údefault_factoryÚchecks_passedÚchecks_failedN)Ú__name__Ú
__module__Ú__qualname__ÚstrÚ__annotations__ÚboolÚfloatr   Úlistr   r   © r   r   ú+/var/www/screener/premarket/setup_engine.pyr   1   s   
 r   r   ÚchecksÚreturnc                 C   sd   dd„ |D ƒ}dd„ |D ƒ}|rt t|ƒt|ƒ d dƒnd}t| t|ƒdko,t|ƒdk|||d�S )	aV  checks: list of (passed: bool, description: str). valid requires
    ALL checks to pass (a setup is either confirmed or it isn't -- no
    partial credit toward triggering an entry), but confidence reports
    the fraction anyway so a near-miss is visible in logs rather than
    looking identical to a setup with nothing going for it at all.c                 S   s   g | ]\}}|r|‘qS r   r   ©Ú.0ÚokÚdescr   r   r   Ú
<listcomp>@   ó    z_result.<locals>.<listcomp>c                 S   s   g | ]\}}|s|‘qS r   r   r!   r   r   r   r%   A   r&   ç      Y@é   ç        r   )r   r   r   r   r   )ÚroundÚlenr   )r   r   ÚpassedÚfailedr   r   r   r   Ú_result:   s   "ÿr.   Úcfgc           	      C   sª   dd„ |j D ƒ}t|ƒo|d jdk}| j d¡}| j d¡}|jdv df|jd	kd
f|df|d uo9|d uo9||kdf|jdkdf| j	 d¡pHd|d kdfg}t
t|ƒS )Nc                 S   ó$   g | ]}|j d kr|jdur|‘qS ©ÚLOWN©ÚkindÚlabel©r"   Úsr   r   r   r%   H   ó   $ z0detect_pullback_continuation.<locals>.<listcomp>éÿÿÿÿÚHLÚema9Úlast)ÚSTRONG_UPTRENDÚWEAK_UPTRENDz"established uptrend (trend_engine)ÚBULLISH_STRUCTUREz5confirmed bullish swing structure (the prior impulse)z6most recent low swing is a higher low, not a lower lowúprice has reclaimed EMA9r   z1momentum turning back positive after the pullbackÚvolume_accelerationr   z*volume re-expanding, not still contracting)Úswingsr   r5   ÚemaÚgetÚpriceÚstateÚ	structureÚtrend_slopeÚvolumer.   r   )	ÚfeaturesÚtrendrG   r/   ÚlowsÚlast_low_is_hlr;   rE   r   r   r   r   Údetect_pullback_continuationG   s   ÿú
	rN   c           	      C   sž   dd„ |j D ƒ}t|ƒo|d jdk}| j dd¡}| j dd¡}|dkdf||d	 kd
|d›d�f|df|jdkdf| j d¡pBd|d kdfg}tt|ƒS )Nc                 S   r0   r1   r3   r6   r   r   r   r%   Y   r8   z'detect_vwap_reclaim.<locals>.<listcomp>r9   r:   Úprice_vs_vwap_pctr   Útime_below_pctzprice currently above VWAPr
   z%spent real time below VWAP recently (z.0fz'% of window), this is a genuine reclaimzEheld a higher low since reclaiming (didn't immediately lose it again)zmomentum turning positiverA   r   z$volume accelerating into the reclaim)	rB   r   r5   ÚvwaprD   rH   rI   r.   r   )	rJ   rK   rG   r/   rL   rM   Úprice_vs_vwapÚ
time_belowr   r   r   r   Údetect_vwap_reclaimX   s   

ÿÿú
	rT   Úbarsc                    sÂ   |j ‰ | j d¡}d}d}d}ˆ d urF|d urF|rFt‡ fdd„|D ƒƒ}ˆ |d d  }	t|ˆ  ƒ|	kpAˆ |  ko?ˆ |	 kn  }|ˆ k}ˆ d udf|df|d	f|d
f|jdkdfg}
tt|
ƒS )Nr<   Fc                 3   s   � | ]	}|d  ˆ kV  qdS )ÚcNr   )r"   Úb©Ú
resistancer   r   Ú	<genexpr>p   s   € z)detect_breakout_retest.<locals>.<genexpr>r   r'   z.a confirmed swing-high resistance level existsz4price has broken above that resistance at some pointz;price has pulled back to retest the former resistance levelzNresistance is holding as new support on the retest (not failing back below it)ÚUPzunderlying trend is still up)Úlast_swing_highrE   rD   ÚanyÚabsÚ	directionr.   r   )rJ   rK   rG   rU   r/   rE   Úbroke_aboveÚ	retestingÚholdingÚbufferr   r   rX   r   Údetect_breakout_retesti   s"   ,
û
rd   c           	      C   s    dd„ |j D ƒ}t|ƒdko|d jdko|d jdk}| j d¡}| j d	¡}|d
f|jdkdf|d uo<|d uo<||kdf| j d¡pEddkdfg}tt	|ƒS )Nc                 S   r0   r1   r3   r6   r   r   r   r%      r8   z.detect_higher_low_reversal.<locals>.<listcomp>é   éþÿÿÿÚLLr9   r:   r;   r<   zNmade a lower low, then a higher low (bottoming pattern, not a fresh downtrend)r   z"price velocity has turned positiver@   Úvelocity_subz very recent velocity is positive)
rB   r+   r5   rC   rD   rE   rH   Úvelocityr.   r   )	rJ   rK   rG   r/   rL   Úbottomed_then_hlr;   rE   r   r   r   r   Údetect_higher_low_reversal~   s   (ü
rk   c           	      C   s˜   dd„ |j D ƒ}|jdv }| p|d jdv }| j d¡}|d uo&||d kd|› d	�f|d
f|df| j d¡p9d|d kdf|jdkdfg}tt|ƒS )Nc                 S   s   g | ]	}|j d kr|‘qS )r2   )r4   r6   r   r   r   r%   �   s    z0detect_compression_expansion.<locals>.<listcomp>)r?   ÚRANGE_STRUCTUREr9   )Nr:   Úrange_expansion_pctr   z@range expanding out of a prior compression (range_expansion_pct=ú)zGstructure held (bullish or range) through the compression, no breakdownzAmost recent low did not undercut the prior one during compressionrA   r   r   z$volume expanding alongside the rangeÚDOWNznot breaking down as it expands)	rB   rG   r5   Ú
volatilityrD   rI   r_   r.   r	   )	rJ   rK   rG   r/   rL   Úholding_structureÚlast_low_okÚrange_expansionr   r   r   r   Údetect_compression_expansionŒ   s   

ÿÿù
	rt   Nc                 C   s    i t ¥|ptƒ  di ¡¥}|pg }t| ddƒs#t|ddƒs#t|ddƒr*dd„ tD ƒS tt| |||ƒtt| |||ƒt	t
| ||||ƒtt| |||ƒtt| |||ƒiS )am  Returns {setup_name: SetupResult} for all five setups. Callers
    (opportunity_ranker.py, eventually) filter this against
    regime_engine's allowed_setups for the current regime before
    treating any of them as tradeable -- this function doesn't know or
    care what regime is active, it just reports what patterns the
    current snapshot is consistent with.Úsetup_engineÚinsufficient_dataFc              	   S   s    i | ]}|t |d ddgd�“qS )Fr)   zinsufficient upstream data)r   r   r   r   )r   ©r"   Únamer   r   r   Ú
<dictcomp>©   s
    
þ
ÿz#evaluate_setups.<locals>.<dictcomp>)ÚDEFAULT_CONFIGr   rD   ÚgetattrÚ
ALL_SETUPSr   rN   r   rT   r   rd   r   rk   r	   rt   )rJ   rK   rG   rU   r/   r   r   r   Úevaluate_setups�   s   
ÿþûr}   Úsetupsc                    s*   ‡ fdd„t D ƒ}|sdS t|dd„ d�S )a  The single highest-confidence VALID setup, or None if nothing
    validated. Ties broken by ALL_SETUPS priority order (see
    regime_engine.py's original design-brief priority note -- pullback
    continuation first, recovery-style reversal last), not arbitrary
    dict order.c                    s(   g | ]}ˆ   |¡rˆ | jrˆ | ‘qS r   )rD   r   rw   ©r~   r   r   r%   ¼   s   ( zbest_setup.<locals>.<listcomp>Nc                 S   s   | j S )N)r   )Úrr   r   r   Ú<lambda>¿   s    zbest_setup.<locals>.<lambda>)Úkey)r|   Úmax)r~   r   r   r   r   Ú
best_setup¶   s   r„   )NN)Ú__doc__Údataclassesr   r   Úconfig_loaderr   r   r   r   r   r	   r|   rz   r   r   r   r.   ÚdictrN   rT   rd   rk   rt   r}   r„   r   r   r   r   Ú<module>   s4    ÿü