o
    êœ j1(  ã                   @   s4  d Z ddlmZmZ ddlmZ dddddœd	d
ddddœddd	di ddd	dœdœ	ZdZdZdZ	dZ
dZeG dd„ dƒƒZd7dededed efd!d"„ZeG d#d$„ d$ƒƒZd%ed efd&d'„Zd(ed)efd*d+„Zd(ed efd,d-„Zd.eded/efd0d1„Z				d8d.ed(ededed2ed3ed4ed/eded efd5d6„ZdS )9a  
relative_strength_engine.py

[Phase 3 -- new] Compares a candidate's own return against the broad market
(SPY/QQQ) and, where a sector/theme mapping exists, against its peer group --
per the project's explicit requirement that an isolated "ticker is green" is
weaker evidence than the same move confirmed by sector-wide participation
(SMR strongly positive + OKLO/URA/nuclear peers positive + SPY/QQQ flat is a
materially stronger signal than SMR green in isolation).

Pure function, no state, no network calls. This module is handed bars for
the symbol AND for SPY/QQQ/peers by the CALLER -- monitor.py owns whatever
market-data subscriptions are needed to get those bars, the same separation
of concerns every other engine here already follows (structure_engine/
trend_engine don't fetch their own bars either, they're handed them).

SECTOR/PEER MAPPING IS A STATIC, CONFIGURED MAP
(config.relative_strength_engine.sector_map) -- there is no dynamic sector-
classification data source anywhere in this codebase, and fabricating one
would violate this project's explicit no-manufactured-data rule (see
prediction_engine.py's own relative_strength docstring for the same
principle applied to the individual-symbol case). A symbol with no entry in
the map simply skips the sector layer -- rs_score vs SPY/QQQ still applies
in full. Per the project's explicit instruction, sector confirmation is an
ADDITIONAL confidence multiplier, never a requirement: an unmapped symbol,
or one whose peers didn't confirm, is not penalized for it, only left
without the bonus.
é    )Ú	dataclassÚfield)Ú
get_configé   é   é   é   )Ú1mÚ3mÚ5mÚ15mg333333Ã?g      Ð?g333333Ó?gš™™™™™É?gš™™™™™¹?)r	   r
   r   r   Ú	premarketç       @g333333ã?r   g      à?)ÚstrongÚmild)	ÚwindowsÚwindow_weightsÚrs_scale_pctÚsector_breadth_min_for_bonusÚsector_confirmation_bonusÚsector_peer_windowÚ
sector_mapÚmarket_regime_windowÚmarket_regime_thresholdsÚSTRONG_RISK_ONÚRISK_ONÚNEUTRALÚRISK_OFFÚSTRONG_RISK_OFFc                   @   sJ   e Zd ZU eZeed< dZeed< dZ	eed< dZ
eed< dZeed< dS )ÚMarketRegimeReadingÚregimeNÚspy_return_pctÚqqq_return_pctÚcomposite_return_pctFÚinsufficient_data)Ú__name__Ú
__module__Ú__qualname__r   r    ÚstrÚ__annotations__r!   Úfloatr"   r#   r$   Úbool© r,   r,   ú7/var/www/screener/premarket/relative_strength_engine.pyr   7   s   
 r   NÚspy_barsÚqqq_barsÚcfgÚreturnc           	      C   sâ   i t ¥|ptƒ  di ¡¥}|d  |d d¡}t| |ƒ}t||ƒ}|du s)|du r.tdd�S || d }|d	 }||d
 krAt}n||d krJt}n||d
  krTt}n||d  kr^t}nt	}t|t
|dƒt
|dƒt
|dƒd�S )a¾  
    Broad-market context (section 19): STRONG_RISK_ON..STRONG_RISK_OFF from
    SPY/QQQ's own recent composite return. Deliberately NOT a hard filter
    anywhere downstream -- per the project's explicit instruction, a strong
    symbol outperforming a weak market should score BETTER for its relative
    strength, not be rejected for the market being weak. See winner_score.py
    for how this is actually used (a small, non-veto weight).
    Úrelative_strength_enginer   r   r   NT)r$   r   r   r   r   r   )r    r!   r"   r#   )ÚDEFAULT_CONFIGr   ÚgetÚ_window_return_pctr   r   r   r   r   r   Úround)	r.   r/   r0   ÚnÚspy_retÚqqq_retÚ	compositeÚtr    r,   r,   r-   Úclassify_market_regime@   s(   	


ÿr<   c                   @   s    e Zd ZU eed< dZeed< dZeed< ee	d�Z
e	ed< ee	d�Ze	ed< dZeed	< dZeed
< dZeed< dZeed< dZeed< eed�Zeed< dS )ÚRelativeStrengthReadingÚsymbolç        Úrs_scoreÚfinal_score)Údefault_factoryÚ	rs_vs_spyÚ	rs_vs_qqqNÚsectorÚsector_breadthÚsector_momentumÚsector_volume_confirmationFr$   Úmissing)r%   r&   r'   r(   r)   r@   r*   rA   r   ÚdictrC   rD   rE   rF   rG   rH   r$   r+   ÚlistrI   r,   r,   r,   r-   r=   a   s   
 r=   Úxc                 C   s   t dtd| ƒƒS )Nr?   ç      ð?)ÚmaxÚmin)rL   r,   r,   r-   Ú_clamp01p   s   rP   ÚbarsÚn_barsc                 C   sJ   | rt | ƒ|kr
d S | d d | d|  d }}|sd S || | d S )NéÿÿÿÿÚcç      Y@)Úlen)rQ   rR   ÚnowÚthenr,   r,   r-   r5   t   s   r5   c                 C   sŒ   t | ƒdkrdd„ | dd… D ƒndd„ | D ƒ}t |ƒdk r dS t |ƒd }t|d|… ƒ| p1d}t||d… ƒt |ƒ|  }|| d	kS )
zÁSame recent-half-vs-early-half shape as indicators.volume_acceleration,
    reused here rather than re-derived under a different name -- a peer
    "confirms" when its own volume is not fading.é
   c                 S   s   g | ]}|d  ‘qS )Úvr,   )Ú.0Úbr,   r,   r-   Ú
<listcomp>�   s    z&_volume_confirming.<locals>.<listcomp>iöÿÿÿNé   é   g•Ö&è.>rM   )rV   Úsum)rQ   ÚvolsÚmidÚearlyÚrecentr,   r,   r-   Ú_volume_confirming}   s   0re   r>   Ú	peer_barsc                 C   sþ   |  di ¡  | ¡}|r|sdS |  d¡}|d   |d d¡}g g }}| ¡ D ]\}}	t|	|ƒ}
|
d ur9| |
¡ t|	ƒ}|d urF| |¡ q'|sO|d d d fS ttdd„ |D ƒƒt|ƒ d	ƒ}tt|ƒt|ƒ d	ƒ}|rwtt|ƒt|ƒ d	ƒnd }||||fS )
Nr   )NNNNrE   r   r   r   c                 s   s   � | ]	}|d krdV  qdS )r   r   Nr,   )r[   Úrr,   r,   r-   Ú	<genexpr>�   ó   € z _sector_layer.<locals>.<genexpr>r   )r4   Úitemsr5   Úappendre   r6   r`   rV   )r>   r0   rf   ÚentryrE   rR   Úpeer_returnsÚpeer_vol_flagsÚpeerrQ   ÚretÚvol_okÚbreadthÚmomentumÚvol_confirmationr,   r,   r-   Ú_sector_layerŠ   s(   




€ ru   Úpremarket_return_pctÚpremarket_spy_return_pctÚpremarket_qqq_return_pctc	                    s\  i t ¥|ptƒ  di ¡¥}t|d ƒ‰ t|ƒdk s%t|ƒdk s%t|ƒdk r-t| ddgd�S i i i }	}
‰|d  ¡ D ]T\}}t||ƒ}t||ƒ}t||ƒ}|du sZ|du sZ|du raˆ  |d¡ q;t	|| d	ƒ|	|< t	|| d	ƒ|
|< |	| |
|  d
 }t
||d  d|d   ƒd ˆ|< q;d|||fv}|rÈt	|| d	ƒ|	d< t	|| d	ƒ|
d< |	d |
d  d
 }t
||d  d|d   ƒd ˆd< nˆ  dd¡ ˆsØt| ddgd�S t‡ fdd„ˆD ƒƒ}|röt	t‡ ‡fdd„ˆD ƒƒ| dƒnd}t| ||pþi ƒ\}}}}|}|du�r||d k�rtd|d|d   ƒ}t| |t	|dƒ|	|
||||dd�
S )ai  
    bars / spy_bars / qqq_bars: regular-session 1-min bars, oldest first, for
        the candidate / SPY / QQQ respectively -- same shape stream.py hands
        every other engine.
    premarket_*_return_pct: optional plain floats (this symbol's own, SPY's,
        QQQ's premarket % change) for the "premarket" window -- plain floats
        rather than raw bars because premarket_scanner.py already computes
        the candidate's own premarket move (see scorer.price_momentum);
        recomputing that from raw bars a second time here would duplicate
        it. Omit any of the three to skip this window; its weight is simply
        redistributed across the others.
    peer_bars: optional {peer_symbol: bars} for this symbol's sector map
        entry's `peers` list. Omit entirely to skip the sector layer -- rs
        vs SPY/QQQ is unaffected either way.
    r2   r   r_   Tz,need at least 2 bars each for symbol/SPY/QQQ)r>   r$   rI   r   Nr   r   r   rU   r   z1no window had enough bars for a return comparisonc                 3   s   � | ]	}ˆ   |d ¡V  qdS ©r   N©r4   ©r[   Úw)Úweightsr,   r-   rh   Ù   ri   z,compute_relative_strength.<locals>.<genexpr>c                 3   s$   � | ]}ˆ| ˆ   |d ¡ V  qdS ry   rz   r{   ©r}   Úwindow_scoresr,   r-   rh   Ú   s   €" r?   r   r   r   F)
r>   r@   rA   rC   rD   rE   rF   rG   rH   r$   )r3   r   r4   rJ   rV   r=   rj   r5   Úpopr6   rP   r`   ru   rO   )r>   rQ   r.   r/   rv   rw   rx   rf   r0   rC   rD   Úlabelr7   Úsym_retr8   r9   Úavg_vs_marketÚhave_premarketÚweight_totalr@   rE   rr   rs   Úvol_confrA   r,   r~   r-   Úcompute_relative_strength£   s`   $ÿ


&ÿ
ÿÿ$ÿÿür‡   )N)NNNNN)Ú__doc__Údataclassesr   r   Úconfig_loaderr   r3   r   r   r   r   r   r   rK   rJ   r<   r=   r*   rP   Úintr5   r+   re   r(   ru   r‡   r,   r,   r,   r-   Ú<module>   sX    õ!	üÿþýüüü