o
    áÃªjj7  ã                   @   s  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edƒZd+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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	!	d,d"ed#ed$ed%ed&ed'ed(edefd)d*„ZdS )-a/  
breakout_scanner.py

[FEATURE 2026-09-16] Shadow/comparison scanner implementing the
"breakout_bot" candidate-scoring model from
docs/breakout_bot_design_conversation.pdf (Part II, sections 2-4) --
run ALONGSIDE sip_bot's own premarket_scanner.py every morning, writing
its own separate ranked candidate list to data/breakout_scanner/ so the
two scoring philosophies can be compared side by side on the exact same
universe and the exact same morning, without either one affecting the
other.

Deliberately scoped to ONLY the scanner piece: the PDF's 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 (stream.py / entry.py /
exit.py in the PDF's blueprint) -- none of that exists here, and this
module never runs on sip_bot's 30-minute intraday-rescan cadence.
monitor.py calls this exactly once, right after its own 9:29 premarket
scan, wrapped in a try/except there so a bug in this module can never
delay or crash the live trading pipeline.

Universe-building (get_universe_symbols / prefilter_by_snapshot /
compute_volatility_baseline, all in premarket_scanner.py) is REUSED
rather than reimplemented -- per explicit instruction, this keeps both
scanners working from the identical symbol pool so any quality
difference in the resulting candidate lists reflects the SCORING
model, not a universe-filtering discrepancy between two independently
written implementations. The PDF's own universe filters (price $5-$15,
avg daily volume >=500k, OTC/ETF/leveraged exclusion, max-5-syllable
names -- Part II section 2) are already identical to sip_bot's.

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. The
PDF's own last line says "the next engineering step is to define every
calculation mathematically" -- that was never finished before design
work stopped, so the exact formulas for premarket_price_strength and
distance_to_resistance below are this module's own concrete
interpretation (decided 2026-09-16, see that day's conversation), not a
literal transcription from the PDF. See each helper's docstring for the
specific reasoning.

Also deliberately different from sip_bot's own scorer.py: NO hard
reject gates beyond universe membership and a minimum-bars
data-availability check. sip_bot's premarket_scanner.scan() hard-rejects
on meets_min_volume/meets_min_rvol/meets_min_volatility/spread_ok; this
scanner's whole premise (per the PDF: candidate_score "answers 'which
stocks deserve our attention today' rather than 'which stock will go
up'") is a purely continuous, non-gating score -- every prefiltered
symbol with enough bars gets ranked, top N taken. This is an
intentional point of comparison against sip_bot's hard-gated approach,
not an oversight -- see 2026-09-16's RVOL-starvation incident (sip_bot
selected ZERO candidates on a thin premarket morning) for exactly the
failure mode a purely continuous score never has.
é    )ÚdatetimeÚ	timedeltaÚtimezone)Ú
get_config)Ú
get_logger)Úrelative_volumeNÚbreakout_scannerç        ç      ð?c                 C   s   t |t|| ƒƒS )N)ÚmaxÚmin)ÚxÚloÚhi© r   úbreakout_scanner.pyÚ_clampE   s   r   ÚsymbolsÚreturnc           
      C   sˆ   t ƒ d }| dd¡}t tj¡}|t|d d d� }|  |||¡}i }| ¡ D ]\}}	|	dd… }	|	s6q)t	d	d
„ |	D ƒƒ||< q)|S )a’  
    {symbol: 20-day high} -- one bulk multi-symbol daily-bars call, same
    endpoint/chunking pattern as premarket_scanner.compute_volatility_
    baseline(). Feeds the distance-to-resistance sub-score's third
    priority level (premarket high -> previous-day high -> 20-day high,
    per the PDF'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 it treats one that's below current price:
    just skip to the next priority level, never reject the symbol.
    ÚuniverseÚvolatility_lookback_daysé   é   é
   )ÚdaysiìÿÿÿNc                 s   ó   � | ]}|d  V  qdS ©ÚhNr   ©Ú.0Úbr   r   r   Ú	<genexpr>b   ó   € z)compute_range_20d_high.<locals>.<genexpr>)
r   Úgetr   Únowr   Úutcr   Úget_daily_bars_bulkÚitemsr   )
Úclientr   ÚcfgÚlookback_daysr$   ÚstartÚ
daily_barsÚoutÚsymbolÚbarsr   r   r   Úcompute_range_20d_highI   s   
r0   ÚpriceÚ
prev_closec                 C   s   |sdS | | | d S )z˜(premarket_last_price - prev_close) / prev_close -- the exact
    formula from the original design conversation's own feature table
    (Part I page 6).r	   ç      Y@r   )r1   r2   r   r   r   Ú_gap_pctf   s   r4   r/   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 )	ad  
    [2026-09-16, concrete interpretation -- see module docstring] Splits
    the premarket bar window into thirds (early/mid/late, the same
    "split premarket into thirds" idea as the PDF's own premarket_shape
    feature, Part I page 6) 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 explicit 2026-09-16
    design decision (see the PDF's Part I pages 7-9) to drop setup-type
    classification entirely: sip_bot's own backtesting found the
    elaborate composite/setup-aware score had near-zero correlation
    with outcomes, so a generic strength read doesn't need setup
    awareness either.

    Returns a % change (early-segment avg -> late-segment avg), NOT
    yet normalized to a 0-1 sub-score -- see score_breakout_candidate's
    price_strength_full_credit_pct for that.
    é   r	   é   Nc                 s   r   ©ÚcNr   r   r   r   r   r!   Œ   r"   z,_premarket_price_strength.<locals>.<genexpr>c                 s   r   r7   r   r   r   r   r   r!   �   r"   r3   )Úlenr   Úsum)r/   ÚnÚthirdÚearlyÚlateÚ	early_avgÚlate_avgr   r   r   Ú_premarket_price_strengtho   s   rA   Úpm_highÚprev_day_highÚrange_20d_highc                 C   s8   |||fD ]}|r|| kr||  | d |f  S qdS )aÆ  
    Nearest overhead level still ABOVE current price, checked in the
    PDF's own explicit 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. This mirrors sip_bot's own 2026-09-15 correction of the
    identical mistake in its old distance_from_pm_high scoring (see
    scorer.py's _note there), reached independently here rather than by
    importing that fix, per this module's "different scoring
    implementation, shared universe only" design split.
    r3   )NNr   )r1   rB   rC   rD   Úlevelr   r   r   Ú_distance_to_resistance_pct“   s
   €rF   r.   Ú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 )aN  
    bars: 1-min premarket bars, oldest first, each {"t","o","h","l","c","v"}

    Returns {"symbol", "candidate_score", "breakdown", "metrics"} -- see
    module docstring for the weighting and "no hard gates" philosophy.
    candidate_score is 0-100, same scale as sip_bot's total_score, for
    direct side-by-side comparison.
    r   Úweightsc                 S   ó   g | ]}|d  ‘qS )r8   r   r   r   r   r   Ú
<listcomp>»   ó    z,score_breakout_candidate.<locals>.<listcomp>c                 S   rJ   )Úvr   r   r   r   r   rK   ¼   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_pctr3   r   éd   r	   r   c                 S   s   i | ]
\}}|t |d ƒ“qS )r   )Úround)r   ÚkrM   r   r   r   Ú
<dictcomp>ñ   s    z,score_breakout_candidate.<locals>.<dictcomp>)r1   Ú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)r.   Úcandidate_scoreÚ	breakdownÚmetrics)
r   r:   r   r4   rA   rF   r   ÚvaluesrY   r'   )r.   r/   rB   rG   rH   r2   rC   rD   r)   ÚwÚclosesÚvolumesr1   rP   r_   r]   rc   Údist_pctÚresistance_levelrg   ÚtotalÚmax_possiblerf   r   r   r   Úscore_breakout_candidate­   s^   


ÿÿÿû

ÿóürq   é   ÚprefilteredÚvolume_baselinesÚprev_day_highsÚprev_closesÚrange_20d_highsÚlookback_hoursÚcandidate_countc                 C   sD  t ƒ d }|p| dd¡}t tj¡}	|	t|d� }
g }d}|D ]S}t |  	||
|	¡¡}t
|ƒdk r3q | |¡}|s?|d7 }q tdd	„ |D ƒƒ}td
d	„ |D ƒƒ}| |¡p\t ƒ d d }t||||||| |¡| |¡ƒ}| |¡ q |jdd„ dd� |d|… }t dt
|ƒ› dt
|ƒ› dt
|ƒ› d|› d�	¡ t |¡ |S )aõ  
    One-shot scan over an ALREADY-prefiltered symbol list (reuses
    monitor.py's own cached prefiltered universe -- see module
    docstring). No hard reject gates beyond a minimum-bars
    data-availability check and requiring a real prev_close (gap_pct's
    denominator -- unlike sip_bot's optional baselines, this one can't
    fail open since gap is a required, weighted input here); every
    other prefiltered symbol gets ranked, top candidate_count taken
    purely by candidate_score.
    r   Útop_candidate_countr   )Úhoursr   r5   r6   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"   r   Úmin_avg_daily_volumec                 S   s   | d S )Nrf   r   )Úrr   r   r   Ú<lambda>+  s    zscan.<locals>.<lambda>T)ÚkeyÚreverseNz[BREAKOUT-SCANNER] Selected z candidates (of z	 scored, z prefiltered, z  skipped for missing prev_close))r   r#   r   r$   r   r%   r   Úpremarket_scannerÚ_bars_to_dictsÚget_minute_barsr9   r   r   rq   ÚappendÚsortÚlogÚinfoÚ
data_storeÚ!write_breakout_scanner_candidates)r(   rs   rt   ru   rv   rw   rx   ry   r)   r$   r+   ÚscoredÚskipped_no_prev_closer.   r/   r2   rB   rG   rH   ÚresultÚtopr   r   r   Úscan  s>   

þÿ
ÿ
r�   )r	   r
   )rr   N)Ú__doc__r   r   r   Úconfig_loaderr   Úlogger_setupr   Ú
indicatorsr   r‚   r‰   r‡   r   ÚlistÚdictr0   Úfloatr4   rA   rF   Ústrrq   Úintr�   r   r   r   r   Ú<module>   sN    9
	$
ÿÿÿþþ
þXþÿÿÿþþ