o
    ¿ž©j/  ã                   @   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
mZ ddlZddlZddlZedƒZd	efd
d„Zdeded	efdd„Z		ddedededed	ef
dd„Zdefdd„Zedkr~eƒ Zerxeded › ded › �ƒ dS edƒ dS dS )u§  
top_stocks.py

Runs whenever a position closes and a slot opens up. Re-scans CURRENT
market conditions (not the stale 09:25 list) to find the strongest
actionable candidate right now, respecting symbol cooldowns and
currently-open positions.

[FEATURE 2026-08-17] The candidate pool searched here is now
premarket_20 (the full morning list of 20), not final_10 -- per the
project's updated intraday-scanner requirement, premarket_20 is the
primary intraday universe. Ranking now uses intraday_health.py's
continuous health score (via evaluate_symbol()) rather than a one-shot
score_premarket_candidate() call, so the same continuously-updated
health state that decides entry eligibility also decides replacement
ranking -- one source of truth, not two competing scores. Symbols
health.evaluate_symbol() confirms as STALE/UNHEALTHY/REMOVED are
excluded from replacement selection entirely.

[FEATURE 2026-08-18] Every symbol in premarket_20 -- not just the ones
currently eligible to fill a slot -- gets its health recomputed and
persisted every time find_replacement() runs. Previously, a symbol
sitting in exclude_symbols (currently open) or on cooldown never got
recomputed during a replacement search, so its state/intraday_health.json
entry could go stale until the next periodic 60-second cycle even though
other parts of the system (the entry shortlist ranking, the dashboard)
read that same file. Now every slot-open event doubles as a full health
refresh for the whole pool, and selection among the symbols that CAN
fill the slot is still: highest health_score wins, total_score only
breaks a tie.

Writes the running result to state/top_stocks.json (write_top_stocks)
so the whole system has a single canonical file of "what looks best
right now," as required by the project spec.

The returned candidate still must pass fast_pipeline.evaluate_fast_pipeline()
before monitor.py actually buys it â€” this module only ranks, it never
enters directly.
é    )ÚdatetimeÚtimezoneÚ	timedelta)Ú
get_config)Ú
get_logger)Ú
get_client)Úscore_premarket_candidateNÚ
top_stocksÚreturnc              
   C   sJ   g }| D ]}|  |jt|jƒt|jƒt|jƒt|jƒt|jƒdœ¡ q|S )N)ÚtÚoÚhÚlÚcÚv)ÚappendÚ	timestampÚfloatÚopenÚhighÚlowÚcloseÚvolume)ÚbarsÚoutÚb© r   útop_stocks.pyÚ_bars_to_dicts6   s   
þr   ÚsymbolÚ	cooldownsc                 C   sD   |  | ¡}|s	dS tƒ d d }t |¡t|d� }t tj¡|k S )NFÚriskÚsymbol_cooldown_minutes)Úminutes)Úgetr   r   Úfromisoformatr   Únowr   Úutc)r   r    ÚtsÚcooldown_minutesÚexpiresr   r   r   Ú_is_on_cooldown@   s   
r+   Úexclude_symbolsÚcandidate_poolÚvolume_baselinesÚvolatility_baselinesc                 C   sÜ  t ƒ }t| pg ƒ} t ¡ }t ¡ }|du r,t ¡ }| dg ¡p$| dg ¡}dd„ |D ƒ}|s5t d¡ dS t	ƒ }t
 tj¡}	|	t|d d d	� }
|d
 d }|pRi }|pVi }t dt|ƒ› d�¡ i }i }|D ]X}| |¡}|rwt|ddƒnd}t ||¡\}}|s�t d|› d|› �¡ qht| ||
|	¡ƒ}t|ƒdk r¨t d|› d�¡ qht ||| ||¡|¡\}}|||< ||f||< qh|rÝ| |¡ t |¡ t dt|ƒ› dt|ƒ› d�¡ g }| ¡ D ]¢\}\}}|| v rîqãt||ƒrýt d|› d�¡ qãt |j¡�st d|› d|j› �¡ qãtdd„ |D ƒƒ}|  |¡}|�r+|j!�r+t"|j!ƒnd}|�r9|j#�r9t"|j#ƒnd}t$|||t%dd„ |D ƒƒ| ||¡||| |¡d�}|d  dd¡�sht d|› d �¡ qã||d!< |j&|d"< |j|d#< |j'|d$< |j(|d%< | )|¡ qã|�s�t d&¡ dS |j*d'd(„ dd)� |d* }| d+i ¡ d,d*¡}|d" |k �rÈt d-|d. › d/|d" › d0|› d1�¡ t +|¡ dS t d2¡ t d|d. › d3|d# › d4|d" › d5|d6 › d7�	¡ t +|¡ |S )8a  
    exclude_symbols: currently-open symbols, always excluded from
                     filling the slot (but still get a fresh health
                     refresh below -- see [FEATURE 2026-08-18]).
    candidate_pool: optional restricted pool; if None, defaults to
                     state/watchlist.json's premarket_20 (falling back
                     to final_10 only if premarket_20 is somehow empty
                     -- e.g. a very early call before the 09:00 scan has
                     ever written the file).

    [FEATURE 2026-08-18] Every symbol in the pool -- including ones
    currently open or on cooldown, not just the ones that could fill
    THIS slot -- gets a fresh intraday_health.evaluate_symbol() call and
    a persisted state/intraday_health.json update before any selection
    happens. Previously, a symbol sitting in exclude_symbols/cooldown
    never got its health recomputed during a replacement search (its
    entry in the state file could go stale until the next periodic
    60-second eval cycle), even though that same file also feeds
    _scan_for_entries()' shortlist ranking and the dashboard. Refreshing
    the whole pool here means every slot-open event doubles as a full
    health refresh for premarket_20, not just for the symbols eligible
    to fill it right now.

    Selection itself is unchanged: among symbols that ARE eligible to
    fill the slot (not excluded, not on cooldown, health-eligible per
    that just-refreshed reading), the highest health_score wins,
    total_score only breaks a tie.
    NÚpremarket_20Úfinal_10c                 S   s"   g | ]}t |tƒr|d  n|‘qS )r   )Ú
isinstanceÚdict)Ú.0r   r   r   r   Ú
<listcomp>o   s   " z$find_replacement.<locals>.<listcomp>z3[REPLACEMENT] No candidate pool available to searchÚintraday_healthÚfull_rescan_lookback_hours)ÚhoursÚuniverseÚmin_avg_daily_volumez([REPLACEMENT] Refreshing health for all z? premarket_20 symbols before selecting a fill for the open slotÚnameÚ z[REPLACEMENT] z
 skipped: é   z4 skipped: insufficient bars for a fresh health checkz#[REPLACEMENT] Refreshed health for ú/z pool symbolsz skipped: on cooldownz skipped: health=c                 s   ó   � | ]}|d  V  qdS )r   Nr   ©r4   r   r   r   r   Ú	<genexpr>°   ó   € z#find_replacement.<locals>.<genexpr>c                 s   r?   )r   Nr   r@   r   r   r   rA   µ   rB   )Údaily_atr_pctÚflagsÚmeets_min_volatilityTzK skipped: below min daily ATR% (structurally low volatility, e.g. BDC/SPAC)Úpm_highÚhealth_scoreÚhealth_stateÚprice_slopeÚ
vwap_slopez>[REPLACEMENT] No actionable (health-eligible) candidates foundc                 S   s   | d | d fS )NrG   Útotal_scorer   )Úrr   r   r   Ú<lambda>Ð   s    z"find_replacement.<locals>.<lambda>)ÚkeyÚreverser   ÚtradingÚmin_replacement_health_scorez[REPLACEMENT] Best candidate r   z (health_score=z() is below min_replacement_health_score=zI -- leaving slot empty this cycle rather than filling it with a weak pickz#[REPLACEMENT] Running top_stocks.pyz selected (health=z, health_score=z, score=rK   ú)),r   ÚsetÚ
data_storeÚload_cooldownsÚload_health_stateÚload_watchlistr$   ÚlogÚinfor   r   r&   r   r'   r   ÚlenÚ	get_assetÚgetattrÚsymbol_filtersÚpasses_symbol_filtersÚdebugr   Úget_minute_barsr6   Úevaluate_symbolÚupdateÚsave_health_stateÚitemsr+   Úis_eligible_for_entryÚconfirmed_stateÚmaxÚget_latest_quoteÚ	bid_pricer   Ú	ask_pricer   ÚminrG   Úprice_slope_classÚvwap_slope_classr   ÚsortÚwrite_top_stocks)r,   r-   r.   r/   Úcfgr    rH   Ú	watchlistÚclientr&   ÚstartÚfallback_baselineÚhealth_updatesÚfreshr   ÚassetÚ
asset_nameÚokÚreasonr   ÚreadingÚnew_health_entryÚscoredÚsession_highÚquoteÚbidÚaskÚresultÚbestrQ   r   r   r   Úfind_replacementI   s¶   

ÿ


ÿ

þ




ÿ
þ

ÿ
ÿ
r„   c                 C   s*   t  ¡ }t tj¡ ¡ || < t  |¡ d S )N)rT   rU   r   r&   r   r'   Ú	isoformatÚsave_cooldowns)r   r    r   r   r   Úregister_cooldownï   s   r‡   Ú__main__zBest replacement: z score=rK   zNo replacement candidate found.)NNNN)Ú__doc__r   r   r   Úconfig_loaderr   Úlogger_setupr   Úalpaca_clientr   Úscorerr   r]   r6   rT   rX   Úlistr   Ústrr3   Úboolr+   r„   r‡   Ú__name__r‚   Úprintr   r   r   r   Ú<module>   s:    (
	ÿÿÿ
ÿ ' û