o
    p£j%'  ã                
   @   sÂ   d 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 G dd„ dejƒZG d	d
„ d
ejƒZedkr_e ¡  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)ÚFastPredictionReadingc                   @   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   ú(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_threshold1   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_failures6   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_elapsesA   r0   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_streakG   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_benchP   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   ÚCRMLr5   r'   r   r   r   Ú&test_symbols_are_tracked_independentlyW   s   zBTestEntryAttemptTrackerUnit.test_symbols_are_tracked_independentlyN)Ú__name__Ú
__module__Ú__qualname__Ú__doc__r   r$   r)   r/   r3   r4   r6   r8   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   r7   r   g…ëQ¸îR@)ÚsymbolÚtotal_scorer7   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   ©Ú_selfrI   r   r   r   Úis_symbol_openƒ   ó   zWTestScanForEntriesSkipsBenchedSymbols.setUp.<locals>.FakePositionManager.is_symbol_openc                 S   rK   )NTr   )rN   r   r   r   Úhas_available_slot†   rP   z[TestScanForEntriesSkipsBenchedSymbols.setUp.<locals>.FakePositionManager.has_available_slotc                 S   rK   rL   r   rM   r   r   r   Úhas_closed_position_today‰   rP   zbTestScanForEntriesSkipsBenchedSymbols.setUp.<locals>.FakePositionManager.has_closed_position_todayc                    s   ˆ j  |¡ d S ©N)ÚenteredÚappend)rN   rI   Úentry_priceÚequityÚbarsÚreasonr   r   r   Úenter_positionŒ   s   zWTestScanForEntriesSkipsBenchedSymbols.setUp.<locals>.FakePositionManager.enter_positionN)r9   r:   r;   rO   rQ   rR   rZ   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   rK   rL   r   rM   r   r   r   Úis_symbol_stale�   rP   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%   rM   r   rg   r   Úget_bars“   s   
ÿzHTestScanForEntriesSkipsBenchedSymbols.setUp.<locals>.FakeStream.get_barsc                 S   s   g S rS   r   rM   r   r   r   Úget_bars_sub˜   rP   zLTestScanForEntriesSkipsBenchedSymbols.setUp.<locals>.FakeStream.get_bars_subc                 S   s   d S rS   r   rM   r   r   r   Ú	get_quote›   rP   zITestScanForEntriesSkipsBenchedSymbols.setUp.<locals>.FakeStream.get_quoteN)r9   r:   r;   r\   rj   rk   rl   r   r   r   r   Ú
FakeStream�   s
    rm   ÚWATCHç     €V@)ÚstateÚhealth_scoreç     €Q@)r   ÚSessionOrchestratorÚ__new__ÚorchÚcfgr   Ú_entry_trackerÚ_fast_engine_stateÚ
simulationÚclientÚ_opening_volume_baselineÚpremarket_20rT   Úposition_mgrÚstreamÚhealth_state)r   r[   rm   r   r   r   r   i   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 )NrD   c	                 S   s&   | dkrdnd}	t | |	d�}
|
tƒ fS )Nr   ro   rr   )rI   Ú
confidence)r   r   )rI   rX   Úbars_subÚquoteÚbaselinerp   Úsub_bucket_secondsÚas_ofÚpremarket_resultr€   Ú
predictionr   r   r   Úfake_compute_fast_ranking«   s   
zWTestScanForEntriesSkipsBenchedSymbols._run_one_cycle.<locals>.fake_compute_fast_rankingc                    s   ˆ   | ¡ t| | dkdd�S )Nr7   g        )rI   Úshould_enterÚconfirmation_score)rU   r   )rI   r‡   Úfeaturesrp   r…   ©Úevaluate_entry_callsr   r   Úfake_evaluate_fast_entry_only´   s   

ÿz[TestScanForEntriesSkipsBenchedSymbols._run_one_cycle.<locals>.fake_evaluate_fast_entry_onlyÚ
data_storerE   Úcompute_fast_ranking)Úside_effectÚevaluate_fast_entry_onlyTrn   )Ú	raw_state)NrD   NN)NN)r   Úobjectr   r   Úload_health_stateÚreturn_valueÚis_eligible_for_entryr   Úcompute_healthru   Ú_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   r7   r7   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œ   ÚassertEqualrT   r&   ru   rw   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)r9   r:   r;   r<   r   rœ   rŸ   r   r   r   r   r=   ^   s
    
Ar=   Ú__main__)r<   ÚsysÚosÚunittestÚunittest.mockr   r   r   r   r   ÚpathÚinsertÚdirnameÚabspathÚ__file__r   Úfast_pipeliner   Úfast_prediction_enginer   ÚTestCaser	   r=   r9   Úmainr   r   r   r   Ú<module>   s     &4 ÿ