o
    uÆŒjO*  ã                   @   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fdd„Zdefdd„Zedkrxeƒ Zerre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 entry_engine.evaluate_entry()
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   úF/var/www/screener/trade/premarket_backup_2026-09-08_2010/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_poolc                 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 }	t dt|ƒ› d�¡ i }
i }|D ]T}| |¡}|rot|ddƒnd}t ||¡\}}|s‡t d|› d|› �¡ q`t| |||¡ƒ}t|ƒdk r t d|› d�¡ q`t |||	|¡\}}||
|< ||f||< q`|
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}|�r-|j#�r-t"|j#ƒnd}t$|||t%dd„ |D ƒƒ|	||ƒ}||d< |j&|d< |j|d< |j'|d< |j(|d < | )|¡ q×|�sht d!¡ dS |j*d"d#„ d$d%� |d& }| d'i ¡ d(d&¡}|d |k �r t d)|d* › d+|d › d,|› d-�¡ t +|¡ dS t d.¡ t d|d* › d/|d › d0|d › d1|d2 › d3�	¡ t +|¡ |S )4a  
    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>n   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   ©r2   r   r   r   r   Ú	<genexpr>©   ó   € z#find_replacement.<locals>.<genexpr>c                 s   r=   )r   Nr   r>   r   r   r   r?   ®   r@   Úpm_highÚhealth_scoreÚhealth_stateÚprice_slopeÚ
vwap_slopez>[REPLACEMENT] No actionable (health-eligible) candidates foundc                 S   s   | d | d fS )NrB   Útotal_scorer   )Úrr   r   r   Ú<lambda>¿   s    z"find_replacement.<locals>.<lambda>T)Ú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=rF   ú)),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_barsr4   Úevaluate_symbolÚupdateÚsave_health_stateÚitemsr+   Úis_eligible_for_entryÚconfirmed_stateÚmaxÚget_latest_quoteÚ	bid_pricer   Ú	ask_pricer   ÚminrB   Úprice_slope_classÚvwap_slope_classr   ÚsortÚwrite_top_stocks)r,   r-   Úcfgr    rC   Ú	watchlistÚclientr&   ÚstartÚavg_vol_baselineÚhealth_updatesÚfreshr   ÚassetÚ
asset_nameÚokÚreasonr   ÚreadingÚnew_health_entryÚscoredÚsession_highÚquoteÚbidÚaskÚresultÚbestrL   r   r   r   Úfind_replacementI   sª   

ÿ


ÿ

ÿ




ÿ
þ

ÿ
ÿ
r   c                 C   s*   t  ¡ }t tj¡ ¡ || < t  |¡ d S )N)rO   rP   r   r&   r   r'   Ú	isoformatÚsave_cooldowns)r   r    r   r   r   Úregister_cooldownÞ   s   r‚   Ú__main__zBest replacement: z score=rF   zNo replacement candidate found.)NN)Ú__doc__r   r   r   Úconfig_loaderr   Úlogger_setupr   Úalpaca_clientr   Úscorerr   rX   r4   rO   rS   Úlistr   Ústrr1   Úboolr+   r   r‚   Ú__name__r}   Úprintr   r   r   r   Ú<module>   s*    (
	  û