o
    9§‡jl  ã                
   @   s®   d Z ddlZddlm  mZ ddlZddlZddl	m	Z	m
Z
mZ ej dej ej ej e¡¡¡¡ ddlZd
dd„ZG dd„ dƒZedkrUddlZe ed	g¡ dS dS )ai  
test_momentum_classification.py

Regression tests for the 2026-08-20 change that wires
momentum_slope_class into compute_health()'s raw_state classification:

  1. HEALTHY now requires momentum not be fading (in addition to the
     existing price/VWAP/volume/structure requirements).
  2. A corroborated-only path reclassifies price_slope=positive +
     momentum-fading readings to STALE when at least one other weak
     signal agrees (volume declining, lower-highs/lower-lows, or price
     already lost VWAP) -- deliberately NOT on momentum alone, matching
     the module's existing require-multiple-signals philosophy.
  3. Fading momentum with NO corroborating signal should NOT downgrade
     a reading -- single-signal noise should still fall through to
     WATCH, same as before this change.

Run:
    python -m pytest tests/test_momentum_classification.py -v
é    N)ÚdatetimeÚ	timedeltaÚtimezonec              
   C   sl   |pt dddddtjd�}g }tt| |ƒƒD ]\}\}}| |t|d� |d |d	 |d	 ||d
œ¡ q|S )Niê  é   é   é   é   )Útzinfo)Úminutesg{®Gáz„?g{®Gáz”?)ÚtÚoÚhÚlÚcÚv)r   r   ÚutcÚ	enumerateÚzipÚappendr   )ÚpricesÚvolumesÚstartÚbarsÚiÚpr   © r   ú^/var/www/screener/trade/premarket_backup_2026-09-08_2010/tests/test_momentum_classification.pyÚ	make_bars   s   
þr   c                   @   s,   e Zd Zdd„ Zdd„ Zdd„ Zdd„ Zd	S )
Ú TestMomentumFadingClassificationc                 C   sþ   g d¢}g d¢}t ||ƒ}tjd|dd�}|j}d}||k}|sYt d|fd||f¡d	t ¡ v s5t |¡r:t 	|¡nd	t 	|¡t 	|¡d
œ }t 
d¡d d|i }	tt |	¡ƒ‚d } }}|jd }
|
 }|syddt 	|
¡i }tt |¡ƒ‚d }
}dS )zÈA clean, accelerating uptrend (increasing bar-over-bar gains,
        rising volume, no pullback) should still classify HEALTHY --
        confirms the new momentum gate doesn't break the normal case.)çš™™™™™ @g…ëQ¸ž @g)\�Âõ¨ @ç¸…ëQ¸ @çÍÌÌÌÌÌ @çfffffæ @g…ëQ¸!@g)\�Âõ(!@)i'  éø*  iÈ2  i€>  é N  é¨a  é }  é@œ  ÚTESTé † ©Úavg_vol_baselineÚnegative©ú!=)z<%(py2)s
{%(py2)s = %(py0)s.momentum_slope_class
} != %(py5)sÚreading©Úpy0Úpy2Úpy5z>test setup should produce accelerating (non-negative) momentumú
>assert %(py7)sÚpy7NÚmomentum_fadingúassert not %(py1)sÚpy1)r   Úintraday_healthÚcompute_healthÚmomentum_slope_classÚ
@pytest_arÚ_call_reprcompareÚ@py_builtinsÚlocalsÚ_should_repr_global_nameÚ	_safereprÚ_format_assertmsgÚAssertionErrorÚ_format_explanationÚflags)Úselfr   r   r   r/   Ú@py_assert1Ú@py_assert4Ú@py_assert3Ú@py_format6Ú@py_format8Ú@py_assert0Ú@py_assert2Ú@py_format3r   r   r   Ú)test_healthy_requires_non_fading_momentum+   sH   
þþþþþþþþþþ
þÿþþþ@zJTestMomentumFadingClassification.test_healthy_requires_non_fading_momentumc                 C   sz  g d¢}g d¢}t ||ƒ}tjd|dd�}|jd r»|jd }| }|s4dd	t |¡i }tt |¡ƒ‚d
 }}|jd }| }|sRdd	t |¡i }tt |¡ƒ‚d
 }}|jd }|smdd	t |¡i }tt |¡ƒ‚d
}|j}	d}
|	|
k}|s³t 	d|fd|	|
f¡dt
 ¡ v s�t |¡r”t |¡ndt |	¡t |
¡dœ }t d¡d d|i }tt |¡ƒ‚d
 }	 }}
d
S d
S )zþFading momentum with NO corroborating weak signal (volume
        healthy, price above VWAP, no lower-highs/lower-lows) should
        NOT downgrade to STALE -- single-signal noise still falls
        through to WATCH, unchanged from before this feature.)r   r    r!   g\�Âõ(Ü @r"   çìQ¸…ë @g®Gázî @gq=
×£ð @)r$   éR  iðU  iØY  iÀ]  r%   é�e  ixi  r(   r)   r*   r6   Úvolume_decliningr7   r8   NÚlower_highs_lower_lowsÚprice_above_vwapzassert %(py1)sÚSTALEr-   )z1%(py2)s
{%(py2)s = %(py0)s.raw_state
} != %(py5)sr/   r0   zzmomentum fading alone (no corroboration) should not produce STALE -- got raw_state=STALE with no other weak signal presentr4   r5   )r   r9   r:   rE   r<   rA   rC   rD   Ú	raw_stater=   r>   r?   r@   rB   )rF   r   r   r   r/   rL   rM   rN   Ú@py_format2rG   rH   rI   rJ   rK   r   r   r   Ú3test_decelerating_momentum_alone_does_not_downgrade;   sP   

<<2üüüüüüüüüü
üýüüüüzTTestMomentumFadingClassification.test_decelerating_momentum_alone_does_not_downgradec                 C   sˆ  g d¢}g d¢}t ||ƒ}tjd|dd�}|jd rÀ|jd rÂ|j}d}||k}|sgt d	|fd
||f¡dt ¡ v s?t 	|¡rDt 
|¡ndt 
|¡t 
|¡dœ }t d|j› �¡d d|i }	tt |	¡ƒ‚d } }}tj}|j}||ƒ}
|
 }|s¶ddt ¡ v sˆt 	t¡r�t 
t¡ndt 
|¡dt ¡ v s�t 	|¡r¢t 
|¡ndt 
|¡t 
|
¡dœ }tt |¡ƒ‚d } } }
}dS dS dS )zûFading momentum CORROBORATED by declining volume should
        reclassify to STALE -- the core new behavior, and the exact
        QUBT-style pattern (price still nominally positive, but the
        move is running out of steam) this feature targets.)r   r!   rP   g{®Gáú @gÃõ(\�!@gHáz®!@g
×£p=
!@g²�ï§Æ!@)r'   r&   rR   rQ   ihB  i°6  r#   i(#  r(   r)   r*   r6   rS   rV   )ú==)z1%(py2)s
{%(py2)s = %(py0)s.raw_state
} == %(py5)sr/   r0   zFexpected STALE with momentum fading + volume declining, got raw_state=r4   r5   Nzzassert not %(py7)s
{%(py7)s = %(py2)s
{%(py2)s = %(py0)s.is_eligible_for_entry
}(%(py5)s
{%(py5)s = %(py3)s.raw_state
})
}r9   )r1   r2   Úpy3r3   r5   )r   r9   r:   rE   rW   r<   r=   r>   r?   r@   rA   rB   rC   rD   Úis_eligible_for_entry)rF   r   r   r   r/   rG   rH   rI   rJ   rK   Ú@py_assert6Ú@py_assert8Ú@py_format9r   r   r   Ú>test_fading_momentum_with_declining_volume_downgrades_to_staleQ   sP   
ýýýýýýýýýý
ýþÿýýý¦ùz_TestMomentumFadingClassification.test_fading_momentum_with_declining_volume_downgrades_to_stalec                 C   s  t j}d}||ƒ}d}||u }|sMt d|fd||f¡dt ¡ v s&t t ¡r+t t ¡ndt |¡t |¡t |¡t |¡dœ }dd|i }tt 	|¡ƒ‚d	 } } } }}t j}d
}||ƒ}d}||u }|s¤t d|fd||f¡dt ¡ v s}t t ¡r‚t t ¡ndt |¡t |¡t |¡t |¡dœ }dd|i }tt 	|¡ƒ‚d	 } } } }}t j}d}||ƒ}d}||u }|sût d|fd||f¡dt ¡ v sÔt t ¡rÙt t ¡ndt |¡t |¡t |¡t |¡dœ }dd|i }tt 	|¡ƒ‚d	 } } } }}d	S )a  Directly confirms the protective mechanism: whatever produces
        a STALE raw_state via the new momentum path is blocked at the
        same is_eligible_for_entry() gate every other STALE read uses --
        this is what makes the feature more than cosmetic.ÚHEALTHYT)Úis)z[%(py6)s
{%(py6)s = %(py2)s
{%(py2)s = %(py0)s.is_eligible_for_entry
}(%(py4)s)
} is %(py9)sr9   )r1   r2   Úpy4Úpy6Úpy9zassert %(py11)sÚpy11NÚWATCHrV   F)
r9   r\   r<   r=   r>   r?   r@   rA   rC   rD   )rF   rG   rI   Ú@py_assert5r^   Ú@py_assert7Ú@py_format10Ú@py_format12r   r   r   Ú.test_stale_from_momentum_is_not_entry_eligiblef   s   ®®²zOTestMomentumFadingClassification.test_stale_from_momentum_is_not_entry_eligibleN)Ú__name__Ú
__module__Ú__qualname__rO   rY   r`   rl   r   r   r   r   r   *   s
    r   Ú__main__z-v)N)Ú__doc__Úbuiltinsr>   Ú_pytest.assertion.rewriteÚ	assertionÚrewriter<   ÚsysÚosr   r   r   ÚpathÚinsertÚdirnameÚabspathÚ__file__r9   r   r   rm   ÚpytestÚmainr   r   r   r   Ú<module>   s    "&
Fþ