o
    5Ÿ©jŽ2  ã                   @   s6  U d Z ddlmZmZ ddlmZ ddlmZmZ ddlm	Z	 e	dƒZ
ddlZddlZddlmZ dd	lmZ dd
lmZmZmZmZ ddlmZmZ ddlmZ ddlmZmZmZ ddlm Z m!Z!m"Z"m#Z#m$Z$ ddl%m&Z& dZ'dZ(de)de*fdd„Z+de)fdd„Z,G dd„ dƒZ-da.ee- e/d< de-fdd„Z0dS )a  
alpaca_client.py

Thin wrapper around alpaca-py. Isolates every direct Alpaca SDK call in
one place so the rest of the system (scanner, entry engine, position
manager) never talks to the SDK directly. This makes it possible to
swap data providers later without touching strategy code, and makes
simulation mode trivial (just don't call the order methods).

Uses:
- alpaca.data.historical for premarket snapshots / bars
- alpaca.data.live for SIP streaming
- alpaca.trading for account, positions, and order submission (paper by default)
é    )ÚdatetimeÚ	timedelta)ÚOptional)Úget_envÚ
get_config)Ú
get_loggerÚalpaca_clientN)ÚStockHistoricalDataClient)ÚStockDataStream)ÚStockBarsRequestÚStockLatestQuoteRequestÚStockLatestTradeRequestÚStockSnapshotRequest)Ú	TimeFrameÚTimeFrameUnit)ÚTradingClient)ÚMarketOrderRequestÚGetAssetsRequestÚGetOrdersRequest)Ú	OrderSideÚTimeInForceÚ
AssetClassÚAssetStatusÚQueryOrderStatus)ÚDataFeediðgi�›hÚexcÚreturnc                 C   s¼   t | ƒ}d}zt |¡}W n< tjtfyI   | d¡}| d¡}|dkrG|dkrG||krGzt |||d … ¡}W n tjyF   d}Y nw Y nw t|tƒrX| 	d¡t
krXdS d| ¡ v S )	a/  
    Returns True if exc is Alpaca's "position not found" rejection
    (code 40410000), False otherwise. Same best-effort JSON-then-regex
    parsing strategy as parse_wash_trade_error() below, for the same
    reason: a minor SDK error-formatting change shouldn't silently
    disable this check.
    Nú{ú}éÿÿÿÿé   ÚcodeTzposition not found)ÚstrÚjsonÚloadsÚJSONDecodeErrorÚ	TypeErrorÚfindÚrfindÚ
isinstanceÚdictÚgetÚPOSITION_NOT_FOUND_ERROR_CODEÚlower)r   ÚtextÚpayloadÚstartÚend© r2   úalpaca_client.pyÚparse_position_not_found_errorH   s$   

ÿ€ú	r4   c                 C   sà   t | ƒ}d}zt |¡}W n< tjtfyI   | d¡}| d¡}|dkrG|dkrG||krGzt |||d … ¡}W n tjyF   d}Y nw Y nw t|tƒr[| 	d¡t
kr[| 	d¡S d| ¡ v rnt d	|¡}|rn| d¡S dS )
a@  
    Alpaca's SDK raises an APIError whose string form is (or embeds) the
    raw JSON error body, e.g.:
        {"code":40310000,"existing_order_id":"...","message":"potential
         wash trade detected. use complex orders","reject_reason":
         "opposite side market/stop order exists"}

    Returns the existing_order_id (str) if this exception is a wash-trade
    rejection that names a conflicting order, otherwise None. Best-effort:
    tries json.loads first, falls back to a regex so a minor SDK
    formatting difference doesn't silently disable the guard.
    Nr   r   r   r    r!   Úexisting_order_idz
wash tradez#"existing_order_id"\s*:\s*"([^"]+)")r"   r#   r$   r%   r&   r'   r(   r)   r*   r+   ÚWASH_TRADE_ERROR_CODEr-   ÚreÚsearchÚgroup)r   r.   r/   r0   r1   Úmr2   r2   r3   Úparse_wash_trade_errorc   s,   

ÿ€ú	

r;   c                   @   sê   e Zd Zdd„ Zdefdd„Zdd„ Zdefd	d
„Zd-de	de
de
fdd„Z	d.dede
de
dede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e	ded e	fd!d"„Zde	fd#d$„Zd%e	fd&d'„Zde	fd(d)„Zd*d+„ Zd,S )/ÚAlpacaClientc                 C   sœ   t ƒ }| ¡  || _tƒ | _t|j|jd|jv d�| _	t
|j|jƒ| _| j di ¡ dd¡}zt|ƒ| _W d S  tyM   t d|› d�¡ tj| _Y d S w )NÚpaper)r=   Ú	streamingÚfeedÚsipzUnknown feed 'z'' in config; defaulting to DataFeed.SIP)r   ÚvalidateÚenvr   Úcfgr   Úapi_keyÚ
secret_keyÚbase_urlÚtradingr	   Ú	hist_datar+   r   Ú_feedÚ
ValueErrorÚlogÚwarningÚSIP)ÚselfrB   r?   r2   r2   r3   Ú__init__‰   s   ÿþzAlpacaClient.__init__r   c                 C   s   t | jj| jj| jd�S )z8Returns a fresh StockDataStream configured for SIP feed.)r?   )r
   rB   rD   rE   rI   ©rN   r2   r2   r3   Ú
new_streamŸ   s   ÿzAlpacaClient.new_streamc                 C   s   t tjtjd�}| j |¡S )N)Úasset_classÚstatus)r   r   Ú	US_EQUITYr   ÚACTIVErG   Úget_all_assets)rN   Úreqr2   r2   r3   Úget_tradable_assets©   s   z AlpacaClient.get_tradable_assetsÚsymbolsc              
   C   s^   |si S t || jd�}z| j |¡W S  ty. } zt d|› �¡ i W  Y d}~S d}~ww )zEBulk snapshot (latest trade/quote/min bar/day bar) for a symbol list.©Úsymbol_or_symbolsr?   zget_snapshots failed: N)r   rI   rH   Úget_stock_snapshotÚ	ExceptionrK   rL   )rN   rY   rW   Úer2   r2   r3   Úget_snapshots­   s   €þzAlpacaClient.get_snapshotséô  Úsymbolr0   r1   c              
   C   s„   t |tdtjƒ|||| jd�}z| j |¡}||jv r || W S g W S  tyA } zt	 
d|› d|› �¡ g W  Y d }~S d }~ww )Nr    ©r[   Ú	timeframer0   r1   Úlimitr?   zget_minute_bars(ú
) failed: )r   r   r   ÚMinuterI   rH   Úget_stock_barsÚdatar]   rK   rL   )rN   ra   r0   r1   rd   rW   Úbarsr^   r2   r2   r3   Úget_minute_bars¸   s    
ú€þzAlpacaClient.get_minute_barsé<   éÈ   rd   Ú
chunk_sizec                 C   sÎ   i }|s|S t dt|ƒ|ƒD ]V}|||| … }t|tdtjƒ|||t|ƒ | jd�}	z| j |	¡}
W n t	yM } zt
 d|› �¡ W Y d}~qd}~ww |D ]}||
jv rcdd„ |
j| D ƒ||< qPq|S )a   
        [FEATURE 2026-09-15] Bulk multi-symbol daily bars, chunked to keep
        each request URL/param size reasonable -- used by
        premarket_scanner.py's volatility-floor filter (see its
        prefilter_by_snapshot() docstring), which needs each symbol's
        real day-to-day range history, not just today's intraday bars.
        One bulk call per chunk instead of one REST call per symbol,
        same efficiency reasoning as get_snapshots() already uses.

        Returns {symbol: [bar dicts]}, oldest first; a symbol with no
        data (delisted, too new, or the request failed) is simply absent
        from the result rather than raising -- callers must treat a
        missing symbol as "unknown," never as "confirmed low volatility."

        [BUGFIX 2026-09-15] `limit` on a MULTI-symbol StockBarsRequest is
        a combined cap across the whole response, not per-symbol --
        confirmed live: requesting 7 symbols with limit=60 silently
        returned 49 bars for the first alphabetically and 11 for the
        second, and NOTHING for the other five (49+11=60 exactly). Every
        chunk request below multiplies `limit` by the chunk size so each
        symbol still gets its full requested history regardless of how
        many other symbols share the request.
        r   r    rb   z$get_daily_bars_bulk() chunk failed: Nc              	   S   s>   g | ]}|j t|jƒt|jƒt|jƒt|jƒt|jƒd œ‘qS ))ÚtÚoÚhÚlÚcÚv)Ú	timestampÚfloatÚopenÚhighÚlowÚcloseÚvolume)Ú.0Úbr2   r2   r3   Ú
<listcomp>õ   s    þÿÿz4AlpacaClient.get_daily_bars_bulk.<locals>.<listcomp>)ÚrangeÚlenr   r   r   ÚDayrI   rH   rg   r]   rK   rL   rh   )rN   rY   r0   r1   rd   rm   ÚoutÚiÚchunkrW   ri   r^   ra   r2   r2   r3   Úget_daily_bars_bulkÈ   s8   

ú€þ

ý€þz AlpacaClient.get_daily_bars_bulkc              
   C   sd   t || jd�}z| j |¡}| |¡W S  ty1 } zt d|› d|› �¡ W Y d }~d S d }~ww )NrZ   zget_latest_quote(re   )r   rI   rH   Úget_stock_latest_quoter+   r]   rK   rL   )rN   ra   rW   Úqr^   r2   r2   r3   Úget_latest_quoteü   s   €þzAlpacaClient.get_latest_quotec              
   C   sL   z| j  |¡W S  ty% } zt d|› d|› �¡ W Y d }~d S d }~ww )Nz
get_asset(re   )rG   Ú	get_assetr]   rK   rL   ©rN   ra   r^   r2   r2   r3   rˆ   	  s   €þzAlpacaClient.get_assetc                 C   s
   | j  ¡ S ©N)rG   Úget_accountrP   r2   r2   r3   r‹     s   
zAlpacaClient.get_accountc              
   C   sF   z| j  ¡ W S  ty" } zt d|› �¡ g W  Y d }~S d }~ww )Nzget_open_positions failed: )rG   Úget_all_positionsr]   rK   rL   ©rN   r^   r2   r2   r3   Úget_open_positions  s   €þzAlpacaClient.get_open_positionsÚqtyÚsidec                 C   s6   |  ¡ dkr	tjntj}t|||tjd�}| j |¡S )NÚbuy)ra   r�   r�   Útime_in_force)	r-   r   ÚBUYÚSELLr   r   ÚDAYrG   Úsubmit_order)rN   ra   r�   r�   Ú
order_siderW   r2   r2   r3   Úsubmit_market_order  s   üz AlpacaClient.submit_market_orderc              
   C   s@   z| j  |¡W S  ty } zt d|› d|› �¡ ‚ d }~ww )Nzclose_position(re   )rG   Úclose_positionr]   rK   Úerrorr‰   r2   r2   r3   r™   $  s   €þzAlpacaClient.close_positionÚorder_idc              
   C   sL   z| j  |¡W S  ty% } zt d|› d|› �¡ W Y d}~dS d}~ww )ztFetch a single order by id -- used to poll a pending close's
        fill status instead of blindly resubmitting it.z
get_order(re   N)rG   Úget_order_by_idr]   rK   rL   )rN   r›   r^   r2   r2   r3   Ú	get_order+  s   €þzAlpacaClient.get_orderc              
   C   s^   t tj|gd�}z| j |¡W S  ty. } zt d|› d|› �¡ g W  Y d}~S d}~ww )zÌOpen (unfilled) orders for a single symbol, most recent first.
        Used to recover the order_id Alpaca is referencing in a wash-trade
        rejection so we can track/poll it instead of resubmitting.)rS   rY   zget_open_orders(re   N)r   r   ÚOPENrG   Ú
get_ordersr]   rK   rL   )rN   ra   rW   r^   r2   r2   r3   Úget_open_orders4  s   €þzAlpacaClient.get_open_ordersc              
   C   s<   z| j jdd�W S  ty } z	t d|› �¡ ‚ d }~ww )NT)Úcancel_orderszclose_all_positions failed: )rG   Úclose_all_positionsr]   rK   rš   r�   r2   r2   r3   r¢   ?  s   €þz AlpacaClient.close_all_positionsN)r`   )rk   rl   )Ú__name__Ú
__module__Ú__qualname__rO   r
   rQ   rX   Úlistr_   r"   r   rj   Úintr*   r„   r‡   rˆ   r‹   rŽ   r˜   r™   r�   r    r¢   r2   r2   r2   r3   r<   ˆ   s.    
ÿÿÿ
ÿ4
	r<   Ú_client_singletonc                   C   s   t d u rtƒ a t S rŠ   )r¨   r<   r2   r2   r2   r3   Ú
get_clientJ  s   r©   )1Ú__doc__r   r   Útypingr   Úconfig_loaderr   r   Úlogger_setupr   rK   r#   r7   Úalpaca.data.historicalr	   Úalpaca.data.liver
   Úalpaca.data.requestsr   r   r   r   Úalpaca.data.timeframer   r   Úalpaca.trading.clientr   Úalpaca.trading.requestsr   r   r   Úalpaca.trading.enumsr   r   r   r   r   Úalpaca.data.enumsr   r6   r,   r]   Úboolr4   r;   r<   r¨   Ú__annotations__r©   r2   r2   r2   r3   Ú<module>   s0    % @