o
    ‹Þ¶j[/  ã                   @   sD  d Z ddlmZmZmZ ddlmZ ddlmZ ddlm	Z	 ddl
Z
ddlZddlZedƒZd3d
d„Z		d4dedededefdd„Zdefdd„Z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dedefd d!„Zd"ededed#ed$ededededefd%d&„Z	'	(d5d)ed*ed+ed,ed-ed.ed/ed0edefd1d2„ZdS )6að  
scanner.py

The candidate-scoring model from docs/breakout_bot_design_conversation.
pdf (Part II, sections 2-4). Ported from screener/premarket's
breakout_scanner.py (which ran this same scoring as a shadow/comparison
scanner alongside sip_bot) -- here it's the primary and only scanner
for this project, not a comparison run.

Deliberately scoped to ONLY the scanner piece, per "build piece by
piece, checking performance as we go": the full breakout_bot design
also calls for a 1-min-bar / 5-min-rolling-window state machine (WATCH
-> BUILDING -> PRE_BREAKOUT -> BREAKOUT -> CONFIRMED) driven by live
streaming data, plus independent entry/exit modules -- none of that
exists yet. This module only answers "which stocks deserve attention
today," never "which stock will go up" or "buy this."

CANDIDATE SCORE weighting is the PDF's own (Part II section 3): 30%
relative volume, 20% premarket volume, 20% gap/price movement, 15%
premarket price strength, 15% distance to important resistance.

Deliberately NO hard reject gates beyond universe membership (see
universe.py) and a minimum-bars data-availability check -- every
prefiltered symbol with enough bars gets ranked, top N taken. This is
an intentional design choice, not an oversight: sip_bot's own
premarket_scanner.py hard-rejects on RVOL/spread/volatility floors,
which on a genuinely thin market-wide day (2026-09-16) rejected all
540 prefiltered symbols and produced ZERO candidates for the session.
A purely continuous score never has that failure mode.
é    )ÚdatetimeÚ	timedeltaÚtimezone)Ú
get_config)Ú
get_logger)Úrelative_volumeNÚscannerç        ç      ð?c                 C   s   t |t|| ƒƒS ©N)ÚmaxÚmin)ÚxÚloÚhi© r   ú#/var/www/screener/trade1/scanner.pyÚ_clamp,   s   r   ÚsymbolsÚdaily_atr_outÚ	ref5d_outÚreturnc                    sÚ   t  tj¡}|tdd� }|  |||¡}i }t  tj¡ ¡ ‰ | ¡ D ]H\}}	|dur>‡ fdd„|	D ƒ}
t 	|
¡}|r>|||< |durV‡ fdd„|	D ƒ}t 
|¡}|rV|||< |	dd… }	|	s_q"tdd	„ |	D ƒƒ||< q"|S )
a2  
    {symbol: 20-day high} -- one bulk multi-symbol daily-bars call.
    Feeds the distance-to-resistance sub-score's third priority level
    (premarket high -> previous-day high -> 20-day high, per the
    design doc's resistance priority order, Part I page 16 / Part II
    section 10).

    A symbol absent from the result (too new, delisted, or the request
    failed) is simply missing -- _distance_to_resistance_pct() treats a
    missing level the same as one that's below current price: skip to
    the next priority level, never reject the symbol.
    é(   )ÚdaysNc                    ó   g | ]
}t |ƒˆ k r|‘qS r   ©Ú	_bar_date©Ú.0Úb©Útodayr   r   Ú
<listcomp>I   ó    z*compute_range_20d_high.<locals>.<listcomp>c                    r   r   r   r   r    r   r   r"   N   r#   iìÿÿÿc                 s   ó   � | ]}|d  V  qdS ©ÚhNr   r   r   r   r   Ú	<genexpr>U   ó   € z)compute_range_20d_high.<locals>.<genexpr>)r   Únowr   Úutcr   Úget_daily_bars_bulkÚdateÚitemsÚ
volatilityÚ	daily_atrÚfive_day_referencer   )Úclientr   r   r   r)   ÚstartÚ
daily_barsÚoutÚsymbolÚbarsÚdoneÚdatrÚdone5Úrefr   r    r   Úcompute_range_20d_high0   s*   

r;   c                 C   s‚   ddl m} tƒ d }t| d dƒr| d nt t| d ƒ¡}| ||d ƒ¡}dd„ |d	  d
¡D ƒ\}}}|j	|j
f||fk S )Nr   )ÚZoneInfoÚscheduleÚtÚ
astimezoner   c                 s   s   � | ]}t |ƒV  qd S r   )Úint)r   r   r   r   r   r'   ^   r(   z _is_premarket.<locals>.<genexpr>Úmarket_open_timeú:)Úzoneinfor<   r   Úhasattrr   ÚfromisoformatÚstrr?   ÚsplitÚhourÚminute)r   r<   Úschedr>   Út_etÚohÚomÚ_r   r   r   Ú_is_premarketY   s   
(rO   c                 C   s.   |   d¡}t|dƒr| ¡ S t t|ƒ¡ ¡ S )Nr>   r,   )ÚgetrD   r,   r   rE   rF   )r   r>   r   r   r   r   b   s   
$r   ÚpriceÚ
prev_closec                 C   s   |sdS | | | d S )z1(premarket_last_price - prev_close) / prev_close.r	   ç      Y@r   )rQ   rR   r   r   r   Ú_gap_pctg   s   rT   r6   c                 C   sˆ   t | ƒ}|dk r
dS td|d ƒ}| d|… }| | d… }tdd„ |D ƒƒt |ƒ }tdd„ |D ƒƒt |ƒ }|s<dS || | d S )	añ  
    Splits the premarket bar window into thirds (early/mid/late) and
    compares the late segment's average close against the early
    segment's -- a steady upward trend across the whole premarket
    session scores well; flat or declining scores near zero.

    Deliberately does NOT try to distinguish a steady climb from a
    dip-then-reverse-up (V) shape -- both trend up from early to late
    and would score similarly. This matches the design doc's decision
    to drop setup-type classification entirely (sip_bot's own
    backtesting found the elaborate composite/setup-aware score had
    near-zero correlation with outcomes).

    Returns a % change (early-segment avg -> late-segment avg), NOT
    yet normalized to a 0-1 sub-score.
    é   r	   é   Nc                 s   r$   ©ÚcNr   r   r   r   r   r'   …   r(   z,_premarket_price_strength.<locals>.<genexpr>c                 s   r$   rW   r   r   r   r   r   r'   †   r(   rS   )Úlenr   Úsum)r6   ÚnÚthirdÚearlyÚlateÚ	early_avgÚlate_avgr   r   r   Ú_premarket_price_strengthn   s   ra   Úpm_highÚprev_day_highÚrange_20d_highc                 C   s8   |||fD ]}|r|| kr||  | d |f  S qdS )ar  
    Nearest overhead level still ABOVE current price, checked in
    priority order (premarket high -> previous-day high -> 20-day
    high). Returns (distance_pct, level_used) -- distance_pct is None
    if price has already cleared every known level ("clear air"): a
    breakout already in progress shouldn't be penalized for having no
    ceiling left to measure against, so that case is full credit at
    the call site rather than a zero/undefined distance.

    More room below the nearest ceiling scores BETTER here, not worse
    -- sitting right at a resistance level is the risky spot, not the
    strong one.
    rS   ©NNr   )rQ   rb   rc   rd   Úlevelr   r   r   Ú_distance_to_resistance_pctŒ   s
   €rg   r5   Úpm_lowÚavg_vol_baselinec                 C   sš  t ƒ d }|d }	dd„ |D ƒ}
dd„ |D ƒ}|
d }t|ƒ}t||ƒ}t||ƒ}t|ƒ}t||||ƒ\}}|	d t||d  ƒ |	d	 t||d
  ƒ |	d t||d  ddƒ |	d t||d  ddƒ dœ}|du rt|	d |d< n|	d t||d  ƒ |d< t| ¡ ƒ}t|	 ¡ ƒ}|ršt|| d ddƒnd}| t|dƒdd„ | 	¡ D ƒ||t|dƒ||t|dƒ||||t|dƒ|durÆt|dƒnd|dœdœS )z´
    bars: 1-min premarket bars, oldest first, each {"t","o","h","l","c","v"}

    Returns {"symbol", "candidate_score", "breakdown", "metrics"}.
    candidate_score is 0-100.
    r   Úweightsc                 S   ó   g | ]}|d  ‘qS )rX   r   r   r   r   r   r"   ­   ó    z,score_breakout_candidate.<locals>.<listcomp>c                 S   rk   )Úvr   r   r   r   r   r"   ®   rl   éÿÿÿÿr   Úrvol_full_credit_multipleÚpremarket_volumeÚpremarket_volume_full_creditÚgap_or_price_movementÚgap_full_credit_pctg      ð¿r
   Úpremarket_price_strengthÚprice_strength_full_credit_pct)r   rp   rr   rt   NÚdistance_to_resistanceÚ#resistance_full_credit_distance_pctrS   r   éd   r	   é   c                 S   s   i | ]
\}}|t |d ƒ“qS )ry   )Úround)r   Úkrm   r   r   r   Ú
<dictcomp>Ñ   r#   z,score_breakout_candidate.<locals>.<dictcomp>)rQ   Úprevious_closeÚgap_pctrp   Úavg_daily_volumeÚrvolÚpremarket_highÚpremarket_lowÚprevious_day_highrd   Úprice_strength_pctÚdistance_to_resistance_pctÚresistance_level_used)r5   Úcandidate_scoreÚ	breakdownÚmetrics)
r   rZ   r   rT   ra   rg   r   Úvaluesrz   r-   )r5   r6   rb   rh   ri   rR   rc   rd   ÚcfgÚwÚclosesÚvolumesrQ   rp   r€   r~   r„   Údist_pctÚresistance_levelrˆ   ÚtotalÚmax_possibler‡   r   r   r   Úscore_breakout_candidate¡   s^   
	

ÿÿÿû

ÿóür“   é   Ú ÚprefilteredÚvolume_baselinesÚprev_day_highsÚprev_closesÚrange_20d_highsÚlookback_hoursÚcandidate_countÚcandidates_file_suffixc	                 C   s†  t ƒ d }	|p|	 dd¡}t tj¡}
|
t|d� }g }d}|D ]r}t |  	|||
¡¡}t
|ƒdk r3q | |¡}|s?|d7 }q tdd	„ |D ƒƒ}td
d	„ |D ƒƒ}| |¡p\t ƒ d d }t||||||| |¡| |¡ƒ}|	 d¡r�dd„ |D ƒ}|r…tdd	„ |D ƒƒnd| di ¡d< | |¡ q |jdd„ dd� |d|… }t dt
|ƒ› dt
|ƒ› dt
|ƒ› d|› d�	¡ tj||d� |S )a/  
    One-shot scan over an already-prefiltered symbol list. No hard
    reject gates beyond a minimum-bars data-availability check and
    requiring a real prev_close (gap_pct's denominator) -- every other
    prefiltered symbol gets ranked, top candidate_count taken purely by
    candidate_score.
    r   Útop_candidate_counté   )Úhoursr   rU   rV   c                 s   r$   r%   r   r   r   r   r   r'   ÿ   r(   zscan.<locals>.<genexpr>c                 s   r$   )ÚlNr   r   r   r   r   r'      r(   ÚuniverseÚmin_avg_daily_volumeÚtrue_premarket_highc                 S   s   g | ]}t |ƒr|‘qS r   )rO   r   r   r   r   r"     s    zscan.<locals>.<listcomp>c                 s   r$   r%   r   r   r   r   r   r'     r(   Nr‰   r�   c                 S   s   | d S )Nr‡   r   )Úrr   r   r   Ú<lambda>  s    zscan.<locals>.<lambda>T)ÚkeyÚreversez[SCANNER] Selected z candidates (of z	 scored, z prefiltered, z  skipped for missing prev_close))Úsuffix)r   rP   r   r)   r   r*   r   r¢   Ú_bars_to_dictsÚget_minute_barsrY   r   r   r“   Ú
setdefaultÚappendÚsortÚlogÚinfoÚ
data_storeÚwrite_candidates)r1   r–   r—   r˜   r™   rš   r›   rœ   r�   r‹   r)   r2   ÚscoredÚskipped_no_prev_closer5   r6   rR   rb   rh   ri   ÚresultÚpm_barsÚtopr   r   r   Úscanä   sF   


þ
ÿÿ
ÿr¸   )r	   r
   re   )r”   Nr•   )Ú__doc__r   r   r   Úconfig_loaderr   Úlogger_setupr   Ú
indicatorsr   r¢   r±   r.   r¯   r   ÚlistÚdictr;   ÚboolrO   r   ÚfloatrT   ra   rg   rF   r“   r@   r¸   r   r   r   r   Ú<module>   sd    
ÿÿ
ÿ)	
ÿÿÿþþ
þDþÿÿÿþþþ