o
    ››j§t  ã                   @   sü   d Z ddlmZmZ ddlmZ ddlmZ ddlm	Z
 ddlmZmZmZmZmZmZmZmZmZmZmZ ddlZe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eG d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 )aX  
entry_engine.py

Combines premarket setup + real-time opening behavior into a single
ENTRY / NO ENTRY decision with a documented reason string, per the
project's explicit example:

    Candidate ranked #2 premarket
    + Opening volume expansion
    + Price above VWAP
    + VWAP rising
    + Higher lows
    + Breaks premarket resistance
    + Breakout holds
    + Fresh intraday health check confirms tradeable  [BUGFIX 2026-08-18]
    = ENTRY

Never enters purely on premarket rank. Every check here operates on
live intraday bars pulled from stream.py's buffer.

[BUGFIX 2026-08-18] Price-above-VWAP / VWAP-rising / volume-expansion
are necessary but NOT sufficient -- they can all be momentarily true on
a few noisy ticks while the broader structure is already deteriorating.
The caller (monitor.py) is required to compute a fresh
intraday_health.HealthReading from the SAME live_bars being evaluated
here (not from the periodically-persisted state/intraday_health.json,
which can be stale by up to intraday_health.eval_interval_seconds) and
pass it in as health_reading. See evaluate_entry()'s docstring.
é    )Ú	dataclassÚfield)Ú
get_config)Ú
get_logger)Útime)ÚvwapÚ
vwap_slopeÚis_higher_highs_higher_lowsÚbreakout_confirmedÚpullback_then_continuationÚ
spread_pctÚextension_from_vwap_pctÚrelative_volumeÚrsiÚis_extended_downtrendÚdistance_from_high_pctNÚentry_engineÚcfgÚ	raw_stateÚreturnc                 C   s0   |   d¡}t|tƒr||v r|| S |   dd¡S )aq  
    [FEATURE 2026-08-25] Backward-compatible resolution order:
      1. entry.min_health_score_by_state[raw_state], if that dict and key exist
      2. entry.min_health_score (the old flat single-number config), if set
      3. 0 (no floor)
    2026-08-24 session review: WATCH-classified entries ranged score=48
    to score=92 and were all treated identically by the flat floor,
    even though WATCH is explicitly the weaker of the two entry-eligible
    states. Splitting the floor per-state lets WATCH require a
    meaningfully higher score than HEALTHY without touching the
    categorical raw_state gate itself.
    Úmin_health_score_by_stateÚmin_health_scorer   )ÚgetÚ
isinstanceÚdict)r   r   Úby_state© r   ú+/var/www/screener/premarket/entry_engine.pyÚ_health_floor_for_state.   s   
r   c                 C   s&   |   dd¡sdS t| |jƒ}|j|k S )NÚrequire_min_health_scoreF)r   r   r   Úhealth_score)r   Úhealth_readingÚfloorr   r   r   Ú_below_state_dependent_floorA   s   
r#   c              
   C   s°   |   di ¡}|  dd¡s| S t ¡  ¡ }|  dg ¡D ];}zt |d ¡}t |d ¡}W n ttfy7   Y qw ||  krB|k rUn qt| ƒ}| 	|  di ¡¡ |  S q| S )aÀ  
    [FEATURE 2026-08-27] Lets entry.time_based_overrides define different
    entry-config values for different parts of the trading day -- e.g.
    stricter thresholds right at the volatile open, looser mid-day,
    something else again in the afternoon. Disabled unless
    entry.time_based_overrides.enabled is true, in which case:

      1. Find the window whose [start, end) contains the current time
         (America/New_York, matching every other schedule check in this
         project -- see market_time.now_et()).
      2. Return a NEW dict: the base cfg with that window's "overrides"
         keys shallow-merged on top. Only keys present in a window's
         overrides are changed; everything else falls through to the
         base config unchanged.
      3. If no window matches the current time (e.g. gaps between
         windows, or outside regular hours), the unmodified base cfg is
         used -- a config typo or gap fails safe to normal behavior,
         not to an undefined state.

    Windows are checked in the order they appear in config.json; the
    first match wins, so don't define overlapping windows unless that
    ordering-dependent behavior is actually what's intended.
    Útime_based_overridesÚenabledFÚwindowsÚstartÚendÚ	overrides)
r   Úmarket_timeÚnow_etr   Údt_timeÚfromisoformatÚKeyErrorÚ
ValueErrorr   Úupdate)r   Úwindows_cfgÚnowÚwindowr'   r(   Úmergedr   r   r   Ú_apply_time_window_overridesH   s"   ÿýr5   c                   @   sJ   e Zd ZU eed< eed< eed< eed�Z	eed< eed�Z
eed< dS )ÚEntryDecisionÚsymbolÚshould_enterÚconfirmation_score)Údefault_factoryÚreasons_forÚreasons_againstN)Ú__name__Ú
__module__Ú__qualname__ÚstrÚ__annotations__ÚboolÚfloatr   Úlistr;   r<   r   r   r   r   r6   s   s   
 r6   Fr7   Úpremarket_resultÚ	live_barsÚquoteÚopening_baseline_volumeÚ
is_reentryÚcfg_overridec           -      C   sŽ  t ƒ d }t|ƒ}|ri |¥|¥}g }	g }
t|ƒdk r$t| ddg dgƒS |d d }| dd	¡}|}t|ƒ}t|t|d
 t|ƒd ƒd�}tdd„ |D ƒƒ}|rUt	||ƒnd}d	}d	}d}|d rˆ|d7 }||kr{|d7 }|	 
d|d›d|d›�¡ n|
 
d|d›d|d›�¡ |d r£|d7 }|d	krž|d7 }|	 
d¡ n|
 
d¡ |d rÊ|d7 }||d krÀ|d7 }|	 
d|d›d�¡ n
|
 
d|d›d�¡ |d rî|d7 }t|t|d t|ƒƒd�ré|d7 }|	 
d ¡ n|
 
d!¡ d}|d" �rB|d	k�rB|d7 }t|||d# |d$ ƒ}|�r |d7 }|	 
d%|d›d&|d$ › d'�¡ n"|d( �r9t||d) ƒ�r9|d7 }d*}|	 
d+¡ n	|
 
d,|d›�¡ | d-d¡}|�rh|d.u�rh|jd/k�r^d*}|
 
d0¡ n3|	 
d1|j› �¡ n)| d2d¡�r‘|d.u�r‘|d7 }|jd/k�rŒ|d7 }|	 
d1|j› �¡ n|
 
d3¡ | d4d¡�r¸|d.u�r¸|d7 }|j d5d¡�s³|d7 }|	 
d6¡ n|
 
d7¡ | d8d*¡�r{|d.u �rÌd*}|
 
d9¡ n¯|jd:v�rd*}|
 
d;|j› d<|jd=›d>|j› d?|j› d@|j› dA|j d5¡› dB|j dC¡› dD|j dE¡› dF�¡ nvt||ƒ�rKd*}t||jƒ}|
 
dG|j› d<|jd=›dH|j› dI|› d>|j› d?|j› d@|j› dA|j d5¡› dB|j dC¡› dD|j dE¡› dF�¡ n0|	 
dJ|j› dK|jd=›d>|j› d?|j› d@|j› dA|j d5¡› dB|j dC¡› dD|j dE¡› dF�¡ |�r›|\}}}t||ƒ}||dL k�r›d*}|
 
dM|d›dN�¡ t||ƒ}||dO k�r¸d*}|
 
dP|d›dQ|dO › dN�¡ | dRd	¡}|d	k�r×||k �r×d*}|
 
dS|d›dT|› dU�¡ | dVd¡�r| dWdX¡}t||dY�}| dZd[¡} || k�rd*}|
 
d\|› d]|d^›d_| › dF�¡ n|	 
d\|› d`|d^›�¡ | dad¡�rat| dbdc¡t|ƒƒ}!tddd„ ||! d.… D ƒƒ}"t||"ƒ}#| dedf¡}$|#|$k�rXd*}|
 
dg|#d›dh|"d›di|$› dj�¡ n	|	 
dk|$› dl�¡ t|ƒdk�rŒdmdn„ |dod.… D ƒ}%t|%ƒdk�rŒ|%d |%d	 dp k �rŒd*}|
 
dq¡ | drd*¡�rµt|| dsdt¡| dudv¡| dwdv¡dx��r°d*}|
 
dy¡ n|	 
dz¡ |�r¾|| d{ nd}&d}'|�ræ| d|d¡�ræ| d}d~¡}(|&|(k �ræd*}'|
 
d|(› d€|&d=›d��¡ | �oó|' �oó|&|d‚ k})t| |)t|&dƒ|	|
dƒ�}*|)�rt d„| › d…|&d=›d†d‡ |	¡› �¡ |*S t ƒ  dˆi ¡ d‰d¡}+d„| › dŠ|&d=›d†|
�r2d‡ |
¡nd‹› �},|+�r@t |,¡ |*S t  |,¡ |*S )Œa  
    premarket_result: the scored dict from scorer.score_premarket_candidate
                       (gives us pm_high, premarket rank/score context)
    live_bars: list of 1-min bars since market open, oldest first
    quote: (bid, ask, timestamp) or None
    opening_baseline_volume: expected "normal" opening volume for
                              comparison, e.g. premarket total volume,
                              used to detect "opening volume expansion"
    cfg_override: [SIMULATION-ONLY 2026-09-04] optional dict shallow-merged
        on top of the base entry config (same mechanism as
        _apply_time_window_overrides' per-window overrides, applied AFTER
        it so this wins on any overlapping key) -- lets a caller evaluate
        this same, unchanged confirmation logic against a different rule
        set without duplicating it. Added for regime_strategies.py /
        simulate_regime_strategies.py so each regime_detector.py regime
        can carry its own independent entry rules (see config.json's
        regime_strategies section) while every real check above stays
        the single source of truth. None (the default) preserves prior
        behavior exactly -- monitor.py's live call site is unaffected.
    health_reading: an intraday_health.HealthReading computed by the
        CALLER, right now, from THESE SAME live_bars. [BUGFIX 2026-08-18]

        This must be freshly computed at decision time, not read from
        state/intraday_health.json (that file is only as current as the
        last periodic intraday_health eval cycle -- up to
        intraday_health.eval_interval_seconds old, and can lag further
        if a cycle was skipped). Confirmed live: two entries on RCAT
        fired using a cached health state that was ~10 minutes stale at
        the moment of entry, because nothing forced a fresh check at the
        entry decision itself.

        Health is a hard disqualifier here, not just another vote in
        the confirmation-score tally -- "price above VWAP, VWAP rising,
        opening volume expansion" can all be true for a few ticks on
        pure noise while the broader structure (price/VWAP slope,
        higher-highs/higher-lows, volume trend) is already fading; that
        combination is exactly what health_reading evaluates. No
        combination of the other checks can outvote a failed health
        read, matching how the spread/extension disqualifiers already
        work below.
    is_reentry: True if this symbol already has a closed position
        earlier THIS SESSION (computed by the caller via
        position_manager.has_closed_position_today()). [FEATURE
        2026-08-27] Replaces the earlier time-based
        same_symbol_reentry_cooldown_minutes approach -- rather than a
        fixed wait, a re-entry into a symbol the bot already traded
        (and exited, win or loss) today must clear
        entry.reentry_min_confirmation_score (100% by default) instead
        of the normal entry.min_confirmation_score. See
        require_full_confirmation_on_reentry in config.json.
    Úentryé   Fg        z!insufficient live bars since openéÿÿÿÿÚcÚpm_highr   Úvwap_rising_lookback_barsé   )Úlookbackc                 s   ó   � | ]}|d  V  qdS )ÚvNr   ©Ú.0Úbr   r   r   Ú	<genexpr>Ã   ó   € z!evaluate_entry.<locals>.<genexpr>g      ð?Úrequire_price_above_vwapzprice $z.2fz above VWAP $z below VWAP $Úrequire_vwap_risingzVWAP risingzVWAP flat or decliningÚ require_opening_volume_expansionÚopening_volume_expansion_ratiozopening volume expansion Úxz'opening volume expansion insufficient (zx)Úrequire_higher_lowsÚhigher_lows_lookback_barszhigher lows structure intactz"no clean higher-lows structure yetÚ)require_breakout_of_pm_high_or_resistanceÚbreakout_buffer_pctÚbreakout_hold_barszbreakout of resistance $z
 held for z barsÚ!allow_pullback_continuation_entryÚpullback_max_retrace_pctTz)healthy pullback followed by continuationzhas not confirmed breakout of $Ú"require_momentum_not_fading_strictNÚnegativezhmomentum fading (momentum_slope=negative) -- hard disqualifier, require_momentum_not_fading_strict is onz	momentum Úrequire_momentum_not_fadingz)momentum fading (momentum_slope=negative)Úrequire_volume_not_decliningÚvolume_decliningzvolume not decliningzvolume decliningÚrequire_fresh_health_checkz/no fresh health reading available at entry time)ÚHEALTHYÚWATCHz!fresh health check failed (state=z, score=z.0fz, price_slope=z, vwap_slope=z, momentum_slope=z, volume_declining=z, lower_hl=Úlower_highs_lower_lowsz, price_above_vwap=Úprice_above_vwapú)z"fresh health score too low (state=z < min_health_score[z]=zfresh health z (score=Úmax_spread_pct_entryzspread too wide (z%)Úmax_extension_from_vwap_pctzprice extended z% above VWAP (max Úmin_extension_from_vwap_pctzprice only z% above VWAP (min z%) -- insufficient separationÚrequire_rsi_not_overboughtÚ
rsi_periodé   )ÚperiodÚmax_rsi_at_entryéF   zRSI(z) overbought (z.1fz > z)=Ú!require_no_recent_spike_extensionÚrecent_spike_lookback_barsé
   c                 s   rS   )ÚhNr   rU   r   r   r   rX   Ô  rY   Ú!max_pullback_from_recent_high_pctg       @zprice already z% off its recent high $z (max z3%) -- looks like a fading spike, not fresh strengthzstill within z1% of its recent high (not chasing a fading spike)c                 S   s   g | ]}|d  ‘qS )rN   r   rU   r   r   r   Ú
<listcomp>â  s    z"evaluate_entry.<locals>.<listcomp>éýÿÿÿg…ëQ¸…ï?z#price fading immediately after openÚrequire_no_extended_downtrendÚdowntrend_min_barsé   Údowntrend_min_decline_pctg      ø?Ú downtrend_recovery_threshold_pct)Úmin_barsÚmin_decline_pctÚrecovery_threshold_pctz€session still in a downtrend from its high -- this looks like a bounce, not a confirmed reversal (require_no_extended_downtrend)zno extended session downtrendg      Y@Ú$require_full_confirmation_on_reentryÚreentry_min_confirmation_scoreéd   zre-entry requires z% confirmation, got z&% (symbol already traded this session)Úmin_confirmation_score)r7   r8   r9   r;   r<   z[CONFIRMATION] z confirmed (z%): z; ÚloggingÚlog_rejectionsz not confirmed (zno specific reason recorded)!r   r5   Úlenr6   r   r   r   ÚminÚsumr   Úappendr	   r
   r   Úmomentum_slope_classÚflagsr   r    Úprice_slope_classÚvwap_slope_classr#   r   r   r   r   Úmaxr   r   ÚroundÚlogÚinfoÚjoinÚdebug)-r7   rE   rF   rG   rH   r!   rI   rJ   r   r;   r<   Úcurrent_pricerO   Ú
resistanceÚv_wapÚv_slopeÚopening_volumeÚvol_expansion_ratioÚchecks_passedÚchecks_totalÚdisqualifiedÚbreakout_okÚmomentum_strictr"   ÚbidÚaskÚ_ÚsprÚ	extensionÚmin_extensionru   Ú	rsi_valueÚmax_rsirR   Úrecent_highÚpullback_pctÚmax_pullbackÚrecent_closesr9   Úreentry_blockedÚreentry_min_scorer8   ÚdecisionrŽ   Úmessager   r   r   Úevaluate_entry|   sÚ  
7

ÿÿÿ!ÿ

(

ÿþýü
û
ú
ùÿ

ÿþþýüû
ú
ù
øÿ
ÿþýü
û
ú
ùÿ



ÿ	
ÿ
"

ÿÿ$



üÿ

ÿÿÿ
ýûÿøÿ

þr¸   )NFN)$Ú__doc__Údataclassesr   r   Úconfig_loaderr   Úlogger_setupr   Údatetimer   r,   Ú
indicatorsr   r   r	   r
   r   r   r   r   r   r   r   r*   r™   r   r@   rC   r   rB   r#   r5   r6   rD   Útupler¸   r   r   r   r   Ú<module>   s4    4+
ýÿÿþýý