o
    ü™jÅ<  ã                
   @   s  d Z ddlmZ dedefdd„Zdedefdd„ZdMded
edefdd„Zdededefdd„Z	dededefdd„Z
dNdededefdd„ZdNde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fd d!„ZdOded
edefd#d$„ZdPded
edefd&d'„Zd(ed)edefd*d+„Zd(ed,edefd-d.„Zd/ed0edefd1d2„Zded3ed4ed5edef
d6d7„Zded8edefd9d:„Zdedefd;d<„Zdedefd=d>„Zd?ed@edefdAdB„ZdOded
edefdCdD„ZdedefdEdF„ZdedGedHedIedef
dJdK„ZdLS )QuE  
indicators.py

Pure functions for VWAP, EMA, ATR, slope, and price-structure detection
(higher highs/lows, consolidation, extension). Shared by scorer.py
(premarket scoring) and entry_engine.py (opening confirmation) so both
stages use identical math â€” the whole point of "confirm the prediction"
is that premarket and open must be measuring the same things.

All functions take plain lists/dicts of bar-like data:
    bar = {"t": timestamp, "o": open, "h": high, "l": low, "c": close, "v": volume}
so they work identically against Alpaca bars or synthetic premarket
snapshots.
é    )ÚmeanÚbarsÚreturnc                 C   sn   d}d}| D ]}|d |d  |d  d }|||d  7 }||d 7 }q|dkr3| r1| d d S dS || S )	z6Volume-weighted average price over the given bar list.ç        ÚhÚlÚcç      @Úvr   éÿÿÿÿ© )r   Útotal_pvÚtotal_vÚbÚtypicalr   r   úF/var/www/screener/trade/premarket_backup_2026-09-08_2010/indicators.pyÚvwap   s   r   c                 C   sp   g }d}d}| D ]-}|d |d  |d  d }|||d  7 }||d 7 }|  |dkr0|| n|d ¡ q|S )z7Running VWAP value at each bar (for slope calculation).r   r   r   r   r	   r
   r   ©Úappend)r   ÚoutÚcum_pvÚcum_vr   r   r   r   r   Úvwap_series    s    r   é   Úlookbackc                 C   s0   t | ƒ}t|ƒ|d k rdS |d |d|   S )z6Positive => VWAP rising over the last `lookback` bars.é   r   r   )r   Úlen)r   r   Úseriesr   r   r   Ú
vwap_slope-   s   r   ÚvaluesÚperiodc                 C   sF   | sdS d|d  }| d }| dd … D ]}|| |d|   }q|S )Nr   é   r   r   r   )r   r    ÚkÚer
   r   r   r   Úema5   s   r$   c                 C   sR   | sg S d|d  }| d g}| dd … D ]}|  || |d d|   ¡ q|S )Nr!   r   r   r   r   )r   r    r"   r   r
   r   r   r   Ú
ema_series?   s   
 r%   é   c           	      C   s¬   t | ƒdk rdS g }tdt | ƒƒD ]-}| | d | | d | |d  d }}}t|| t|| ƒt|| ƒƒ}| |¡ qt |ƒ|krL|| d… n|}|rTt|ƒS dS )z<Average True Range using close-to-close/high-low true range.r!   r   r   r   r   r   N)r   ÚrangeÚmaxÚabsr   r   )	r   r    ÚtrsÚir   r   Úprev_cÚtrÚwindowr   r   r   ÚatrI   s   , r/   c           	         sî   dd„ | D ƒ‰ t ˆ ƒ|d k rdS ‡ fdd„tdt ˆ ƒƒD ƒ}dd„ |D ƒ}dd„ |D ƒ}t|d|… ƒ}t|d|… ƒ}t|t |ƒƒD ]}||d  ||  | }||d  ||  | }qD|d	krk|d	krid
S dS || }d
d
d|   S )a‚  
    [FEATURE 2026-08-26] Relative Strength Index, using Wilder's original
    smoothing method (the standard definition -- an exponential-style
    average of gains/losses, not a plain rolling mean) over closing
    prices. Returns a 0-100 value; readings above ~70 are the classic
    "extended, overbought" signal, below ~30 "oversold." Needs at least
    period+1 closes to produce a real value; returns a neutral 50.0
    with too little data, since "unknown" shouldn't silently read as
    either overbought or oversold.

    This is a genuinely different measurement from price_slope/
    momentum_slope elsewhere in this codebase: those measure recent
    DIRECTION over a short (8-bar) lookback; RSI normalizes the
    balance of gains vs. losses over a longer window into a bounded
    scale, closer to "how stretched is this move," not just "which
    way is it currently pointing."
    c                 S   ó   g | ]}|d  ‘qS ©r   r   ©Ú.0r   r   r   r   Ú
<listcomp>h   ó    zrsi.<locals>.<listcomp>r   g      I@c                    s    g | ]}ˆ | ˆ |d    ‘qS )r   r   ©r3   r+   ©Úclosesr   r   r4   l   s     c                 S   s   g | ]
}|d kr
|nd‘qS ©r   r   r   ©r3   Údr   r   r   r4   m   s    c                 S   s   g | ]}|d k r| nd‘qS r9   r   r:   r   r   r   r4   n   s    Nr   ç      Y@ç      ð?)r   r'   r   )	r   r    ÚdeltasÚgainsÚlossesÚavg_gainÚavg_lossr+   Úrsr   r7   r   ÚrsiV   s   rD   Úcurrent_volumeÚavg_volume_baselinec                 C   s   |dkrdS | | S )Nr   r   r   )rE   rF   r   r   r   Úrelative_volume€   s   rG   Úvolumesc                 C   sL   t | ƒ}|dk r
dS |d }t| d|… ƒpd}t| |d… ƒp!d}|| S )zÀ
    Ratio of the average of the most recent half of the window vs the
    earlier half. >1 means volume is accelerating (matches the project
    spec's "developing" vs "fading" example).
    é   r=   r!   Ng•Ö&è.>r   )r   r   )rH   ÚnÚmidÚearlyÚrecentr   r   r   Úvolume_acceleration†   s   rN   r8   c                 C   s8   t | ƒdk s| d dkrdS | d | d  | d  d S )z>Simple normalized momentum: % change from first to last close.r!   r   r   r   r<   )r   r7   r   r   r   Úprice_momentum•   s   rO   rI   c                    sÌ   t | ƒ|k rdS | | d… }dd„ |D ƒ‰ dd„ |D ƒ‰t‡ fdd„tdt ˆ ƒƒD ƒƒ}t‡fd	d„tdt ˆƒƒD ƒƒ}t |ƒd }tdt|d
 ƒƒ}ˆ d ˆ d ko[ˆd ˆd k}||koe||koe|S )uÚ   
    True if, over the last `lookback` bars, each bar's high is >= the
    prior bar's high (allowing 1 tick tolerance) and lows are trending
    up overall â€” i.e. a healthy stairstep rather than a single spike.
    FNc                 S   r0   ©r   r   r2   r   r   r   r4   ¥   r5   z/is_higher_highs_higher_lows.<locals>.<listcomp>c                 S   r0   ©r   r   r2   r   r   r   r4   ¦   r5   c                 3   ó(   � | ]}ˆ | ˆ |d   krd V  qdS ©r   Nr   r6   ©Úhighsr   r   Ú	<genexpr>§   ó   €& z.is_higher_highs_higher_lows.<locals>.<genexpr>r   c                 3   rR   rS   r   r6   ©Úlowsr   r   rV   ¨   rW   çffffffæ?r   r   ©r   Úsumr'   r(   Úround)r   r   r.   Úhigher_highsÚhigher_lowsÚstepsÚ	thresholdÚ
net_higherr   ©rU   rY   r   Úis_higher_highs_higher_lowsœ   s      rd   é   c                 C   sŒ   t | ƒdk rdS t | ƒ|kr| | d… n| }dd„ |D ƒ}dd„ |D ƒ}dd„ |D ƒ}|r2t|ƒnd}|d	kr:dS t|ƒt|ƒ | d S )
z‹
    Returns range-as-percent-of-price over the lookback window.
    Lower = tighter consolidation (generally healthier pre-breakout).
    r!   r<   Nc                 S   r0   rP   r   r2   r   r   r   r4   º   r5   z+consolidation_tightness.<locals>.<listcomp>c                 S   r0   rQ   r   r2   r   r   r   r4   »   r5   c                 S   r0   r1   r   r2   r   r   r   r4   ¼   r5   r=   r   )r   r   r(   Úmin)r   r   r.   rU   rY   r8   Ú	avg_pricer   r   r   Úconsolidation_tightness²   s   rh   Úcurrent_priceÚreference_highc                 C   s   |dkrdS ||  | d S ©Nr   r   r<   r   )ri   rj   r   r   r   Údistance_from_high_pctÃ   ó   rl   Ú
vwap_valuec                 C   s   |dkrdS | | | d S rk   r   )ri   rn   r   r   r   Úextension_from_vwap_pctÉ   rm   ro   ÚbidÚaskc                 C   s<   | dks|dkr
dS | | d }|dkrdS ||  | d S )Nr   r<   ç       @r   )rp   rq   rK   r   r   r   Ú
spread_pctÏ   s   rs   Ú
resistanceÚ
buffer_pctÚ	hold_barsc                    s@   t | ƒ|k rdS |d|d   ‰ t‡ fdd„| | d… D ƒƒS )u»   
    True if the last `hold_bars` closes have all been above
    resistance * (1 + buffer_pct/100) â€” i.e. the breakout held rather
    than spiking through and immediately failing.
    Fr   r<   c                 3   s   � | ]	}|d  ˆ kV  qdS )r   Nr   r2   ©Útriggerr   r   rV   á   ó   € z%breakout_confirmed.<locals>.<genexpr>N)r   Úall)r   rt   ru   rv   r   rw   r   Úbreakout_confirmedØ   s    r{   Úmax_retrace_pctc           	      C   sÈ   t | ƒdk rdS dd„ | D ƒ}t |ƒdkr | t|dd… ƒ¡nd}|dks.|t |ƒd kr0dS || }||d… rBt||d… ƒn|}||d  }|dkrPdS || | d	 }|d |k}||koc|S )
zù
    Detects: price made a local high, pulled back no more than
    max_retrace_pct of the prior up-move, then resumed making new
    highs. Used to allow "healthy pullback followed by continuation"
    entries rather than only fresh breakouts.
    re   Fc                 S   r0   r1   r   r2   r   r   r   r4   í   r5   z.pullback_then_continuation.<locals>.<listcomp>r   Nr   r   r<   )r   Úindexr(   rf   )	r   r|   r8   Úpeak_idxÚpeakÚtroughÚmove_upÚretraceÚresumedr   r   r   Úpullback_then_continuationä   s   & r„   c                    s€   t | ƒ}|dk r
dS tt|ƒƒ}t|ƒ| ‰ t| ƒ| ‰t‡ ‡fdd„t|| ƒD ƒƒ}t‡ fdd„|D ƒƒ}|dkr<dS || S )u,  
    Least-squares linear regression slope of `values` against bar index
    0..n-1 (i.e. "change in value per bar"), using ALL points in the
    window rather than just comparing the first and last one â€” a single
    noisy bar at either end can't swing this the way a two-point delta
    can.
    r!   r   c                 3   s$   � | ]\}}|ˆ  |ˆ  V  qd S )Nr   )r3   ÚxÚy©Úmean_xÚmean_yr   r   rV     s   €" zlinreg_slope.<locals>.<genexpr>c                 3   s   � | ]	}|ˆ  d  V  qdS )r!   Nr   )r3   r…   )rˆ   r   r   rV     ry   r   )r   Úlistr'   r\   Úzip)r   rJ   ÚxsÚnumÚdenr   r‡   r   Úlinreg_slope   s   r�   c                 C   s4   | sdS t | ƒt| ƒ }|dkrdS t| ƒ| d S )uø   
    Regression slope expressed as a percentage of the series' own mean,
    per bar â€” makes slope comparable across stocks at very different
    price levels (a $6 stock and a $14 stock can both be scored on the
    same "% per bar" scale).
    r   r   r<   )r\   r   r�   )r   r‰   r   r   r   Únormalized_slope_pct  s   r�   Ú	slope_pctÚflat_threshold_pctc                 C   s   | |krdS | | k rdS dS )zÇ
    Buckets a normalized slope (see normalized_slope_pct) into
    "positive" / "flat" / "negative" against a symmetric dead-zone
    around zero, so small noise doesn't get labeled as a trend.
    ÚpositiveÚnegativeÚflatr   )r‘   r’   r   r   r   Úclassify_slope$  s
   
r–   c                    sÌ   t | ƒ|k rdS | | d… }dd„ |D ƒ‰ dd„ |D ƒ‰t‡ fdd„tdt ˆ ƒƒD ƒƒ}t‡fd	d„tdt ˆƒƒD ƒƒ}t |ƒd }tdt|d
 ƒƒ}ˆ d ˆ d k o[ˆd ˆd k }||koe||koe|S )u  
    The deteriorating-structure mirror of is_higher_highs_higher_lows():
    True if a clear majority of steps in the window make a strictly
    LOWER high and a strictly lower low, AND the window is net lower
    overall (first vs last) â€” same net-direction guard used on the
    healthy-structure check, so a choppy down-up-down sequence can't
    pass on step-count alone.
    FNc                 S   r0   rP   r   r2   r   r   r   r4   =  r5   z-is_lower_highs_lower_lows.<locals>.<listcomp>c                 S   r0   rQ   r   r2   r   r   r   r4   >  r5   c                 3   ó(   � | ]}ˆ | ˆ |d   k rd V  qdS rS   r   r6   rT   r   r   rV   ?  rW   z,is_lower_highs_lower_lows.<locals>.<genexpr>r   c                 3   r—   rS   r   r6   rX   r   r   rV   @  rW   rZ   r   r   r[   )r   r   r.   Úlower_highsÚ
lower_lowsr`   ra   Ú	net_lowerr   rc   r   Úis_lower_highs_lower_lows1  s   	   r›   c                 C   sp   t | ƒ}|dk r
dS t| ƒ}|d d }t| ƒ| }|||  }|}|||d   }|dkr0dS || | d S )u“  
    Percent change implied by a least-squares regression line fit across
    `values`, from the fitted line's value at the first point to its
    value at the last â€” NOT (values[-1] - values[0]) / values[0], which
    is exactly the two-point noise problem linreg_slope()'s docstring
    already warns about for a raw first/last delta.

    This is deliberately a different shape of number from
    normalized_slope_pct(): that one is "%-per-bar," which shrinks in
    magnitude as the window gets longer (the same total move spread over
    more bars) â€” useful for classify_slope()'s per-bar dead-zone, but not
    for comparing "how far did this move, net" across windows of very
    different lengths. Using the fitted line's own endpoints instead of
    the raw first/last bars keeps that comparison meaningful at any
    window length while still being robust to a single noisy bar at
    either end.
    r!   r   r   rr   r   r<   )r   r�   r\   )r   rJ   Úsloperˆ   r‰   Ú	interceptÚ	fit_startÚfit_endr   r   r   Úregression_fit_pct_changeW  s   r    Úmin_barsÚmin_decline_pctÚrecovery_threshold_pctc           	      C   s\   t | ƒ|k rdS dd„ | D ƒ}|d }tdd„ | D ƒƒ}t|ƒ}t||ƒ}|| ko-||kS )uE  
    True only when BOTH hold â€” same "signals must agree" pattern as
    intraday_health._is_severe():

      - regression_fit_pct_change() over ALL of `bars` (the full session
        so far, not a short lookback) is down at least min_decline_pct â€”
        the day, net, is still heading down, not just the last few bars.
      - current price remains at least recovery_threshold_pct below the
        highest high anywhere in that same history â€” a bounce that has
        already reclaimed the session high isn't "still in a downtrend"
        no matter what the regression over the whole window says, so
        this can't fire on a name that's already fully recovered.

    Requires at least min_bars of bars; with less history than that,
    "the session's trend so far" isn't a meaningful question yet (a
    stock 3 minutes off the open has no session trend), so this fails
    safe to False rather than judging a fresh candidate on noise â€”
    same fail-safe-to-normal-behavior pattern used elsewhere in this
    project (e.g. entry_engine._apply_time_window_overrides()).
    Fc                 S   r0   r1   r   r2   r   r   r   r4   �  r5   z)is_extended_downtrend.<locals>.<listcomp>r   c                 s   s   � | ]}|d  V  qdS )r   Nr   r2   r   r   r   rV   ‘  s   € z(is_extended_downtrend.<locals>.<genexpr>)r   r(   r    rl   )	r   r¡   r¢   r£   r8   ri   Úsession_highÚfit_pct_changeÚdist_from_highr   r   r   Úis_extended_downtrendw  s   
r§   N)r   )r&   )rI   )re   )Ú__doc__Ú
statisticsr   rŠ   Úfloatr   r   Úintr   r$   r%   r/   rD   rG   rN   rO   Úboolrd   rh   rl   ro   rs   r{   r„   r�   r�   Ústrr–   r›   r    r§   r   r   r   r   Ú<module>   s:    

*	& ÿÿ