o
    @¸ºjÄˆ  ã                   @   sŽ  d Z ddlmZmZ ddlmZmZ ddlmZ ddlZddl	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mZ dZdZd	Zi d
d“ddddœ“dd“dd“dd“dd“dd“dd“dd“dd“d d!“d"d#“d$d!“d%d&“d'd(“d)d*“d+d,“d!d-d.d/œd!d!d#dd#d(d0d1d2œi d*dd3d4d5d#d6d*d1d3ddd#d7œd8œ¥ZeG d9d:„ d:ƒƒZd;d<„ Zd=d>„ Zd?ed@edAedBefdCdD„Z dEe!dFe!dGe!dBe!fdHdI„Z"dJe!dKedBe#fdLdM„Z$dNe%dOedGe!fdPdQ„Z&dKedRe!fdSdT„Z'dUedBefdVdW„Z(	dhdXe#dNe%dYedZed[e!dGe!dUed\edBe!fd]d^„Z)dKed_edRe!dGe!dBe!f
d`da„Z*dKed_edRe!dGe!dBe!f
dbdc„Z+			didXe#dNe%dYedZedRe!dde!d[e!d\edeedGe!dBefdfdg„Z,dS )ja  
smart_engine.py

The live entry decision engine for this project -- answers one
question per symbol, per call: BUY / WAIT / REJECT. Built from the
3-stage sequence worked out in the 2026-09-16 design conversation:

  Stage 1 -- "is this stock worth trading right now?"
      Continuously re-checked, every call. A HARD disqualifier (lost
      VWAP support, VWAP sloping down, spread too wide, a confirmed
      negative trend, lower-highs/lower-lows structure, or volume pace
      too thin) fails Stage 1 immediately and clears Stage 2's
      persistence timer -- same "reset and rewatch fresh next call"
      contract as sip_bot's fast_entry_gate.py, not a ban or cooldown.
      Passing Stage 1 also requires a genuinely constructive read, not
      just "nothing bad yet": either a real positive trend, or a flat-
      but-constructive one (EMA9 above EMA20, holding above a rising-
      or-flat VWAP) -- the second branch exists specifically so a
      naturally quiet, low-ATR grinder isn't judged by the same bar as
      an actively reversing stock (see trend classification below).

  Stage 2 -- "is momentum actually developing?"
      Direction has to hold continuously (Stage 1 passing on every
      call) for persistence_seconds before Stage 3 even gets checked.
      Same caller-held persistence_state contract as fast_entry_gate.py
      ({"since", "last_seen"}, fed back in on every call).

  Stage 3 -- "is there room to run?"
      Checked fresh the instant Stage 2's hold completes (not before --
      resistance/reward:risk can shift during the hold, so checking it
      early would be stale). If Stage 3 fails, the decision is WAIT (not
      REJECT) and the Stage 2 timestamp is preserved -- momentum
      genuinely confirmed, just not enough room yet -- so it gets
      rechecked next call without losing the hold, unless Stage 1 breaks
      in the meantime.

      [2026-09-17] Two interchangeable implementations, chosen by
      cfg["use_reward_risk_gate"]:
        - reward:risk (the original): finds the nearest known resistance
          level still above price, computes an ATR-based stop, and
          requires the resulting reward:risk ratio to clear a minimum.
        - trade-flow imbalance (now the default -- reward:risk is
          DISABLED pending more days of live data): uses
          trade_imbalance, the (buy_vol-sell_vol)/(buy_vol+sell_vol) read
          from stream.py over the last imbalance_window_seconds, as the
          "is this a real breakout" confirmation instead of a resistance
          distance calc. Two tiers: imbalance >= imbalance_buy_threshold
          passes outright (order flow alone is confirmation enough,
          don't wait for a level to cross); imbalance >=
          imbalance_cross_threshold requires price to have ALSO crossed
          the nearest REAL resistance level first (the momentary
          "session_high" tracked live is deliberately excluded from this
          check -- see _evaluate_stage3_imbalance's docstring for why).
          Traced from real 2026-09-17 trade data: at the instant of each
          symbol's actual breakout, ABSI/SECZ/CRML (genuine moves) read
          +30% to +71% imbalance; USDE (a breakout that immediately
          faded, never entered on until an hour later at a much worse
          price) read -71% at the same instant. Thresholds are a first
          cut off that one day -- expect to retune both as more days of
          live data come in.

Deliberately takes already-computed bars/quote/resistance-levels as
input and performs NO I/O of its own (no Alpaca calls, no file writes,
no knowledge of where its inputs came from). That's what makes this
safe to drop into whatever the live-monitoring loop ends up looking
like later -- a real stream, a replay, or a manual test all look
identical to this module.
é    )Ú	dataclassÚfield)ÚdatetimeÚtimezone)Ú
get_configN)ÚvwapÚ
vwap_slopeÚemaÚatrÚrelative_volumeÚvolume_accelerationÚis_lower_highs_lower_lowsÚpullback_then_continuationÚdistance_from_high_pctÚ
spread_pctÚnormalized_slope_pctÚclassify_slopeÚBUYÚWAITÚREJECTÚ
atr_periodé   Útrendgš™™™™™©?g      Ð?)Úmin_flat_threshold_pctÚflat_threshold_atr_fractionÚstructure_lookback_barsé   Úpullback_max_retrace_pctg      I@Úmax_spread_pctç      ð?Úmin_volume_pace_ratiogš™™™™™Ù?Úpersistence_secondsé   Úmax_evaluation_gap_secondsé   Úatr_multiplier_stopg333333ó?Úmin_reward_risk_ratiog      ø?Úclear_air_requires_targetFÚclear_air_atr_target_multiplierg      @Úuse_reward_risk_gateÚimbalance_window_secondsé   Úimbalance_buy_thresholdç      à?Úimbalance_cross_thresholdg333333Ó?Úmin_entry_scoreç        é   Úpositive)ÚenabledÚlookback_barsÚmodeÚopenç       @)r3   Úbase_pctÚstep_pctÚstrong_pace_ratioÚstrong_imbalanceÚanchorÚvwap_base_pctgš™™™™™É?g333333Ã?gš™™™™™¹?g      .@)Úweight_imbalanceÚweight_volume_paceÚweight_volatilityÚweight_slopeÚweight_volume_accelÚpace_full_credit_ratioÚpace_zero_credit_ratioÚatr_full_credit_pctÚatr_zero_credit_pctÚslope_full_credit_pctÚslope_zero_credit_pctÚvol_accel_full_creditÚvol_accel_zero_credit)Úentry_slope_ruleÚuse_setup_planÚmax_extension_from_openÚmin_stopÚentry_scorec                   @   sš   e Zd ZU eed< 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	< eed
�Zeed< eed
�Zeed< eed
�Zeed< eed
�Zeed< dS )ÚSmartEngineDecisionÚsymbolÚstateFÚshould_enterÚstage1_passedr0   Ústage2_elapsedNÚstage3_passed)Údefault_factoryÚreasons_forÚreasons_againstÚmetricsÚpersistence)Ú__name__Ú
__module__Ú__qualname__ÚstrÚ__annotations__Ú
STATE_WAITrR   rS   ÚboolrT   rU   ÚfloatrV   r   ÚlistrX   rY   ÚdictrZ   r[   © rf   rf   ú'/var/www/screener/trade/smart_engine.pyrP       s   
 rP   c                 C   s¢   i t ¥| ptƒ  di ¡¥}i t d ¥| di ¡¥|d< i t d ¥| di ¡¥|d< i t d ¥| di ¡¥|d< t | d¡¡|d< i t d ¥| di ¡¥|d< |S )NÚsmart_enginer   rO   rK   rN   rM   )ÚDEFAULT_CONFIGr   ÚgetÚ
volatilityÚ	merge_cfg)ÚcfgÚbaserf   rf   rg   Ú
_merge_cfg®   s   

ÿro   c                 C   s   t dtd| ƒƒS )Nr0   r   )ÚmaxÚmin)Úxrf   rf   rg   Ú_clamp01¹   s   rs   ÚvalueÚfull_credit_atÚzero_credit_atÚreturnc                 C   s,   ||kr| |kr
dS dS t ||  ||  ƒS )zº1.0 at/below full_credit_at, 0.0 at/beyond zero_credit_at, linear
    between -- used for every 'lower is better, but don't cliff-edge it'
    sub-score below (see _compute_entry_score).r   r0   )rs   )rt   ru   rv   rf   rf   rg   Ú_band_credit½   s   rx   Ústage1_metricsÚstage3rm   c                 C   s&  |d }|  d¡}|d }|durt|| d|  ƒnd}|   d¡}|dur0t||d |d	 ƒnd}t| d
 |d |d ƒ}	t| d |d |d ƒ}
t| d |d |d ƒ}d|d | |d |  |d |	  |d |
  |d |   }t|dƒt|dƒt|dƒt|	dƒt|
dƒt|dƒdœdœS )a]  
    [2026-09-22] A continuous 0-100 read of "how good is THIS entry,"
    checked the instant Stage 3 passes -- separate from and in addition
    to Stage 1-3's existing hard pass/fail gates. Motivated by the
    2026-09-21 giveback_room backtest finding: when a slot frees up
    mid-day, monitor.py currently enters whichever next candidate
    happens to clear Stage 1-3 first, with no notion of "clears the bar,
    but is this one actually a GOOD setup" -- unlike scanner.py's
    premarket candidate_score, which only ever runs once, before the
    open, and (per the user, 2026-09-22) is understood to only really
    apply to the day's first entries, not entries taken hours in.

    Built from the 5 live Stage1/Stage3 metrics that showed the
    (weak -- see caveat below) most consistent SIGN of correlation with
    real P/L across the 39 real trades from 2026-09-18 + 2026-09-21:
    imbalance strength (higher better), volume_pace_ratio (lower
    better -- chasing an already-way-overtraded stock underperformed),
    atr_pct (lower better -- tighter/calmer setups outperformed choppier
    ones), slope_pct (lower/steadier better -- a very steep spike right
    at entry underperformed a modest climb), volume_acceleration (lower
    better -- a sudden last-second volume spike looked more like a
    climax than continuation in this sample).

    CAVEAT, load-bearing: every one of those correlations is weak
    (|r| < 0.3, n=39, NOT statistically significant on its own) --
    this score is a hypothesis built from the SIGN of thin data, not a
    validated model. That's exactly why min_entry_score defaults to 0.0
    (a no-op floor -- entry_score is computed and logged on every entry
    either way, but nothing is rejected on it) until backtesting across
    more real days either confirms a real floor is worth setting or
    shows this doesn't hold up.
    rO   Ú	imbalancer.   Nr   r-   Úvolume_pace_ratiorC   rD   Úatr_pctrE   rF   Ú	slope_pctrG   rH   r   rI   rJ   ç      Y@r>   r?   r@   rA   rB   é   é   )Úimbalance_subÚvolume_pace_subÚvolatility_subÚ	slope_subÚvolume_accel_sub)rO   Ú	breakdown)rj   rs   rx   Úround)ry   rz   rm   Úescr{   Úimbalance_floorr‚   ÚpaceÚpace_subÚatr_subr…   Úvola_subÚscorerf   rf   rg   Ú_compute_entry_scoreÆ   sD   !
ÿÿ
ÿÿÿ

ÿ
þ
ý
üÿýþr�   ÚvolÚpricec                 C   st   |d ur
|| d nd }| d d ur| d d›d�nd}d| d › d|› d	�|d ur7|d›d
|| d›d� S d S )Nr   Údaily_atr_pctú.1fz% daily ATRzdaily ATR unknownzvolatility Úclassú (z), stop distance zc (ú.2fú%)zn/arf   )r‘   r’   ÚstopÚdistÚpctrf   rf   rg   Ú	_vol_note  s    ÿÿrœ   Úbarsr}   c                 C   sD   dd„ | D ƒ}t |ƒ}t|d d ||d d  ƒ}t||ƒ||fS )zäClassifies price trend as positive/flat/negative -- but unlike a
    single fixed threshold for every symbol, the flat/trend boundary
    scales with THIS symbol's own ATR%. See config.json's
    smart_engine.trend note for why.c                 S   ó   g | ]}|d  ‘qS ©Úcrf   ©Ú.0Úbrf   rf   rg   Ú
<listcomp>  ó    z _trend_class.<locals>.<listcomp>r   r   r   )r   rp   r   )r�   r}   rm   Úclosesr~   Úflat_threshrf   rf   rg   Ú_trend_class  s   ÿr¨   Úresistance_levelsc                    s2   ‡ fdd„|pi   ¡ D ƒ}|sdS t|dd„ d�S )aR  resistance_levels: {label: level_or_None}, e.g. {"session_high":
    .., "premarket_high": .., "prev_day_high": .., "range_20d_high":
    ..} -- any subset, any labels. Returns (level, label) for whichever
    supplied level is nearest but still above price, or (None, None) if
    every supplied level is at or below price ("clear air").c                    s.   g | ]\}}t  |¡r|r|ˆ kr||f‘qS rf   ©rk   Úis_level_key©r¢   ÚlabelÚlevel©r’   rf   rg   r¤      s    ÿÿÿz-_nearest_resistance_above.<locals>.<listcomp>)NNc                 S   s   | d S )Nr   rf   )Úpairrf   rf   rg   Ú<lambda>$  s    z+_nearest_resistance_above.<locals>.<lambda>)Úkey)Úitemsrq   )r’   r©   Ú
candidatesrf   r¯   rg   Ú_nearest_resistance_above  s   rµ   Únowc                 C   s¼   ddl m} tƒ d }||d ƒ}|  |¡}dd„ |d  d¡D ƒ\}}}d	d„ |d
  d¡D ƒ\}}	}
|j|||dd�}|j||	|
dd�}||  ¡ d }|dkrTdS ||  ¡ d | S )az  How far into today's regular session `now` is (0-1), computed
    from `now` itself -- NOT market_time.minutes_since_open(), which
    always reads the real wall clock and would silently break replay/
    backtest calls that pass a historical `as_of`. This is what makes
    the module's own as_of contract (see evaluate()'s docstring) actually
    true instead of aspirational.r   )ÚZoneInfoÚscheduler   c                 s   ó   � | ]}t |ƒV  qd S ©N©Úint©r¢   rr   rf   rf   rg   Ú	<genexpr>2  ó   € z,_session_elapsed_fraction.<locals>.<genexpr>Úmarket_open_timeú:c                 s   r¹   rº   r»   r½   rf   rf   rg   r¾   3  r¿   Úmarket_close_time)ÚhourÚminuteÚsecondÚmicrosecondg      N@r0   )Úzoneinfor·   r   Ú
astimezoneÚsplitÚreplaceÚtotal_seconds)r¶   r·   ÚschedÚtzÚnow_etÚohÚomÚos_ÚchÚcmÚcsÚopen_dtÚclose_dtÚsession_lenrf   rf   rg   Ú_session_elapsed_fraction'  s   

rØ   rQ   Úcumulative_volume_todayÚavg_vol_baselineÚquoteÚtrade_imbalancec           ,      C   s  |d d }t |ƒ}	t|ƒ}
t|t|d tdt|ƒd ƒƒd�}|r(|| d nd}t|||ƒ\}}}d	d
„ |D ƒ}t|dƒ}t|dƒ}t|t|d t|ƒƒd�}t	||d ƒ}t
dd
„ |D ƒƒ}t|tdd„ |D ƒƒƒ}|rxt|d |d ƒnd}|r�t||ƒnd}t|ƒ}|dkr�|| nd}g }||	k r¤| d|d›d|	d›�¡ |
dk r­| d¡ |durÆ||d krÆ| d|d›d|d › d�¡ |dkrØ| d |d!›d"|d!›d#�¡ |rß| d$¡ |durø||d% k rø| d&|d›d'|d% › d(�¡ |d) }|�r|d d* nd}|�r|| d+ d nd}|	�r||	 d+ d nd}d} |d, �r¢| d-d.¡d/k}!|!�r5|n|}"|"du�r¢|du�oF||d0 k}#|du�oQ||d1 k}$|!�r[| d2d3¡n|d4 }%|%|d5 t|#ƒt|$ƒ   } |"| k�r¢|!�r{d6|	d›�nd7|d›�}&| d8|"d9›d:|&› d;| d<›d=|#�r”d>nd?› d@|$�rœd>nd?› dA�¡ |dB }'t||'dC  d… ƒ}(|'d, �rÙ|'dD dEk�r¿|nd})|(|)k�rÙ| dF|'dC › dG|(d!›dH|)d!›d#�¡ |dEk�pð|dIk�oð||k�oð||	k�oð|
dk}*i dJ|“d/t|	dKƒ“dLt|
dKƒ“dMt|dKƒ“dNt|dOƒ“dP|“dQt|dKƒ“dRt|dKƒ“dSt|(dKƒ“dT|du�r-t|dƒnd“dU| “dV|du�r=t|dƒnd“dWt|dKƒ“dXt|dKƒ“dY|“dZ|“d[t|dOƒ“t|dOƒ|du�ret|dOƒndt|dOƒ|du�rtt|dOƒndd\œ¥}+| �o~|*|*||+d]œS )^zÊPure per-call read -- no state. Returns a dict of metrics plus
    hard_reasons (any -> Stage 1 fails and Stage 2's timer resets) and
    `passed` (hard_reasons empty AND a genuinely constructive read).éÿÿÿÿr    r   r€   é   )Úperiodr   r0   c                 S   rž   rŸ   rf   r¡   rf   rf   rg   r¤   I  r¥   z$_evaluate_stage1.<locals>.<listcomp>é	   r+   r   )Úlookbackr   c                 S   rž   )Úvrf   r¡   rf   rf   rg   r¤   O  r¥   c                 s   s   � | ]}|d  V  qdS )ÚhNrf   r¡   rf   rf   rg   r¾   P  r¿   z#_evaluate_stage1.<locals>.<genexpr>ÚbidÚaskNr   zprice $r—   z below VWAP $zVWAP sloping downr   zspread z% exceeds max ú%Únegativeztrend negative (slope z.3fz%, flat band r˜   ú lower-highs/lower-lows structurer    zvolume pace zx normal below floor rr   rM   Úor   r3   r<   r6   r   r:   r;   r=   r7   r8   r9   zVWAP $zopen $z	extended z+.1fz% above z (max z.0fz%: volume pace ÚstrongÚnormalz	, buying ú)rK   r4   r5   r2   zlast z bars not sloping up (slope z%, needs > Úflatr’   r   r   r
   r}   r�   r   r~   Úflat_threshold_pctÚrecent_slope_pctÚext_from_open_pctÚmax_ext_allowed_pctÚext_from_vwap_pctÚema9Úema20Úlower_highs_lower_lowsr   r   )r   r   Úrvolr|   )ÚpassedÚconstructiveÚhard_reasonsrZ   )r   r   r
   rq   rp   Úlenr¨   r	   r   r   r   r   Ú_spread_pctr   rØ   Úappendrj   r¼   r   rˆ   ),rQ   r�   rÙ   rÚ   rÛ   rm   r¶   rÜ   r’   Ú
vwap_valueÚvslopeÚar}   r   r~   r§   r¦   ró   rô   ÚlhlÚpullback_okÚ	vol_accelÚdist_from_highÚspreadrö   Úelapsed_fractionr‹   rù   ÚextÚopen_pxÚext_pctÚvwap_ext_pctÚallowedÚby_vwapÚvalÚ
strong_volÚ
strong_buyrn   ÚrefÚruleÚrecent_slopeÚbarrø   rZ   rf   rf   rg   Ú_evaluate_stage1<  sÚ   $






ÿÿÿ

ÿ
ÿ
$ÿÿ
ÿ
ÿ
þ
þþ
ý
ý
üûúù
ø
ø	÷	÷

ö
ô
ür  Úatr_valc                 C   sò   t | |ƒ\}}| ||d   }t| | dƒ}|du r:|d r)| ||d   }d}ndddt|dƒt|dƒddd	d
œS ||  }|| }	|	|d k}
d|d›d|› d|d›d|	d›d|d › d�}|
t|dƒ|t|dƒt|dƒt|dƒt|	dƒ|d
œS )z:Checked once, fresh, the instant Stage 2's hold completes.r%   ç{®Gáz„?Nr'   r(   Úclear_air_atr_projectionTr   zFclear air -- no known resistance above price, reward:risk gate skipped)r÷   ÚtargetÚtarget_labelr™   ÚriskÚrewardÚreward_risk_ratioÚreasonr&   ztarget r—   r–   z), stop z, reward:risk z (min rì   r�   )rµ   rp   rˆ   )r’   r  r©   rm   r  r  r™   r  r  Úratior÷   r  rf   rf   rg   Ú_evaluate_stage3›  s2   üÿÿür  c              	   C   s°  | ||d   }t | | dƒ}dd„ |pi  ¡ D ƒ}t| |ƒ\}}	|du }
t|dƒt|dƒ|dur6t|dƒnd|	|
|durBt|dƒnddœ}|du rSi |¥dd	d
œ¥S |dk rei |¥dd|d›d�d
œ¥S ||d kr‚i |¥dd|d›d|d d›d|d›�d
œ¥S ||d krÄ|
r¡i |¥dd|d›d|d d›d|d›�d
œ¥S i |¥d|durºd|d›d|	› d|d›d�d
œ¥S d|d›d�d
œ¥S i |¥dd|d›d|d d›d�d
œ¥S )a¸  [2026-09-17] Trade-flow-imbalance Stage 3 -- see evaluate()'s
    docstring for the two-tier logic. Still computes stop/risk off the
    same ATR formula reward:risk used, since position_manager.py sizes
    every position off this stop regardless of which Stage 3 is active.

    The "has price crossed resistance" check deliberately excludes
    "session_high" from resistance_levels: session_high is
    max(bar.high for bar in bars) recomputed live every poll, so during
    an active rally it's never more than a few cents above price by
    construction -- it isn't a real level, it's just "wherever price
    was a moment ago," and treating it as resistance would make the
    30%-tier's "cross resistance first" condition nearly impossible to
    satisfy during a genuine breakout (this is Bug #4 from the
    2026-09-17 review). The static, pre-scan levels (premarket_high,
    prev_day_high, range_20d_high) are what this checks against.
    r%   r  c                 S   s(   i | ]\}}|d krt  |¡r||“qS )Úsession_highrª   r¬   rf   rf   rg   Ú
<dictcomp>Ï  s    ÿz._evaluate_stage3_imbalance.<locals>.<dictcomp>Nr   )r™   r  r  r  Úcrossed_resistancer{   Fz5no trade-flow imbalance data yet (cold stream buffer))r÷   r  r   z
imbalance z+.0%z, is negative (net selling) -- refusing entryr,   Tz >= z.0%z buy threshold, stop r—   r.   z and resistance cleared, stop z& confirms momentum but hasn't cleared z $z yetz, confirms momentum, awaiting resistance dataz below z confirmation floor)rp   r³   rµ   rˆ   )r’   r  r©   rÜ   rm   r™   r  Úreal_levelsr  r  Úcrossedrn   rf   rf   rg   Ú_evaluate_stage3_imbalanceº  sd   ýÿÿ
ÿÿÿ
ÿÿÿ
ÿÿÿÿý
ÿÿr$  Úpersistence_stateÚas_ofc
                 C   sæ  t |	ƒ}	|pt tj¡}
|pi }t|ƒdk r!t| tdgddid�S t| |||||	|
|d�}|d d }|rG| 	d	¡rG| 	d
¡rG|d	 |d
  nd}t
 ||pOi  	d¡||	d ¡}|d |d t|d dƒ|	d d dœ|d d< d}|	d r´tj| ||||
|	d d r‡|d nd|d�}| ¡ |d d< t  d¡d }|d r´|d r´|j 	d¡r´dd„ |d D ƒ|d< |d rÈt| td|d |d ddid�S |d  sÛt| tdd!g|d ddid�S | 	d¡}| 	d"¡}|durû|durû|
|  ¡ }||	d# krûd}|pþ|
}|
|  ¡ }||	d$ k �r't| td%|d&|d'›d(|	d$ › d)�g|d ||
d*œd+�S |d d }|d d, }|du�r”|jtjk|j|j|j|j| ¡ d-œ}i |d ¥d.|i¥}|jtjk�r{t| td%d%|d%d/|d'›d)�| ¡ t|||jƒg|||
d*œd0�	S t| td%|dd/|d'›d1| ¡ › �g|||
d*œd2�S |	d3 �r¡t||||	ƒ}nt|||||	ƒ}|	d d �rç| 	d4¡du�rç||d  }||d4 k �rçt|dƒ|d4< t|| dƒ|d5< |d6  d7|d › d8|d9›d:�7  < i |d ¥d.|i¥}|d; �rZt|d ||	ƒ}|d< |d<< |d= |d>< |d< |	d? k �r3t| td%|d%d/|d'›d@|d< d'›dA|	d? d'›�g|||
d*œd2�S t| td%d%|d%d/|d'›d)�|d6 t|||d4 ƒdB|d< d'›�g|||
d*œd0�	S t| td%|ddC|d'›dD|d6 › �g|||
d*œd2�S )Ea-  
    bars: recent 1-min bars for this symbol, oldest first, each
        {"t","o","h","l","c","v"} -- enough history for the configured
        atr_period/structure_lookback_bars (a short rolling window, not
        the whole day).
    cumulative_volume_today: total volume for this symbol since the
        session opened (kept by the caller/stream, NOT derived from
        `bars` -- `bars` is a short window, this is a running total).
    avg_vol_baseline: real prior-day volume for this symbol.
    resistance_levels: {label: level_or_None}, e.g. session_high,
        premarket_high, prev_day_high, range_20d_high -- any subset.
    persistence_state: caller-held {"since": datetime_or_None,
        "last_seen": datetime_or_None} for THIS symbol, fed back in on
        every call (fresh {} = cold start). Same contract as
        fast_entry_gate.evaluate_fast_entry().
    quote: {"bid": float, "ask": float}, or None to skip the spread
        check (fails open, same as fast_entry_gate's features.quote
        contract when a quote isn't available).
    trade_imbalance: (buy_vol-sell_vol)/(buy_vol+sell_vol) over the last
        cfg["imbalance_window_seconds"] of trades (stream.py's
        get_trade_imbalance()), or None if there's no trade data yet.
        Only used when cfg["use_reward_risk_gate"] is False (the
        default) -- ignored entirely by the reward:risk Stage 3.
    as_of: real wall-clock time by default; pass the bar's own
        timestamp for a replay/backtest, same contract as every other
        as_of-taking function in this codebase.
    r�   zinsufficient bar dataÚsinceN)rQ   rR   rY   r[   )rÜ   rZ   r’   rå   rä   r0   Ú	daily_atrrN   r•   r“   Úfloorr   r3   )r•   r“   rN   Úmin_stop_enabledrk   rL   )Ú
stop_floorrÜ   ÚplanÚcontext_rulesÚpullback_exempt_lower_highsÚpullback_candidatec                 S   s   g | ]}|d kr|‘qS )rè   rf   )r¢   Úrrf   rf   rg   r¤   9  s    ÿzevaluate.<locals>.<listcomp>rù   F)rQ   rR   rT   rY   rZ   r[   rø   z;not yet constructive (trend/EMA/VWAP alignment not bullish)Ú	last_seenr#   r!   Tzholding r”   zs / Ús)r'  r1  )rQ   rR   rT   rU   rX   rZ   r[   r
   )r÷   r™   r  Úreward_riskÚsetupr  rz   zheld )	rQ   rR   rS   rT   rU   rV   rX   rZ   r[   zs, plan says )rQ   rR   rT   rU   rV   rY   rZ   r[   r)   r™   r  r  z (stop widened to z-volatility floor r—   rì   r÷   rO   r‡   Úentry_score_breakdownr/   z's, room-to-run passed, but entry_score z below floor zentry_score zmomentum confirmed (zs) but room-to-run failed: )ro   r   r¶   r   Úutcrú   rP   ra   r  rj   rk   r+  rˆ   Úsetup_analyzerÚanalyzeÚto_dictrZ   ÚSTATE_REJECTrË   rR   r   r™   r  r3  r4  ÚsummaryÚ	STATE_BUYrœ   r  r$  r�   )rQ   r�   rÙ   rÚ   r©   r%  rÛ   rÜ   r&  rm   r¶   Ústage1Ú	price_nowÚ
spread_absr‘   r,  Úctxr'  r1  ÚgapÚelapsedr’   r  rz   rZ   ÚflooredrO   rf   rf   rg   Úevaluateü  sä   þÿ,ÿþ
ÿýý

ý
ÿ 
ü
ü
$

ÿÿ
û ÿ
û

ürD  rº   )NNNN)-Ú__doc__Údataclassesr   r   r   r   Úconfig_loaderr   r7  rk   Ú
indicatorsr   r   r	   r
   r   r   r   r   r   r   rû   r   r   r<  ra   r:  ri   rP   ro   rs   rc   rx   re   r�   r_   rœ   rd   r¨   rµ   rØ   r  r  r$  rD  rf   rf   rf   rg   Ú<module>   sæ    E8ÿ
þýüûúùø	÷
öõôóòñðè
"
þóÉI	Aþÿÿÿÿþ
þ_ÿ
ÿCýÿÿÿþýýý