o
    ÷y½jmp  ã                   @   sœ   d Z ddlZddlZddlZddlmZmZm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 ed	ƒZG d
d„ dƒZG dd„ dƒZdS )a7  
stream.py

Manages the Alpaca SIP real-time stream for a set of symbols. Buffers
incoming trades/quotes/bars into an in-memory per-symbol rolling window
that fast_pipeline.py and position_manager.py read from.

Runs the stream in a background thread so monitor.py's main loop stays
simple synchronous polling logic (poll the buffer, not the network).

Handles:
- reconnect with backoff (config: streaming.reconnect_backoff_seconds)
- stale data detection (config: streaming.stale_data_seconds)
- symbol subscribe/unsubscribe as the watchlist changes through the day
é    N)ÚdefaultdictÚdequeÚOrderedDict)ÚdatetimeÚtimezoneÚ	timedelta)Ú
get_config)Ú
get_logger)Ú
get_client)Úsession_open_dtÚstreamc                   @   s  e Zd ZdZd:dd„ZdZdedefd	d
„Zdd„ Z	dd„ Z
d;defdd„Zd<defdd„Zdefdd„Zdefdd„Zdd„ Zdd„ Zd=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fd*d+„Zd(ed)efd,d-„Zd.d/„ Zd0d1„ Zd2d3„ Zd=defd4d5„Zd6edefd7d8„Zd9S )>ÚSymbolBuffera	  Rolling window of 1-min bars + latest quote for one symbol, plus a
    second, parallel sub-minute rolling window (bars_sub / get_bars_sub())
    at a configurable bucket width for finer-grained stream-feature work.
    See config.json's streaming._note_sub_minute.é†  é   c                 C   sd   || _ tƒ | _d | _d | _d | _d | _|| _|| _tƒ | _	d | _
tƒ | _tƒ | _d| _t ¡ | _d S ©Nr   )Úmaxlenr   ÚbarsÚlatest_quoteÚlatest_trade_priceÚlast_updateÚ_current_barÚsub_bucket_secondsÚ
sub_maxlenÚbars_subÚ_current_bar_subr   Ú
_trade_logÚmin_flowÚ_flow_last_sideÚ	threadingÚRLockÚ_lock)Úselfr   r   r   © r"   ú!/var/www/screener/trade/stream.pyÚ__init__%   s   zSymbolBuffer.__init__éZ   ÚpriceÚreturnc                 C   sf   | j du rdS | j \}}}|r|sdS ||krdS ||krdS || d }||kr+dS ||k r1dS dS )z@+1 buyer-initiated, -1 seller-initiated, 0 unknown/no quote yet.Nr   é   éÿÿÿÿé   )r   )r!   r&   ÚbidÚaskÚ_Úmidr"   r"   r#   Ú_classify_tradem   s   
zSymbolBuffer._classify_tradec                 C   sZ   |t | jd� }| jr'| jd d |k r+| j ¡  | jr)| jd d |k sd S d S d S d S )N©Úsecondsr   )r   Ú_TRADE_LOG_MAX_AGE_SECONDSr   Úpopleft)r!   Únow_tsÚcutoffr"   r"   r#   Ú_trim_trade_log   s   
(ÿzSymbolBuffer._trim_trade_logc                 C   s¢  d\}}}| j �¿ |D ]¢\}}}}|dkr||f}q||}	}
d}|r_|d r_|d r_|d |d   kr9dkr_n n$|\}}|| d }|	|krKdn|	|krQdn|	|krWdn|	|k r]dnd}|dkru|duru|	|krmdn|	|k rsdn|}|	|}}|jddd�}| j |¡}|du r”d	d	g }| j|< |dkr¡|d  |
7  < q|dk r­|d  |
7  < qtt| j ¡ ƒd
d… ƒ| _|| _W d  ƒ dS 1 sÊw   Y  dS )a«  [2026-09-30] Pre-load min_flow from historical trades/quotes (a stock added
        mid-day by a rescan had only seconds of buy/sell data: SSL 9/30 was bought 16 s
        after it was added on a '100% buy share'). events: [(ts, "trade"|"quote", a, b)]
        sorted by time. Same classification as live (quote rule, tick-rule fallback);
        only min_flow is touched -- bars/volume already come from the official backfill.)NNr   Úquoter   r(   r*   r)   N©ÚsecondÚmicrosecondç        iÄÿÿÿ)r    Úreplacer   Úgetr   ÚsortedÚitemsr   )r!   ÚeventsÚqÚlast_pÚ	last_sideÚtsÚkindÚaÚbr&   ÚsizeÚsider+   r,   r.   ÚmbÚfr"   r"   r#   Úbackfill_flow„   s6   

44
€"ézSymbolBuffer.backfill_flowé   Úminutesc                 C   sb   | j � t| j ¡ ƒ| d… }W d  ƒ n1 sw   Y  tdd„ |D ƒƒtdd„ |D ƒƒfS )z^(buy_vol, sell_vol) over the last `minutes` minute buckets (incl. the
        forming minute).Nc                 s   ó   � | ]}|d  V  qdS )r   Nr"   ©Ú.0Úxr"   r"   r#   Ú	<genexpr>©   ó   € z(SymbolBuffer.get_flow.<locals>.<genexpr>c                 s   rO   )r(   Nr"   rP   r"   r"   r#   rS   ©   rT   )r    Úlistr   ÚvaluesÚsum)r!   rN   r?   r"   r"   r#   Úget_flow¤   s   ÿ$zSymbolBuffer.get_flowç      4@Úwindow_secondsc           
      C   sÂ   | j �H | js	 W d  ƒ dS | jd d }|t|d� }d }}t| jƒD ]\}}}||k r2 n|dkr;||7 }q'|dk rC||7 }q'W d  ƒ n1 sNw   Y  || }	|	r_|| |	 S dS )zî(buy_vol - sell_vol) / (buy_vol + sell_vol) over the last
        window_seconds of classified trades, or None if there's nothing
        to compute it from (fails open, same convention as spread_pct
        when a quote isn't available).Nr)   r   r0   r;   )r    r   r   Úreversed)
r!   rZ   r4   r5   Úbuy_volÚsell_volrD   rI   rH   Útotalr"   r"   r#   Úget_trade_imbalance«   s$   þ
€ôz SymbolBuffer.get_trade_imbalanceÚbarc                 C   sˆ   | j �7 |d }|| j|< | j |¡ t| jƒ| jkr2| jjdd� t| jƒ| jksW d   ƒ d S W d   ƒ d S 1 s=w   Y  d S ©NÚtF©Úlast)r    r   Úmove_to_endÚlenr   Úpopitem©r!   r`   Úkeyr"   r"   r#   Ú_upsert_barÀ   s   
ÿü"üzSymbolBuffer._upsert_barc                 C   sT   |d }|| j |< | j  |¡ t| j ƒ| jkr(| j jdd� t| j ƒ| jksd S d S ra   )r   re   rf   r   rg   rh   r"   r"   r#   Ú_upsert_bar_subÈ   s   
ÿzSymbolBuffer._upsert_bar_subc                 C   s"   | j }|j| | }|j|dd�S )Nr   r8   )r   r9   r<   )r!   rD   ÚwidthÚbucket_secondr"   r"   r#   Ú_sub_bucket_forÏ   s   zSymbolBuffer._sub_bucket_forc              
   C   sô   |   |¡}| jdu p| jd |k}|r+| jdur|  | j¡ ||||||ddddœ	| _| j}|sNt|d |ƒ|d< t|d |ƒ|d< ||d< |d  |7  < |d	  d
7  < |durv||krh|d  d
7  < dS ||k rx|d  d
7  < dS dS dS )aÎ  
        [SIMULATION-ONLY 2026-09-05] prev_price is the trade price
        immediately before this one (captured by on_trade() BEFORE it
        overwrites self.latest_trade_price) -- compared against the new
        price for tick direction (upticks/downticks), deliberately
        continuous across bucket boundaries rather than resetting
        direction tracking to "unknown" at the start of every new bucket,
        so a bucket's uptick_ratio isn't artificially diluted by treating
        its first tick as directionless. tick_count/upticks/downticks are
        plain O(1) counters -- no raw ticks are stored, so this adds no
        unbounded memory regardless of how many trades print per bucket.
        Nrb   r   )	rb   ÚoÚhÚlÚcÚvÚ
tick_countÚupticksÚ	downticksrp   rq   rr   rs   rt   r(   ru   rv   )rn   r   rk   ÚmaxÚmin)r!   r&   rH   rD   Ú
prev_priceÚbucketÚis_new_bucketrG   r"   r"   r#   Ú_accumulate_bar_subÔ   s,   

þüz SymbolBuffer._accumulate_bar_subTc                 C   óx   | j �/ t| j ¡ ƒ}|r"| jd ur*|t| jƒg }W d   ƒ |S W d   ƒ |S W d   ƒ |S 1 s5w   Y  |S ©N)r    rU   r   rV   r   Údict©r!   Úinclude_formingr   r"   r"   r#   Úget_bars_sub÷   ó   
ýþ
þþ
þüzSymbolBuffer.get_bars_subrH   c                 C   ó:   | j � |  |||¡ W d   ƒ d S 1 sw   Y  d S r~   )r    Ú_on_trade_locked)r!   r&   rH   rD   r"   r"   r#   Úon_tradeþ   ó   "ÿzSymbolBuffer.on_tradec           	      C   s(  | j }|| _ || _|  |||¡ |  ||||¡ |  |¡}| j |||f¡ |  |¡ |}|dkrD|d urD||kr;dn||k rAdn| j}|| _|j	ddd�}| j
 |¡}|d u rvddg }| j
|< t| j
ƒdkrv| j
jdd� t| j
ƒdksh|dkr„|d  |7  < d S |dk r’|d  |7  < d S d S )	Nr   r(   r)   r8   r;   é<   Frc   )r   r   Ú_accumulate_barr|   r/   r   Úappendr6   r   r<   r   r=   rf   rg   )	r!   r&   rH   rD   ry   rI   ÚfsiderJ   rK   r"   r"   r#   r…     s0   

ÿÿzSymbolBuffer._on_trade_lockedr+   r,   c                 C   r„   r~   )r    Ú_on_quote_locked©r!   r+   r,   rD   r"   r"   r#   Úon_quote  r‡   zSymbolBuffer.on_quotec                 C   s   |||f| _ || _d S r~   )r   r   r�   r"   r"   r#   rŒ     s   
zSymbolBuffer._on_quote_lockedc              	   C   s@   | j � |  ||||||¡ W d  ƒ dS 1 sw   Y  dS )zBDirect minute-bar ingestion (Alpaca also streams aggregated bars).N)r    Ú_on_bar_locked)r!   ro   rp   rq   rr   rs   rD   r"   r"   r#   Úon_bar"  s   "ÿzSymbolBuffer.on_barc              	   C   sd   t |dƒr|jddd�n|}|  ||||||dœ¡ || _| jd ur.| jd |kr0d | _d S d S d S )Nr<   r   r8   ©rb   ro   rp   rq   rr   rs   rb   )Úhasattrr<   rj   r   r   )r!   ro   rp   rq   rr   rs   rD   Úminute_bucketr"   r"   r#   r�   '  s   
ÿzSymbolBuffer._on_bar_lockedc                 C   sš   |j ddd�}| jd u s| jd |kr*| jd ur|  | j¡ ||||||dœ| _d S | j}t|d |ƒ|d< t|d |ƒ|d< ||d< |d  |7  < d S )	Nr   r8   rb   r‘   rp   rq   rr   rs   )r<   r   rj   rw   rx   )r!   r&   rH   rD   r“   rG   r"   r"   r#   r‰   2  s   
ÿzSymbolBuffer._accumulate_barc                 C   r}   r~   )r    rU   r   rV   r   r   r€   r"   r"   r#   Úget_barsA  rƒ   zSymbolBuffer.get_barsÚstale_secondsc                 C   sH   | j d u rdS t tj¡}| j }|jd u r|jtjd�}||  ¡ |kS )NT)Útzinfo)r   r   Únowr   Úutcr–   r<   Útotal_seconds)r!   r•   r—   rd   r"   r"   r#   Úis_staleH  s   

zSymbolBuffer.is_staleN)r   r   r   ©rM   ©rY   )T)Ú__name__Ú
__module__Ú__qualname__Ú__doc__r$   r2   ÚfloatÚintr/   r6   rL   rX   r_   r   rj   rk   rn   r|   rU   r‚   r†   r…   rŽ   rŒ   r�   r�   r‰   r”   Úboolrš   r"   r"   r"   r#   r      s.    
F #r   c                   @   s  e Zd Zdd„ Zd-dedefdd„Zdefdd	„Zdefd
d„Zdd„ Z	defdd„Z
defdd„Zdd„ Zdd„ Zdd„ Zdefdd„Zdedefdd„Zdedefdd„Zdefd d!„Zd.ded#efd$d%„Zd-dedefd&d'„Zdedefd(d)„Zdefd*d+„Zd,S )/ÚStreamManagerc                    sš   t ƒ d | _tƒ | _| j dd¡‰ | j dd¡}tdt|d ˆ  ƒƒ‰t‡ ‡fdd	„ƒ| _t	ƒ | _
d | _t	ƒ | _d | _d | _d | _t ¡ | _t ¡ | _d S )
NÚ	streamingÚsub_minute_bucket_secondsr   Úsub_minute_buffer_minutesrM   r(   rˆ   c                      s   t ˆ ˆd�S )N©r   r   )r   r"   r¨   r"   r#   Ú<lambda>Z  s    z(StreamManager.__init__.<locals>.<lambda>)r   Úcfgr
   Úclientr=   rw   r¢   r   ÚbuffersÚsetÚ_subscribedÚshadowÚ	bars_onlyÚ_streamÚ_threadÚ_loopr   ÚEventÚ_stop_eventÚ
_connected)r!   Úsub_buffer_minutesr"   r¨   r#   r$   S  s    ÿ
zStreamManager.__init__rM   ÚsymbolsrN   c                 C   s"  t  tj¡}tƒ  tj¡}||krdS t||t|d� ƒ}|D ]o}zP| j 	|||¡}| j 
|||¡}tdd„ |D ƒdd„ |D ƒ dd„ d�}	| j|  |	¡ | j|  |¡\}
}t d	|› d
t|ƒ› d|› d|
d›d|d›�
¡ W q tyŽ } zt d|› d
|› �¡ W Y d}~qd}~ww dS )zž[2026-09-30] Last `minutes` of trades + quotes from Alpaca -> buy/sell per
        minute (SymbolBuffer.backfill_flow), for symbols subscribed after the open.N)rN   c                 S   ó$   g | ]}|d  d|d |d f‘qS )rb   ÚtradeÚpÚsr"   )rQ   rb   r"   r"   r#   Ú
<listcomp>w  ó   $ z0StreamManager._backfill_flow.<locals>.<listcomp>c                 S   r¹   )rb   r7   rG   rF   r"   rP   r"   r"   r#   r½   x  r¾   c                 S   s   | d S r   r"   )Úer"   r"   r#   r©   x  s    z.StreamManager._backfill_flow.<locals>.<lambda>)ri   z[STREAM] Flow backfilled ú: z trades over z
 min, buy z.0fz / sell z"[STREAM] Flow backfill failed for )r   r—   r   r˜   r   Ú
astimezonerw   r   r«   Úget_historical_tradesÚget_historical_quotesr>   r¬   rL   rX   ÚlogÚinforf   Ú	ExceptionÚwarning)r!   r¸   rN   r—   Úopen_utcÚstartÚsymbolÚtradesÚquotesÚevÚbvÚsvr¿   r"   r"   r#   Ú_backfill_flowk  s4   ÿÿÿÿ"€ÿözStreamManager._backfill_flowc           
         s^  t ƒ }t tj¡}|| tj¡krdS |D ]—}z| jj|||d�}W n ty? } zt	 
d|› d|› �¡ W Y d}~qd}~ww |sCq| j| }|D ]%}|jjddd�}	| |	t|jƒt|jƒt|jƒt|jƒt|jƒdœ¡ qJ| jdur•zddlm‰  | j |‡ fd	d
„|D ƒ¡ W n	 ty”   Y nw t	 d|› dt|ƒ› dt|d jƒd›d�¡ qdS )a´  
        [BUGFIX 2026-09-09] stream_features.compute_features() treats
        bars[0] as "the session's first 1-min bar" (session_open in its
        extension_from_open_pct calc) on the assumption a symbol's
        buffer always spans the whole session from 09:30. That's only
        true if the symbol has been subscribed since before the open --
        false for (a) any symbol added mid-day via add_symbols() (no
        history, subscribe_trades/bars only deliver ticks going
        forward), and (b) EVERY symbol after any intraday process
        restart, since SymbolBuffer is in-memory only and starts empty
        again. Confirmed root cause of a real miss: SRAD, added to the
        pool at 12:13 on 2026-09-09 (well after its 11:55 reversal low
        and post a 12:27 restart besides), had its "session open" read
        as ~$12.65 (the price when the buffer happened to start) instead
        of the real $12.69 09:30 open -- silently understating how
        extended it actually was, undermining the whole extension gate.
        Fix: before subscribing a symbol to the live stream, pull its
        real 1-min bars since market_open_time via REST and pre-load the
        buffer, so bars[0] is always genuinely the session's first bar
        regardless of when/why the subscription started. No-op before
        the open (nothing to backfill yet -- the live stream will
        deliver the real bar 0 itself, same as today).
        N)rÉ   Úendz[STREAM] Backfill failed for rÀ   r   r8   r‘   ©ÚBarc              
      s>   g | ]}ˆ |j t|jƒt|jƒt|jƒt|jƒt|jƒƒ‘qS r"   )Ú	timestampr¡   ÚopenÚhighÚlowÚcloseÚvolume)rQ   rG   ©Ú_SBarr"   r#   r½   ®  s
    ÿÿz+StreamManager._backfill.<locals>.<listcomp>z[STREAM] Backfilled z with z  historical bars (session open $z.4fz) before live subscription)r   r   r—   r   r˜   rÁ   r«   Úget_minute_barsrÆ   rÄ   rÇ   r¬   rÔ   r<   rj   r¡   rÕ   rÖ   r×   rØ   rÙ   r¯   Úentry_rulesrÓ   Ú	seed_barsrÅ   rf   )
r!   r¸   Úopen_dtr—   rÊ   r   r¿   Úbufr`   r“   r"   rÚ   r#   Ú	_backfill€  sD   €þ

þ
ÿÿÿêzStreamManager._backfillc                    sT   ˆ   |¡ ˆ  ‡ fdd„|D ƒ¡ tjˆ j|fdd�ˆ _ˆ j ¡  ˆ jjdd� d S )Nc                    ó   g | ]	}|ˆ j vr|‘qS r"   ©r°   rP   ©r!   r"   r#   r½   ¸  ó    z'StreamManager.start.<locals>.<listcomp>T)ÚtargetÚargsÚdaemoné
   )Útimeout)	rá   rÐ   r   ÚThreadÚ_run_foreverr²   rÉ   r¶   Úwait)r!   r¸   r"   rä   r#   rÉ   ¶  s
   

zStreamManager.startc                 C   sN   | j  ¡  | jr#| jr%zt | j ¡ | j¡ W d S  ty"   Y d S w d S d S r~   )rµ   r­   r³   r±   ÚasyncioÚrun_coroutine_threadsafeÚstop_wsrÆ   rä   r"   r"   r#   Ústop¾  s   
ÿýzStreamManager.stopc              
      s‚  ‡ fdd„|D ƒ}|rOˆ j durOˆ  jt|ƒ8  _zˆ j jˆ jg|¢R Ž  ˆ j jˆ jg|¢R Ž  W n tyN } zt 	d|› d|› �¡ W Y d}~nd}~ww ‡ fdd„|D ƒ}|r_ˆ j du radS ˆ  
|¡ ˆ  |¡ ˆ j |¡ z1ˆ j jˆ jg|¢R Ž  ˆ j jˆ jg|¢R Ž  ˆ j jˆ jg|¢R Ž  t dt|ƒ› d|› �¡ W dS  tyÀ } zt 	d	|› d|› �¡ W Y d}~dS d}~ww )
aß  [FIX 2026-09-23] Used to schedule _subscribe_async() onto
        self._loop -- but self._stream.run() runs on alpaca-py's OWN loop
        (asyncio.run inside run()), so self._loop never ran and the
        coroutine never executed: added symbols were silently never
        subscribed. alpaca-py's subscribe_*/unsubscribe_* are themselves
        thread-safe (they post onto the stream's running loop and wait),
        so they're called directly from the caller's thread here.c                    s$   g | ]}|ˆ j v r|ˆ jv r|‘qS r"   )r°   r®   ©rQ   r¼   rä   r"   r#   r½   Ð  r¾   z-StreamManager.add_symbols.<locals>.<listcomp>Nz#[STREAM] tick subscribe failed for rÀ   c                    râ   r"   ©r®   rò   rä   r"   r#   r½   Ø  rå   z[STREAM] Subscribed z added symbols: z [STREAM] add_symbols failed for )r±   r°   r­   Úsubscribe_tradesÚ	_on_tradeÚsubscribe_quotesÚ	_on_quoterÆ   rÄ   rÇ   rá   rÐ   r®   ÚupdateÚsubscribe_barsÚ_on_barrÅ   rf   )r!   r¸   Úpromoter¿   Únew_symsr"   rä   r#   Úadd_symbolsÆ  s2   
"€ÿ

 $€ÿzStreamManager.add_symbolsc              
      sÊ   ‡ fdd„|D ƒ}|sdS ˆ j  |¡ ˆ jdurJzˆ jj|Ž  ˆ jj|Ž  ˆ jj|Ž  W n tyI } zt d|› d|› �¡ W Y d}~nd}~ww |D ]	}ˆ j	 
|d¡ qLt dt|ƒ› d|› �¡ dS )z«Unsubscribes and drops the buffer for symbols no longer
        watched, so the subscription set stays ~one scanner list wide
        instead of growing with every rescan.c                    s   g | ]	}|ˆ j v r|‘qS r"   ró   rò   rä   r"   r#   r½   ê  rå   z0StreamManager.remove_symbols.<locals>.<listcomp>Nz#[STREAM] remove_symbols failed for rÀ   z[STREAM] Unsubscribed z dropped symbols: )r®   Údifference_updater±   Úunsubscribe_tradesÚunsubscribe_quotesÚunsubscribe_barsrÆ   rÄ   rÇ   r¬   ÚpoprÅ   rf   )r!   r¸   Úgoner¿   Úsymr"   rä   r#   Úremove_symbolsæ  s    
"€ÿzStreamManager.remove_symbolsc                 Ã   sv   �| j |j }| t|jƒt|jƒ|j¡ | jd ur9z| j |j|jt|jƒt|jƒ¡ W d S  ty8   Y d S w d S r~   )	r¬   rÊ   r†   r¡   r&   rH   rÔ   r¯   rÆ   )r!   rº   rà   r"   r"   r#   rõ   ú  s   €
(ÿýzStreamManager._on_tradec              	   Ã   s¢   �| j |j }|jrK|jrM| t|jƒt|jƒ|j¡ | jd urOz | j |j|jt|jƒt|jƒt|jp5dƒt|j	p;dƒ¡ W d S  t
yJ   Y d S w d S d S d S r   )r¬   rÊ   Ú	bid_priceÚ	ask_pricerŽ   r¡   rÔ   r¯   Úbid_sizeÚask_sizerÆ   )r!   r7   rà   r"   r"   r#   r÷     s   €
 
ÿÿúzStreamManager._on_quotec                 Ã   s¶   �| j |j }| t|jƒt|jƒt|jƒt|jƒt|jƒ|j	¡ | j
d urYz(ddlm} | j
 |j||j	t|jƒt|jƒt|jƒt|jƒt|jƒƒ¡ W d S  tyX   Y d S w d S )Nr   rÒ   )r¬   rÊ   r�   r¡   rÕ   rÖ   r×   rØ   rÙ   rÔ   r¯   rÝ   rÓ   rÆ   )r!   r`   rà   rÛ   r"   r"   r#   rú     s   €ÿ
 ÿÿûzStreamManager._on_barc           	         s¨  ˆ j d }ˆ j d }d}ˆ j ¡ sÇ||k rÇzct ¡ ˆ _t ˆ j¡ ˆ j ¡ ˆ _	ˆ j
s/t|ƒˆ _
tˆ j
ƒ}‡ fdd„|D ƒ}|rUˆ j	jˆ jg|¢R Ž  ˆ j	jˆ jg|¢R Ž  ˆ j	jˆ jg|¢R Ž  t dt|ƒ› d�¡ ˆ j ¡  d}ˆ j	 ¡  W nE ty½ } z9ˆ j ¡ r‹W Y d }~n<|t|t|ƒd ƒ }t d	|› d
|› d|d › d|› d�	¡ t |¡ |d7 }W Y d }~nd }~ww ˆ j ¡ sÇ||k s||krÒt d¡ d S d S )NÚreconnect_backoff_secondsÚmax_reconnect_attemptsr   c                    râ   r"   rã   rP   rä   r"   r#   r½   +  rå   z.StreamManager._run_forever.<locals>.<listcomp>z#[STREAM] Connecting SIP stream for z symbolsr(   z[STREAM] Disconnected (z); reconnecting in zs (attempt ú/ú)zf[STREAM] Max reconnect attempts exceeded. Stream is DOWN. monitor.py should fall back to REST polling.)rª   rµ   Úis_setrî   Únew_event_loopr³   Úset_event_loopr«   Ú
new_streamr±   r®   r­   r>   rô   rõ   rö   r÷   rù   rú   rÄ   rÅ   rf   r¶   ÚrunrÆ   rx   rÇ   ÚtimeÚsleepÚerror)	r!   r¸   ÚbackoffsÚmax_attemptsÚattemptÚcurrentÚticksr¿   Údelayr"   rä   r#   rì     sJ   






ÿ
ÿ
€ùëÿzStreamManager._run_foreverrÊ   r'   c                 C   ó   | j |  ¡ S r~   )r¬   r”   ©r!   rÊ   r"   r"   r#   r”   C  s   zStreamManager.get_barsc                 C   r  )z½Sub-minute (configurable bucket width, default 30s) rolling
        bars -- see SymbolBuffer.get_bars_sub() / config.json's
        streaming._note_sub_minute. Not consumed by anything yet.)r¬   r‚   r  r"   r"   r#   r‚   F  s   zStreamManager.get_bars_subc                 C   s   | j | jS r~   )r¬   r   r  r"   r"   r#   Ú	get_quoteL  s   zStreamManager.get_quoterY   rZ   c                 C   ó   | j |  |¡S r~   )r¬   r_   )r!   rÊ   rZ   r"   r"   r#   r_   O  s   z!StreamManager.get_trade_imbalancec                 C   r  )zF(buy_vol, sell_vol) over the last `minutes` minutes (entry_v2 step 3).)r¬   rX   )r!   rÊ   rN   r"   r"   r#   rX   R  s   zStreamManager.get_flowc                 C   s   | j |  | jd ¡S )NÚstale_data_seconds)r¬   rš   rª   r  r"   r"   r#   Úis_symbol_staleV  ó   zStreamManager.is_symbol_stalec                 C   s   | j  ¡ o
| j ¡  S r~   )r¶   r  rµ   rä   r"   r"   r#   Ú
is_healthyY  r"  zStreamManager.is_healthyNr›   rœ   )r�   rž   rŸ   r$   rU   r¢   rÐ   rá   rÉ   rñ   rý   r  rõ   r÷   rú   rì   Ústrr”   r‚   r  r¡   r_   rX   r£   r!  r#  r"   r"   r"   r#   r¤   R  s&    6 	(r¤   )r    rî   r   r  Úcollectionsr   r   r   r   r   r   Úconfig_loaderr   Úlogger_setupr	   Úalpaca_clientr
   Úmarket_timer   rÄ   r   r¤   r"   r"   r"   r#   Ú<module>   s      5