o
    ½~£j¸'  ã                
   @   sè   d Z ddlZddlm  mZ ddlZddlZddl	Z	ddl
mZm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lmZ ddlmZ ddlmZ G dd	„ d	e	jƒZG d
d„ de	jƒZedkrre	  ¡  dS dS )a»  
test_entry_shortlist_starvation.py

Regression test for the shortlist-starvation bug found in the
2026-09-01 session review: monitor.py's _scan_for_entries() ranks
premarket_20 by health_score and only evaluate_entry()-checks the top
entry_shortlist_size symbols each poll cycle. Real log evidence that
day showed F occupying a shortlist slot continuously from 09:32 to
09:56 (~24 minutes) failing entry_engine's confirmation on the same
unchanging "momentum fading" hard disqualifier every single time,
while CRML -- health-eligible and scoring competitively -- was rotated
into premarket_20 at 10:01 but never once reached evaluate_entry()
before rotating back out at the next rescan, because F kept winning
the health-score ranking and occupying the only available shortlist
slot.

_EntryAttemptTracker (see monitor.py) fixes this by benching a symbol
after max_consecutive_confirmation_failures straight not-confirmed
results, freeing its shortlist slot for the next-best candidate. This
test covers both the tracker in isolation and the full wiring through
_scan_for_entries().

Run:
    python -m pytest tests/test_entry_shortlist_starvation.py -v
or:
    python tests/test_entry_shortlist_starvation.py
é    N)ÚpatchÚ	MagicMock)ÚdatetimeÚ	timedeltaÚtimezone)ÚFastPipelineDecision)ÚFastPredictionReading)ÚStreamFeaturesc                   @   sP   e Zd ZdZdd„ Zdd„ Zdd„ Zdd	„ Zd
d„ Zdd„ Z	dd„ Z
dd„ ZdS )ÚTestEntryAttemptTrackerUnitz?Pure unit tests for the tracker, no monitor.py wiring involved.c                 C   s,   t dddddtjd�| _tjddd	�| _d S )
Niê  é	   é   é   r   )Útzinfoé   é´   ©Úmax_failuresÚcooldown_seconds)r   r   ÚutcÚnowÚmonitorÚ_EntryAttemptTrackerÚtracker©Úself© r   úD/var/www/screener/premarket/tests/test_entry_shortlist_starvation.pyÚsetUp.   s   z!TestEntryAttemptTrackerUnit.setUpc                 C   sB   | j jdd| jd� | j jdd| jd� |  | j  d| j¡¡ d S )NÚFF©Ú	confirmedr   ©r   Úrecord_resultr   ÚassertFalseÚ
is_benchedr   r   r   r   Ú!test_not_benched_before_threshold2   s   z=TestEntryAttemptTrackerUnit.test_not_benched_before_thresholdc                 C   s<   t dƒD ]}| jjdd| jd� q|  | j d| j¡¡ d S )Nr   r   Fr   )Úranger   r"   r   Ú
assertTruer$   ©r   Ú_r   r   r   Ú+test_benched_after_max_consecutive_failures7   s   zGTestEntryAttemptTrackerUnit.test_benched_after_max_consecutive_failuresc                 C   óJ   t dƒD ]}| jjdd| jd� q| jtdd� }|  | j d|¡¡ d S )Nr   r   Fr   éµ   ©Úseconds)r&   r   r"   r   r   r#   r$   )r   r)   Úlaterr   r   r   Ú(test_bench_clears_after_cooldown_elapses<   ó   zDTestEntryAttemptTrackerUnit.test_bench_clears_after_cooldown_elapsesc                 C   r+   )Nr   r   Fr   é<   r-   )r&   r   r"   r   r   r'   r$   )r   r)   Úsoonr   r   r   Ú*test_still_benched_before_cooldown_elapsesB   r1   zFTestEntryAttemptTrackerUnit.test_still_benched_before_cooldown_elapsesc                 C   s~   | j jdd| jd� | j jdd| jd� | j jdd| jd� | j jdd| jd� | j jdd| jd� |  | j  d| j¡¡ d S )Nr   Fr   Tr!   r   r   r   r   Ú1test_a_confirmed_result_resets_the_failure_streakH   s   zMTestEntryAttemptTrackerUnit.test_a_confirmed_result_resets_the_failure_streakc                 C   sf   t dƒD ]}| jjdd| jd� q|  | j d| j¡¡ | jjdd| jd� |  | j d| j¡¡ d S )Nr   r   Fr   T©r&   r   r"   r   r'   r$   r#   r(   r   r   r   Ú/test_a_confirmed_result_lifts_an_existing_benchQ   s
   zKTestEntryAttemptTrackerUnit.test_a_confirmed_result_lifts_an_existing_benchc                 C   sR   t dƒD ]}| jjdd| jd� q|  | j d| j¡¡ |  | j d| j¡¡ d S )Nr   r   Fr   ÚCRMLr6   r(   r   r   r   Ú&test_symbols_are_tracked_independentlyX   s   zBTestEntryAttemptTrackerUnit.test_symbols_are_tracked_independentlyN)Ú__name__Ú
__module__Ú__qualname__Ú__doc__r   r%   r*   r0   r4   r5   r7   r9   r   r   r   r   r
   +   s    	r
   c                   @   s(   e Zd ZdZdd„ Zdd„ Zdd„ ZdS )	Ú%TestScanForEntriesSkipsBenchedSymbolsaæ  
    End-to-end through _scan_for_entries(): with entry_shortlist_size=1
    and F always outranking CRML on fast_prediction confidence (the
    ranking key since [FEATURE 2026-09-11 v2] -- "the confidence value
    should be the value used for selecting the top_5") but never
    confirming, F should monopolize the only shortlist slot for
    max_consecutive_confirmation_failures cycles -- then get benched,
    finally letting CRML (which DOES confirm) get evaluated and enter.
    c                    sÜ   t j t j¡ˆ _ddddœddiddid	d
idœˆ j_t jddd�ˆ j_i ˆ j_dˆ j_d ˆ j_	dddœˆ j_
dddœdddœgˆ j_g ˆ _G ‡ fdd„dƒ}G dd„ dƒ}|ƒ ˆ j_|ƒ ˆ j_dddœdddœdœˆ _d S )Nr   r   r   )Úentry_shortlist_sizeÚ%max_consecutive_confirmation_failuresÚ%confirmation_failure_cooldown_secondsÚmin_avg_daily_volumei ¡ Úsimulated_equity_defaultg     jø@Úsub_minute_bucket_secondsé   )Úintraday_healthÚuniverseÚtradingÚ	streamingr   Tg     @�@)r   r8   r   g…ëQ¸îR@)ÚsymbolÚtotal_scorer8   g®GázŽQ@c                       s0   e Zd Zdd„ Zdd„ Zdd„ Z‡ fdd„Zd	S )
zHTestScanForEntriesSkipsBenchedSymbols.setUp.<locals>.FakePositionManagerc                 S   ó   dS ©NFr   ©Ú_selfrJ   r   r   r   Úis_symbol_open„   ó   zWTestScanForEntriesSkipsBenchedSymbols.setUp.<locals>.FakePositionManager.is_symbol_openc                 S   rL   )NTr   )rO   r   r   r   Úhas_available_slot‡   rQ   z[TestScanForEntriesSkipsBenchedSymbols.setUp.<locals>.FakePositionManager.has_available_slotc                 S   rL   rM   r   rN   r   r   r   Úhas_closed_position_todayŠ   rQ   zbTestScanForEntriesSkipsBenchedSymbols.setUp.<locals>.FakePositionManager.has_closed_position_todayc                    s   ˆ j  |¡ d S ©N)ÚenteredÚappend)rO   rJ   Úentry_priceÚequityÚbarsÚreasonr   r   r   Úenter_position�   s   zWTestScanForEntriesSkipsBenchedSymbols.setUp.<locals>.FakePositionManager.enter_positionN)r:   r;   r<   rP   rR   rS   r[   r   r   r   r   ÚFakePositionManagerƒ   s
    r\   c                   @   s,   e Zd Zdd„ Zdd„ Zdd„ Zdd„ Zd	S )
z?TestScanForEntriesSkipsBenchedSymbols.setUp.<locals>.FakeStreamc                 S   rL   rM   r   rN   r   r   r   Úis_symbol_stale‘   rQ   zOTestScanForEntriesSkipsBenchedSymbols.setUp.<locals>.FakeStream.is_symbol_stalec                    s"   t  tj¡‰ ‡ fdd„tdƒD ƒS )Nc              	      s(   g | ]}ˆ t |d � ddddddœ‘qS ))Úminutesr   éd   )ÚtÚoÚhÚlÚcÚv)r   )Ú.0Úi©r   r   r   Ú
<listcomp>–   s
    ÿ
ÿz\TestScanForEntriesSkipsBenchedSymbols.setUp.<locals>.FakeStream.get_bars.<locals>.<listcomp>é   )r   r   r   r   r&   rN   r   rh   r   Úget_bars”   s   
ÿzHTestScanForEntriesSkipsBenchedSymbols.setUp.<locals>.FakeStream.get_barsc                 S   s   g S rT   r   rN   r   r   r   Úget_bars_sub™   rQ   zLTestScanForEntriesSkipsBenchedSymbols.setUp.<locals>.FakeStream.get_bars_subc                 S   s   d S rT   r   rN   r   r   r   Ú	get_quoteœ   rQ   zITestScanForEntriesSkipsBenchedSymbols.setUp.<locals>.FakeStream.get_quoteN)r:   r;   r<   r]   rk   rl   rm   r   r   r   r   Ú
FakeStream�   s
    rn   ÚWATCHç     €V@)ÚstateÚhealth_scoreç     €Q@)r   ÚSessionOrchestratorÚ__new__ÚorchÚcfgr   Ú_entry_trackerÚ_fast_engine_stateÚ
simulationÚclientÚ_opening_volume_baselineÚpremarket_20rU   Úposition_mgrÚstreamÚhealth_state)r   r\   rn   r   r   r   r   j   s6   ý
ø

ÿþ

þz+TestScanForEntriesSkipsBenchedSymbols.setUpc                    s  		 ddd„}d‡ fdd„	}t  td¡�l}t  td¡�M}t jtd|d	��5 t jtd
|d	�� | j|j_d|j_tdd�|j_| j	 
¡  W d   ƒ n1 sMw   Y  W d   ƒ n1 s\w   Y  W d   ƒ n1 skw   Y  W d   ƒ d S W d   ƒ d S 1 sƒw   Y  d S )NrE   c	                 S   s8   | dkrdnd}	t | |	d�}
t| t tj¡d�}|
|fS )Nr   rp   rs   )rJ   Ú
confidence)rJ   Úcomputed_at)r   r	   r   r   r   r   )rJ   rY   Úbars_subÚquoteÚbaselinerq   Úsub_bucket_secondsÚas_ofÚpremarket_resultr�   Ú
predictionÚfeaturesr   r   r   Úfake_compute_fast_ranking¬   s   zWTestScanForEntriesSkipsBenchedSymbols._run_one_cycle.<locals>.fake_compute_fast_rankingc                    s   ˆ   | ¡ t| | dkdd�S )Nr8   g        )rJ   Úshould_enterÚconfirmation_score)rV   r   )rJ   r‰   rŠ   rq   r‡   ©Úevaluate_entry_callsr   r   Úfake_evaluate_fast_entry_only¶   s   

ÿz[TestScanForEntriesSkipsBenchedSymbols._run_one_cycle.<locals>.fake_evaluate_fast_entry_onlyÚ
data_storerF   Úcompute_fast_ranking)Úside_effectÚevaluate_fast_entry_onlyTro   )Ú	raw_state)NrE   NN)NN)r   Úobjectr   r€   Úload_health_stateÚreturn_valueÚis_eligible_for_entryr   Úcompute_healthrv   Ú_scan_for_entries)r   r�   r‹   r�   Úmock_dsÚmock_healthr   rŽ   r   Ú_run_one_cycle«   s*   
þ

ÿþý
÷€ €P z4TestScanForEntriesSkipsBenchedSymbols._run_one_cyclec                 C   sŒ   g }t dƒD ]}|  |¡ q|  |g d¢d¡ |  | jg d¡ |  | jj dt 	t
j¡¡¡ |  |¡ |  |g d¢¡ |  | jdgd¡ d S )	Nr   )r   r   r   z“F should occupy the only shortlist slot on every cycle before it's been benched -- CRML never gets a turn while F still outranks it on health_scorez4F never confirms, so nothing should have entered yetr   )r   r   r   r8   r8   z¥once F is benched, the next-best health-eligible candidate should get evaluated and, since it confirms, entered -- this is the actual fix for the CRML-starvation bug)r&   rž   ÚassertEqualrU   r'   rv   rx   r$   r   r   r   r   )r   Úcallsr)   r   r   r   Ú@test_f_monopolizes_shortlist_until_benched_then_crml_gets_a_turnÈ   s   ÿ
ÿzfTestScanForEntriesSkipsBenchedSymbols.test_f_monopolizes_shortlist_until_benched_then_crml_gets_a_turnN)r:   r;   r<   r=   r   rž   r¡   r   r   r   r   r>   _   s
    
Ar>   Ú__main__)!r=   ÚbuiltinsÚ@py_builtinsÚ_pytest.assertion.rewriteÚ	assertionÚrewriteÚ
@pytest_arÚsysÚosÚunittestÚunittest.mockr   r   r   r   r   ÚpathÚinsertÚdirnameÚabspathÚ__file__r   Úfast_pipeliner   Úfast_prediction_enginer   Ústream_featuresr	   ÚTestCaser
   r>   r:   Úmainr   r   r   r   Ú<module>   s"    "&4 ÿ